mirror of
				https://github.com/mickael-kerjean/filestash.git
				synced 2025-10-31 10:07:15 +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 | ||||
|         }; | ||||
|  | ||||
|         function TaskManager(){ | ||||
|             let jobs = []; | ||||
|             let is_running = false; | ||||
|  | ||||
|             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(); | ||||
|     } | ||||
|  | ||||
|     componentWillMount(){ | ||||
|         this.componentWillReceiveProps(this.props); | ||||
|     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; | ||||
|             } | ||||
|  | ||||
|     componentWillUnmount(){ | ||||
|         window.clearTimeout(this.timeout); | ||||
|             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); | ||||
|             }; | ||||
|  | ||||
|     componentWillReceiveProps(props){ | ||||
|         if(props.error !== null){ | ||||
|             this.componentWillUnmount(); | ||||
|             this.setState({visible: true, error: props.error}); | ||||
|             this.timeout = window.setTimeout(() => { | ||||
|                 this.setState({visible: null}); | ||||
|             }, 5000); | ||||
|             return displayMessage(message) | ||||
|                 .then(waitForABit.bind(this, 5000)) | ||||
|                 .then(hideMessage) | ||||
|                 .then(waitForABit.bind(this, 1000)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     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); | ||||
|             } | ||||
|         }else if(typeof err === 'string'){ | ||||
|             return err; | ||||
|         }else{ | ||||
|             throw('unrecognized notification'); | ||||
|         } | ||||
|     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> | ||||
|               </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; | ||||
|         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; | ||||
|         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; | ||||
|             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.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,13 +137,8 @@ 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> | ||||
|                 </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
	 Mickael KERJEAN
					Mickael KERJEAN