diff --git a/client/components/error.js b/client/components/error.js deleted file mode 100644 index 978ecd5f..00000000 --- a/client/components/error.js +++ /dev/null @@ -1,15 +0,0 @@ -import React from 'react'; -import { NgIf } from './'; - -import "./error.scss"; - -export const Error = (props) => { - return ( -
- {props.err.message || "Oups something went wrong :/"} - - {JSON.stringify(props.err.trace)} - -
- ); -} diff --git a/client/components/error.scss b/client/components/error.scss deleted file mode 100644 index 5cf5d6d1..00000000 --- a/client/components/error.scss +++ /dev/null @@ -1,13 +0,0 @@ -.component_error{ - text-align: center; - margin-top: 50px; - font-size: 25px; - font-style: italic; - font-weight: 100; - - .trace{ - fontSize: 12px; - maxWidth: 500px; - margin: 10px auto 0 auto; - } -} diff --git a/client/components/index.js b/client/components/index.js index 5f8e771a..59c34c69 100644 --- a/client/components/index.js +++ b/client/components/index.js @@ -7,7 +7,6 @@ export { Container } from './container'; export { NgIf } from './ngif'; export { Card } from './card'; export { Loader } from './loader'; -export { Error } from './error'; export { Fab } from './fab'; export { Icon } from './icon'; export { Uploader } from './uploader'; diff --git a/client/components/notification.js b/client/components/notification.js index ee44a9b8..3d6b1971 100644 --- a/client/components/notification.js +++ b/client/components/notification.js @@ -1,68 +1,107 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; import { NgIf } from './'; +import { notify } from '../helpers/'; import './notification.scss'; export class Notification extends React.Component { constructor(props){ super(props); this.state = { - visible: null, - error: null, - timeout: null + appear: false, + message_text: null, + message_type: null }; - } - componentWillMount(){ - this.componentWillReceiveProps(this.props); - } + function TaskManager(){ + let jobs = []; + let is_running = false; - componentWillUnmount(){ - window.clearTimeout(this.timeout); - } - - componentWillReceiveProps(props){ - if(props.error !== null){ - this.componentWillUnmount(); - this.setState({visible: true, error: props.error}); - this.timeout = window.setTimeout(() => { - this.setState({visible: null}); - }, 5000); + const ret = { + addJob: (job) => { + jobs.push(job); + if(is_running === false){ + is_running = true; + ret._executor(); + } + }, + _executor: () => { + let job = jobs.shift(); + if(!job){ + is_running = false; + return Promise.resolve(); + } + return job().then(ret._executor); + } + }; + return ret; } + this.runner = new TaskManager(); } - toggleVisibility(){ - this.setState({visible: !this.state.visible}); - } - - formatError(err){ - if(typeof err === 'object'){ - if(err && err.message){ - return err.message; - }else{ - return JSON.stringify(err); + componentDidMount(){ + notify.subscribe((_message, type) => { + let job = playMessage.bind(this, { + text: stringify(_message), + type: type + }); + this.runner.addJob(job); + }); + function stringify(data){ + if(typeof data === 'object' && data.message){ + return data.message; + }else if(typeof data === 'string'){ + return data; } - }else if(typeof err === 'string'){ - return err; - }else{ - throw('unrecognized notification'); + return JSON.stringify(data); } + function playMessage(message){ + const displayMessage = (message) => { + this.setState({ + appear: true, + message_text: message.text, + message_type: message.type + }); + return Promise.resolve(message); + }; + const waitForABit = (timeout, message) => { + return new Promise((done, err) => { + window.setTimeout(() => { + done(message); + }, timeout); + }); + }; + const hideMessage = (message) => { + this.setState({ + appear: false + }); + return Promise.resolve(message); + }; + + return displayMessage(message) + .then(waitForABit.bind(this, 5000)) + .then(hideMessage) + .then(waitForABit.bind(this, 1000)); + } + } + + close(){ + this.setState({ appear: false }); } render(){ return ( - -
-
- {this.formatError(this.state.error)} + + +
+
+ { this.state.message_text } +
+
X
-
+ ); } } - -Notification.propTypes = { - error: PropTypes.any -} diff --git a/client/components/notification.scss b/client/components/notification.scss index 37c9eb72..86069670 100644 --- a/client/components/notification.scss +++ b/client/components/notification.scss @@ -1,22 +1,42 @@ .component_notification{ position: fixed; - bottom: 0; - left: 0; + bottom: 25px; + left: 25px; right: 0; - text-align: center; + font-size: 0.95em; + + .component_notification--container{ + width: 400px; - > div{ - display: inline-block; - background: #637d8b; - min-width: 200px; - max-width: 400px; - margin: 0 auto; - padding: 10px 15px; - border-top-left-radius: 3px; - border-top-right-radius: 3px; - color: white; text-align: left; - cursor: pointer; - box-shadow: rgba(0, 0, 0, 0.14) 0px 4px 5px 0px, rgba(0, 0, 0, 0.12) 0px 1px 10px 0px, rgba(0, 0, 0, 0.2) 0px 2px 4px -1px; + display: inline-block; + padding: 15px 25px 15px 15px; + border-radius: 2px; + box-shadow: rgba(0, 0, 0, 0.14) 0px 4px 5px 0px; + display: flex; + align-items: center; + + &.info{ + background: rgba(0,0,0,0.6); + color: white; + } + &.error{ + background: var(--error); + color: var(--secondary); + } + &.success{ + background: var(--success); + color: var(--secondary); + } + + .message{ + flex: 1 1 auto; + } + .close{ + color: rgba(0,0,0,0.3); + cursor: pointer; + padding: 0 2px; + font-size: 0.95em; + } } } diff --git a/client/helpers/index.js b/client/helpers/index.js index 8f04e6cf..4016e64b 100644 --- a/client/helpers/index.js +++ b/client/helpers/index.js @@ -10,3 +10,4 @@ export { prepare } from './navigate'; export { invalidate, http_get, http_post, http_delete } from './ajax'; export { screenHeight } from './dom'; export { prompt } from './prompt'; +export { notify } from './notify'; diff --git a/client/helpers/notify.js b/client/helpers/notify.js new file mode 100644 index 00000000..a671bdb3 --- /dev/null +++ b/client/helpers/notify.js @@ -0,0 +1,17 @@ +const Message = function (){ + let fn = null; + + return { + send: function(text, type){ + if(['info', 'success', 'error'].indexOf(type) === -1){ type = 'info'; } + if(!fn){ return window.setTimeout(() => this.send(text,type), 50); } + fn(text, type); + return Promise.resolve(); + }, + subscribe: function(_fn){ + fn = _fn; + } + }; +}; + +export const notify = new Message(); diff --git a/client/model/files.js b/client/model/files.js index fb8bba9f..9bc3ad7f 100644 --- a/client/model/files.js +++ b/client/model/files.js @@ -84,7 +84,7 @@ class FileSystem{ rm(path){ const url = '/api/files/rm?path='+prepare(path); - this._replace(path, 'loading') + return this._replace(path, 'loading') .then(() => http_get(url)) .then((res) => { if(res.status === 'ok'){ @@ -127,14 +127,14 @@ class FileSystem{ mkdir(path){ const url = '/api/files/mkdir?path='+prepare(path); - this._add(path, 'loading') + return this._add(path, 'loading') .then(() => this._add(path, 'loading')) .then(() => http_get(url)) .then((res) => res.status === 'ok'? this._replace(path) : this._replace(path, 'error')); } touch(path, file){ - this._add(path, 'loading') + return this._add(path, 'loading') .then(() => { if(file){ const url = '/api/files/cat?path='+prepare(path); @@ -152,7 +152,7 @@ class FileSystem{ mv(from, to){ const url = '/api/files/mv?from='+prepare(from)+"&to="+prepare(to); - ui_before_request(from, to) + return ui_before_request(from, to) .then(() => this._ls_from_cache(dirname(from))) .then(() => this._ls_from_cache(dirname(to))) .then(() => http_get(url) diff --git a/client/pages/connectpage.js b/client/pages/connectpage.js index 73847c1c..398ae72a 100644 --- a/client/pages/connectpage.js +++ b/client/pages/connectpage.js @@ -5,7 +5,7 @@ import './connectpage.scss'; import { Session } from '../model/'; import { Container, NgIf, Loader, Notification } from '../components/'; import { ForkMe, RememberMe, Credentials, Form } from './connectpage/'; -import { cache } from '../helpers/'; +import { cache, notify } from '../helpers/'; import config from '../../config_client'; import { Alert } from '../components/'; @@ -17,7 +17,6 @@ export class ConnectPage extends React.Component { credentials: {}, remember_me: window.localStorage.hasOwnProperty('credentials') ? true : false, loading: false, - error: null, doing_a_third_party_login: false }; } @@ -52,9 +51,9 @@ export class ConnectPage extends React.Component { const path = params.path && /^\//.test(params.path)? /\/$/.test(params.path) ? params.path : params.path+'/' : '/'; this.props.history.push('/files'+path); }) - .catch(err => { - this.setState({loading: false, error: err}); - window.setTimeout(() => this.setState({error: null}), 1000); + .catch((err) => { + this.setState({loading: false}); + notify.send(err, 'error'); }); } @@ -64,16 +63,16 @@ export class ConnectPage extends React.Component { Session.url('dropbox').then((url) => { window.location.href = url; }).catch((err) => { - this.setState({loading: false, error: err}); - window.setTimeout(() => this.setState({error: null}), 1000); + this.setState({loading: false}); + notify.send(err, 'error'); }); }else if(source === 'google'){ this.setState({loading: true}); Session.url('gdrive').then((url) => { window.location.href = url; }).catch((err) => { - this.setState({loading: false, error: err}); - window.setTimeout(() => this.setState({error: null}), 1000); + this.setState({loading: false}); + notify.send(err, 'error'); }); } } @@ -118,7 +117,6 @@ export class ConnectPage extends React.Component { onCredentialsFound={this.setCredentials.bind(this)} credentials={this.state.credentials} /> -
); diff --git a/client/pages/filespage.js b/client/pages/filespage.js index 68629888..bbc82ab0 100644 --- a/client/pages/filespage.js +++ b/client/pages/filespage.js @@ -6,8 +6,8 @@ import Path from 'path'; import './filespage.scss'; import { Files } from '../model/'; -import { NgIf, Loader, Error, Uploader, EventReceiver } from '../components/'; -import { debounce, goToFiles, goToViewer, event, screenHeight } from '../helpers/'; +import { NgIf, Loader, Uploader, EventReceiver } from '../components/'; +import { notify, debounce, goToFiles, goToViewer, event, screenHeight } from '../helpers/'; import { BreadCrumb, FileSystem } from './filespage/'; @EventReceiver @@ -81,6 +81,7 @@ export class FilesPage extends React.Component { }); this.setState({files: files, loading: false}); }, (error) => { + notify.send(error, 'error'); this.setState({error: error}); }); this.setState({error: false}); @@ -88,18 +89,26 @@ export class FilesPage extends React.Component { onCreate(path, type, file){ if(type === 'file'){ - return Files.touch(path, file); + return Files.touch(path, file) + .then(() => notify.send('A file named "'+Path.basename(path)+'" was created', 'success')) + .catch((err) => notify.send(err, 'error')); }else if(type === 'directory'){ - return Files.mkdir(path); + return Files.mkdir(path) + .then(() => notify.send('A folder named "'+Path.basename(path)+'" was created', 'success')) + .catch((err) => notify.send(err, 'error')); }else{ return Promise.reject({message: 'internal error: can\'t create a '+type.toString(), code: 'UNKNOWN_TYPE'}); } } onRename(from, to, type){ - return Files.mv(from, to, type); + return Files.mv(from, to, type) + .then(() => notify.send('The file "'+Path.basename(from)+'" was renamed', 'success')) + .catch((err) => notify.send(err, 'error')); } - onDelete(file, type){ - return Files.rm(file, type); + onDelete(path, type){ + return Files.rm(path, type) + .then(() => notify.send('The file "'+Path.basename(path)+'" was deleted', 'success')) + .catch((err) => notify.send(err, 'error')); } onUpload(path, files){ @@ -205,10 +214,7 @@ export class FilesPage extends React.Component {
- - - - + diff --git a/client/pages/filespage/thing-existing.js b/client/pages/filespage/thing-existing.js index 8e4b8a9a..ede9ac8e 100644 --- a/client/pages/filespage/thing-existing.js +++ b/client/pages/filespage/thing-existing.js @@ -85,20 +85,14 @@ export class ExistingThing extends React.Component { }; } - onSelect(){ - if(this.state.icon !== 'loading' && this.state.icon !== 'error'){ - } - } - onRename(newFilename){ - if(this.state.icon !== 'loading' && this.state.icon !== 'error'){ - this.props.emit( - 'file.rename', - pathBuilder(this.props.path, this.props.file.name), - pathBuilder(this.props.path, newFilename), - this.props.file.type - ); - } + this.props.emit( + 'file.rename', + pathBuilder(this.props.path, this.props.file.name), + pathBuilder(this.props.path, newFilename), + this.props.file.type + ); + this.setState({is_renaming: false}); } onRenameRequest(){ diff --git a/client/pages/viewerpage.js b/client/pages/viewerpage.js index b36ad4c5..e2ad664a 100644 --- a/client/pages/viewerpage.js +++ b/client/pages/viewerpage.js @@ -2,8 +2,8 @@ import React from 'react'; import Path from 'path'; import { Files } from '../model/'; -import { BreadCrumb, Bundle, NgIf, Loader, Error, Container, EventReceiver, EventEmitter } from '../components/'; -import { debounce, opener, screenHeight } from '../helpers/'; +import { BreadCrumb, Bundle, NgIf, Loader, Container, EventReceiver, EventEmitter } from '../components/'; +import { debounce, opener, screenHeight, notify } from '../helpers/'; import { AudioPlayer, FileDownloader, ImageViewer, PDFViewer } from './viewerpage/'; const VideoPlayer = (props) => ( @@ -29,7 +29,6 @@ export class ViewerPage extends React.Component { needSaving: false, isSaving: false, loading: true, - error: false, height: 0 }; this.props.subscribe('file.select', this.onPathUpdate.bind(this)); @@ -37,7 +36,7 @@ export class ViewerPage extends React.Component { } componentWillMount(){ - this.setState({loading: null, error: false}, () => { + this.setState({loading: null}, () => { window.setTimeout(() => { if(this.state.loading === null) this.setState({loading: true}); }, 500); @@ -50,13 +49,12 @@ export class ViewerPage extends React.Component { if(err && err.code === 'CANCELLED'){ return; } if(err.code === 'BINARY_FILE'){ Files.url(this.state.path).then((url) => { - console.log(this.state.path); this.setState({data: url, loading: false, opener: 'download'}); }).catch(err => { - this.setState({error: err}); + notify.send(err, 'error'); }); }else{ - this.setState({error: err}); + notify.send(err, 'error'); } }); }else{ @@ -64,7 +62,7 @@ export class ViewerPage extends React.Component { this.setState({data: url, loading: false, opener: app}); }).catch(err => { if(err && err.code === 'CANCELLED'){ return; } - this.setState({error: err}); + notify.send(err, 'error'); }); } } @@ -87,13 +85,10 @@ export class ViewerPage extends React.Component { this.setState({needSaving: false}); }) .catch((err) => { - if(err && err.code === 'CANCELLED'){ return; } - this.setState({isSaving: false}); - let message = "Oups, something went wrong"; - if(err.message){ - message += ':\n'+err.message; + if(err && err.code === 'CANCELLED'){ + notify.send(err, 'error'); } - alert(message); + this.setState({isSaving: false}); }); } @@ -142,12 +137,7 @@ export class ViewerPage extends React.Component { - - - - - - + diff --git a/client/router.js b/client/router.js index be9e7bd7..69176922 100644 --- a/client/router.js +++ b/client/router.js @@ -21,6 +21,7 @@ export default class AppRouter extends React.Component { + ); } diff --git a/server/ctrl/files.js b/server/ctrl/files.js index 258b4a7c..bd0dd588 100644 --- a/server/ctrl/files.js +++ b/server/ctrl/files.js @@ -80,7 +80,10 @@ app.post('/cat', function(req, res){ app.get('/mv', function(req, res){ let from = decodeURIComponent(req.query.from), to = decodeURIComponent(req.query.to); - if(from && to){ + + if(from === to){ + res.send({status: 'ok'}); + }else if(from && to){ Files.mv(from, to, req.cookies.auth) .then((message) => { res.send({status: 'ok'});