From 9b063a4616d4d39a6b0d009402f7ce47f561dcf6 Mon Sep 17 00:00:00 2001 From: Alejandro Celaya Date: Tue, 21 Aug 2018 20:33:12 +0200 Subject: [PATCH] Implemented importing servers from CSV file --- src/servers/CreateServer.js | 52 ++++++++++---------- src/servers/helpers/ImportServersBtn.js | 63 +++++++++++++++++++++++++ src/servers/reducers/server.js | 12 ++--- src/servers/services/ServersImporter.js | 27 +++++++++++ src/servers/services/ServersService.js | 37 ++++++++------- src/utils/ColorGenerator.js | 3 -- test/servers/reducers/server.test.js | 13 ----- 7 files changed, 140 insertions(+), 67 deletions(-) create mode 100644 src/servers/helpers/ImportServersBtn.js create mode 100644 src/servers/services/ServersImporter.js diff --git a/src/servers/CreateServer.js b/src/servers/CreateServer.js index 6cda5046..5e8e7fe7 100644 --- a/src/servers/CreateServer.js +++ b/src/servers/CreateServer.js @@ -1,34 +1,33 @@ -import { assoc, pick } from 'ramda'; +import { assoc, dissoc, pick, pipe } from 'ramda'; import React from 'react'; import { connect } from 'react-redux'; import { createServer } from './reducers/server'; import { resetSelectedServer } from './reducers/selectedServer'; import { v4 as uuid } from 'uuid'; -import { UncontrolledTooltip } from 'reactstrap'; import './CreateServer.scss'; +import ImportServersBtn from './helpers/ImportServersBtn'; export class CreateServer extends React.Component { state = { name: '', url: '', apiKey: '', + serversImported: false, }; submit = e => { e.preventDefault(); const { createServer, history: { push } } = this.props; - const server = assoc('id', uuid(), this.state); + const server = pipe( + assoc('id', uuid()), + dissoc('serversImported') + )(this.state); createServer(server); push(`/server/${server.id}/list-short-urls/1`) }; - constructor(props) { - super(props); - this.fileRef = React.createRef(); - } - componentDidMount() { this.props.resetSelectedServer(); } @@ -60,30 +59,29 @@ export class CreateServer extends React.Component { {renderInputGroup('apiKey', 'API key')}
- - - You can create servers by importing a CSV file with columns "name", "apiKey" and "url" - - console.log(file)} - accept="text/csv" - className="create-server__csv-select" - ref={this.fileRef} - /> + { + this.setState({ serversImported: true }); + setTimeout(() => this.setState({ serversImported: false }), 4000); + }} />
+ + {this.state.serversImported && ( +
+
+
+ Servers properly imported. You can now select one from the list :) +
+
+
+ )} ); } } -export default connect(pick(['selectedServer']), {createServer, resetSelectedServer })(CreateServer); +export default connect( + pick(['selectedServer']), + {createServer, resetSelectedServer } +)(CreateServer); diff --git a/src/servers/helpers/ImportServersBtn.js b/src/servers/helpers/ImportServersBtn.js new file mode 100644 index 00000000..9d43041f --- /dev/null +++ b/src/servers/helpers/ImportServersBtn.js @@ -0,0 +1,63 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import { UncontrolledTooltip } from 'reactstrap'; +import serversImporter from '../services/ServersImporter'; +import { createServers } from '../reducers/server'; +import { assoc } from 'ramda'; +import { v4 as uuid } from 'uuid'; +import PropTypes from 'prop-types'; + +const defaultProps = { + serversImporter, +}; +const propTypes = { + onChange: PropTypes.func, +}; + +export class ImportServersBtn extends React.Component { + constructor(props) { + super(props); + this.fileRef = React.createRef(); + } + + render() { + const { serversImporter, onImport } = this.props; + const onChange = e => serversImporter.importServersFromFile(e.target.files[0]).then( + servers => { + const { createServers } = this.props; + const serversWithIds = servers.map(server => assoc('id', uuid(), server)); + createServers(serversWithIds); + onImport(serversWithIds); + } + ); + + return ( + + + + + You can create servers by importing a CSV file with columns "name", "apiKey" and "url" + + + + ); + } +} + +ImportServersBtn.defaultProps = defaultProps; +ImportServersBtn.propTypes = propTypes; + +export default connect(null, { createServers })(ImportServersBtn); diff --git a/src/servers/reducers/server.js b/src/servers/reducers/server.js index 08c86c2e..adc6e895 100644 --- a/src/servers/reducers/server.js +++ b/src/servers/reducers/server.js @@ -2,17 +2,11 @@ import ServersService from '../services/ServersService'; import { curry } from 'ramda'; export const FETCH_SERVERS = 'shlink/servers/FETCH_SERVERS'; -export const CREATE_SERVER = 'shlink/servers/CREATE_SERVER'; -export const DELETE_SERVER = 'shlink/servers/DELETE_SERVER'; export default function reducer(state = {}, action) { switch (action.type) { case FETCH_SERVERS: - case DELETE_SERVER: return action.servers; - case CREATE_SERVER: - const server = action.server; - return { ...state, [server.id]: server }; default: return state; } @@ -35,3 +29,9 @@ export const _deleteServer = (ServersService, server) => { return _listServers(ServersService); }; export const deleteServer = curry(_deleteServer)(ServersService); + +export const _createServers = (ServersService, servers) => { + ServersService.createServers(servers); + return _listServers(ServersService); +}; +export const createServers = curry(_createServers)(ServersService); diff --git a/src/servers/services/ServersImporter.js b/src/servers/services/ServersImporter.js new file mode 100644 index 00000000..8e2c364e --- /dev/null +++ b/src/servers/services/ServersImporter.js @@ -0,0 +1,27 @@ +import csvjson from 'csvjson'; + +export class ServersImporter { + constructor(csvjson) { + this.csvjson = csvjson; + } + + importServersFromFile = (file) => { + if (!file || file.type !== 'text/csv') { + return Promise.reject('No file provided or file is not a CSV'); + } + + const reader = new FileReader(); + return new Promise(resolve => { + reader.onloadend = e => { + const content = e.target.result; + const servers = this.csvjson.toObject(content); + + resolve(servers); + }; + reader.readAsText(file); + }); + }; +} + +const serversImporter = new ServersImporter(csvjson); +export default serversImporter; diff --git a/src/servers/services/ServersService.js b/src/servers/services/ServersService.js index 6fda7987..35f7f9b5 100644 --- a/src/servers/services/ServersService.js +++ b/src/servers/services/ServersService.js @@ -1,5 +1,5 @@ import Storage from '../../utils/Storage'; -import { dissoc } from 'ramda'; +import { assoc, dissoc, reduce } from 'ramda'; const SERVERS_STORAGE_KEY = 'servers'; @@ -8,25 +8,26 @@ export class ServersService { this.storage = storage; } - listServers = () => { - return this.storage.get(SERVERS_STORAGE_KEY) || {}; + listServers = () => this.storage.get(SERVERS_STORAGE_KEY) || {}; + + findServerById = serverId => this.listServers()[serverId]; + + createServer = server => this.createServers([server]); + + createServers = servers => { + const allServers = reduce( + (serversObj, server) => assoc(server.id, server, serversObj), + this.listServers(), + servers + ); + this.storage.set(SERVERS_STORAGE_KEY, allServers); }; - findServerById = serverId => { - const servers = this.listServers(); - return servers[serverId]; - }; - - createServer = server => { - const servers = this.listServers(); - servers[server.id] = server; - this.storage.set(SERVERS_STORAGE_KEY, servers); - }; - - deleteServer = server => { - const servers = dissoc(server.id, this.listServers()); - this.storage.set(SERVERS_STORAGE_KEY, servers); - }; + deleteServer = server => + this.storage.set( + SERVERS_STORAGE_KEY, + dissoc(server.id, this.listServers()) + ); } export default new ServersService(Storage); diff --git a/src/utils/ColorGenerator.js b/src/utils/ColorGenerator.js index d7a4f9e2..126df448 100644 --- a/src/utils/ColorGenerator.js +++ b/src/utils/ColorGenerator.js @@ -15,9 +15,6 @@ export class ColorGenerator { constructor(storage) { this.storage = storage; this.colors = this.storage.get('colors') || {}; - - this.getColorForKey = this.getColorForKey.bind(this); - this.setColorForKey = this.setColorForKey.bind(this); } getColorForKey = key => { diff --git a/test/servers/reducers/server.test.js b/test/servers/reducers/server.test.js index cd3f24b1..7fa70143 100644 --- a/test/servers/reducers/server.test.js +++ b/test/servers/reducers/server.test.js @@ -2,8 +2,6 @@ import reduce, { _createServer, _deleteServer, _listServers, - CREATE_SERVER, - DELETE_SERVER, FETCH_SERVERS, } from '../../../src/servers/reducers/server'; import * as sinon from 'sinon'; @@ -24,17 +22,6 @@ describe('serverReducer', () => { expect(reduce({}, { type: FETCH_SERVERS, servers })).toEqual(servers) ); - it('returns servers when action is DELETE_SERVER', () => - expect(reduce({}, { type: DELETE_SERVER, servers })).toEqual(servers) - ); - - it('adds server to list when action is CREATE_SERVER', () => { - const server = { id: 'abc123' }; - expect(reduce({}, { type: CREATE_SERVER, server })).toEqual({ - [server.id]: server - }) - }); - it('returns default when action is unknown', () => expect(reduce({}, { type: 'unknown' })).toEqual({}) );