mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-10-30 09:37:55 +08:00
feature (notification): proper notification system
This commit is contained in:
@ -1,15 +0,0 @@
|
||||
import React from 'react';
|
||||
import { NgIf } from './';
|
||||
|
||||
import "./error.scss";
|
||||
|
||||
export const Error = (props) => {
|
||||
return (
|
||||
<div className="component_error">
|
||||
{props.err.message || "Oups something went wrong :/"}
|
||||
<NgIf cond={props.err.trace !== undefined} className="trace">
|
||||
{JSON.stringify(props.err.trace)}
|
||||
</NgIf>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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';
|
||||
|
||||
@ -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 (
|
||||
<NgIf cond={this.state.visible === true}>
|
||||
<div className="component_notification">
|
||||
<div onClick={this.toggleVisibility.bind(this)}>
|
||||
{this.formatError(this.state.error)}
|
||||
<NgIf cond={this.state.appear === true} className="component_notification no-select">
|
||||
<ReactCSSTransitionGroup transitionName="notification" transitionLeave={true} transitionLeaveTimeout={200} transitionEnter={true} transitionEnterTimeout={500}>
|
||||
<div className={"component_notification--container "+(this.state.message_type || 'info')}>
|
||||
<div className="message">
|
||||
{ this.state.message_text }
|
||||
</div>
|
||||
<div className="close" onClick={this.close.bind(this)}>X</div>
|
||||
</div>
|
||||
</div>
|
||||
</ReactCSSTransitionGroup>
|
||||
</NgIf>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Notification.propTypes = {
|
||||
error: PropTypes.any
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
17
client/helpers/notify.js
Normal file
17
client/helpers/notify.js
Normal file
@ -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();
|
||||
@ -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)
|
||||
|
||||
@ -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} />
|
||||
</NgIf>
|
||||
<Notification error={this.state.error && this.state.error.message} />
|
||||
</Container>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -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 {
|
||||
<FileSystem path={this.state.path} files={this.state.files} />
|
||||
<Uploader path={this.state.path} />
|
||||
</NgIf>
|
||||
<NgIf cond={!!this.state.error} className="error" onClick={this.componentDidMount.bind(this)}>
|
||||
<Error err={this.state.error}/>
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.loading && !this.state.error}>
|
||||
<NgIf cond={this.state.loading}>
|
||||
<Loader/>
|
||||
</NgIf>
|
||||
</div>
|
||||
|
||||
@ -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(){
|
||||
|
||||
@ -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 {
|
||||
</NgIf>
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.loading === true}>
|
||||
<NgIf cond={this.state.error === false}>
|
||||
<Loader/>
|
||||
</NgIf>
|
||||
<NgIf cond={this.state.error !== false} onClick={this.componentWillMount.bind(this)} style={{cursor: 'pointer'}}>
|
||||
<Error err={this.state.error}/>
|
||||
</NgIf>
|
||||
<Loader/>
|
||||
</NgIf>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -21,6 +21,7 @@ export default class AppRouter extends React.Component {
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
<ModalPrompt />
|
||||
<Notification />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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'});
|
||||
|
||||
Reference in New Issue
Block a user