improvement (UI): fixes + cleanup interface - #11

This commit is contained in:
Mickael KERJEAN 2018-03-16 09:56:25 +11:00
parent 59afd3f0a5
commit 89bb4450f3
51 changed files with 3937 additions and 606 deletions

3
.gitignore vendored
View file

@ -9,4 +9,5 @@ babel_cache/
*~ *~
*.swp *.swp
*.swo *.swo
.tern-port .tern-port
.tern-project.js

View file

@ -9,9 +9,9 @@ before_install:
- echo $DOCKER_PASSWORD | docker login -u=$DOCKER_USERNAME --password-stdin - echo $DOCKER_PASSWORD | docker login -u=$DOCKER_USERNAME --password-stdin
script: script:
- sed -i "s/application_url/$APPLICATION_URL/g" config.js - sed -i "s/application_url/$APPLICATION_URL/g" config_server.js
- sed -i "s/gdrive_client_id/$GOOGLE_CLIENTID/" config.js - sed -i "s/gdrive_client_id/$GOOGLE_CLIENTID/" config_server.js
- sed -i "s/gdrive_client_secret/$GOOGLE_CLIENTSECRET/" config.js - sed -i "s/gdrive_client_secret/$GOOGLE_CLIENTSECRET/" config_server.js
- sed -i "s/dropbox_client_id/$DROPBOX_CLIENTID/" config.js - sed -i "s/dropbox_client_id/$DROPBOX_CLIENTID/" config_server.js
- npm run image - npm run image
- npm run publish - npm run publish

View file

@ -4,16 +4,18 @@
--emphasis: #375160; --emphasis: #375160;
--primary: #9AD1ED; --primary: #9AD1ED;
--emphasis-primary: #2b71bc; --emphasis-primary: #c5e2f1;
--secondary: #466372; --secondary: #466372;
--emphasis-secondary: #466372; --emphasis-secondary: #466372;
--super-light: #ecf1f6; --light: #909090;
--super-light: #f4f4f4;
--error: #f26d6d; --error: #f26d6d;
--success: #63d9b1; --success: #63d9b1;
} }
// --super-light: #ecf1f6;
html { html {
font-family:"San Francisco","Roboto","Arial",sans-serif; font-family:"San Francisco","Roboto","Arial",sans-serif;
@ -68,8 +70,6 @@ input[type="checkbox"]{position: relative; top: 1px; margin: 0; padding: 0;}
border-color: rgb(154, 209, 237)!important; border-color: rgb(154, 209, 237)!important;
} }
.drag-drop{ .drag-drop{
z-index: 2; z-index: 2;
} }
@ -97,26 +97,3 @@ body, body > div, body > div > div, body > div > div > div{ height: 100%;}
.login-form button.active{ .login-form button.active{
box-shadow: 0px 1px 5px rgba(0,0,0,0.20); box-shadow: 0px 1px 5px rgba(0,0,0,0.20);
} }
/* ANIMATION */
.example-enter {
opacity: 0.01;
}
.example-enter.example-enter-active {
opacity: 1;
transition: opacity 500ms ease-in;
}
.example-leave {
opacity: 1;
}
.example-leave.example-leave-active {
opacity: 0.01;
transition: opacity 300ms ease-in;
}

View file

@ -1,6 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> <svg
<g fill-rule="evenodd" transform="matrix(.86667 0 0 .86667 -172.05 -864.43)" fill="#969696"> xmlns:dc="http://purl.org/dc/elements/1.1/"
<path d="m200.2 999.72c-0.28913 0-0.53125 0.2421-0.53125 0.53117v12.784c0 0.2985 0.23264 0.5312 0.53125 0.5312h15.091c0.2986 0 0.53124-0.2327 0.53124-0.5312l0.0004-10.474c0-0.2889-0.24211-0.5338-0.53124-0.5338l-7.5457 0.0005-2.3076-2.3078z" fill-rule="evenodd" fill="#6f6f6f"/> xmlns:cc="http://creativecommons.org/ns#"
</g> xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="16"
width="16"
version="1.0"
id="svg7123"
sodipodi:docname="folder.svg"
inkscape:version="0.92.2 5c3e80d, 2017-08-06">
<metadata
id="metadata7129">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs7127" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1894"
inkscape:window-height="1027"
id="namedview7125"
showgrid="false"
inkscape:zoom="59"
inkscape:cx="7.3533727"
inkscape:cy="9.3706349"
inkscape:window-x="10"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="svg7123" />
<g
transform="matrix(0.86666431,0,0,0.86667,-172.04578,-864.32759)"
id="g7121"
style="fill:#75bbd9;fill-opacity:0.94117647;fill-rule:evenodd">
<path
d="m 200.2,999.72 c -0.28913,0 -0.53125,0.2421 -0.53125,0.5312 v 12.784 c 0,0.2985 0.23264,0.5312 0.53125,0.5312 h 15.091 c 0.2986,0 0.53124,-0.2327 0.53124,-0.5312 l 4e-4,-10.474 c 0,-0.2889 -0.24211,-0.5338 -0.53124,-0.5338 l -7.5457,5e-4 -2.3076,-2.30783 z"
id="path7119"
style="fill:#75bbd9;fill-opacity:0.94117647;fill-rule:evenodd"
inkscape:connector-curvature="0" />
</g>
<g
transform="matrix(0.86667,0,0,0.86667,-172.04692,-864.7834)"
id="g7121-3"
style="fill:#9ad1ed;fill-opacity:1;fill-rule:evenodd">
<path
d="m 200.2,999.72 c -0.28913,0 -0.53125,0.2421 -0.53125,0.5312 v 12.784 c 0,0.2985 0.23264,0.5312 0.53125,0.5312 h 15.091 c 0.2986,0 0.53124,-0.2327 0.53124,-0.5312 l 4e-4,-10.474 c 0,-0.2889 -0.24211,-0.5338 -0.53124,-0.5338 l -7.5457,5e-4 -2.3076,-2.30783 z"
id="path7119-5"
style="fill:#9ad1ed;fill-opacity:1;fill-rule:evenodd"
inkscape:connector-curvature="0" />
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 662 B

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -1,6 +1,69 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?> <?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> <svg
<g fill-rule="evenodd" transform="matrix(.86667 0 0 .86667 -172.05 -864.43)" fill="#969696"> xmlns:dc="http://purl.org/dc/elements/1.1/"
<path d="m200.2 999.72c-0.28913 0-0.53125 0.2421-0.53125 0.53117v12.784c0 0.2985 0.23264 0.5312 0.53125 0.5312h15.091c0.2986 0 0.53124-0.2327 0.53124-0.5312l0.0004-10.474c0-0.2889-0.24211-0.5338-0.53124-0.5338l-7.5457 0.0005-2.3076-2.3078z" fill-rule="evenodd" fill="#6f6f6f"/> xmlns:cc="http://creativecommons.org/ns#"
</g> xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="16"
width="16"
version="1.0"
id="svg7123"
sodipodi:docname="link.svg"
inkscape:version="0.92.2 5c3e80d, 2017-08-06">
<metadata
id="metadata7129">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs7127" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1894"
inkscape:window-height="1027"
id="namedview7125"
showgrid="false"
inkscape:zoom="59"
inkscape:cx="4.3533727"
inkscape:cy="5.9808044"
inkscape:window-x="10"
inkscape:window-y="37"
inkscape:window-maximized="0"
inkscape:current-layer="svg7123" />
<g
transform="matrix(0.86666431,0,0,0.86667,-172.04578,-864.32759)"
id="g7121"
style="fill:#75bbd9;fill-opacity:0.94117647;fill-rule:evenodd">
<path
d="m 200.2,999.72 c -0.28913,0 -0.53125,0.2421 -0.53125,0.5312 v 12.784 c 0,0.2985 0.23264,0.5312 0.53125,0.5312 h 15.091 c 0.2986,0 0.53124,-0.2327 0.53124,-0.5312 l 4e-4,-10.474 c 0,-0.2889 -0.24211,-0.5338 -0.53124,-0.5338 l -7.5457,5e-4 -2.3076,-2.30783 z"
id="path7119"
style="fill:#75bbd9;fill-opacity:0.94117647;fill-rule:evenodd"
inkscape:connector-curvature="0" />
</g>
<g
transform="matrix(0.86667,0,0,0.86667,-172.04692,-864.7834)"
id="g7121-3"
style="fill:#9ad1ed;fill-opacity:1;fill-rule:evenodd">
<path
d="m 200.2,999.72 c -0.28913,0 -0.53125,0.2421 -0.53125,0.5312 v 12.784 c 0,0.2985 0.23264,0.5312 0.53125,0.5312 h 15.091 c 0.2986,0 0.53124,-0.2327 0.53124,-0.5312 l 4e-4,-10.474 c 0,-0.2889 -0.24211,-0.5338 -0.53124,-0.5338 l -7.5457,5e-4 -2.3076,-2.30783 z"
id="path7119-5"
style="fill:#9ad1ed;fill-opacity:1;fill-rule:evenodd"
inkscape:connector-curvature="0" />
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 662 B

After

Width:  |  Height:  |  Size: 2.5 KiB

View file

@ -66,41 +66,28 @@ BreadCrumb.propTypes = {
const BreadCrumbContainer = (props) => { const BreadCrumbContainer = (props) => {
let style1 = {background: 'white', margin: '0 0 0px 0', padding: '6px 0', boxShadow: '0 4px 5px 0 rgba(0,0,0,0.14), 0 1px 10px 0 rgba(0,0,0,0.12), 0 2px 4px -1px rgba(0,0,0,0.2)', zIndex: '1000', position: 'relative'};
let style2 = {margin: '0 auto', width: '95%', maxWidth: '800px', padding: '0', color: 'rgba(#6f6f6f, 0.8)'};
return ( return (
<div className={props.className} style={style1}> <div className={props.className}>
<ul style={style2}> <ul>
{props.children} {props.children}
</ul> </ul>
</div> </div>
); );
} }
const Logout = (props) => { const Logout = (props) => {
let style = {
float: 'right',
display: 'inline-block',
padding: '5px 0px 5px 5px',
margin: '0 0px'
};
return ( return (
<li style={style}> <li className="component_logout">
<Link to="/logout"> <Link to="/logout">
<Icon style={{height: '20px'}} name="power"/> <Icon name="power"/>
</Link> </Link>
</li> </li>
); );
} }
const Saving = (props) => { const Saving = (props) => {
let style = {
display: 'inline',
padding: '0 3px'
};
if(props.needSaving){ if(props.needSaving){
return ( return (
<NgIf style={style} cond={props.needSaving === true && props.isLast === true}> <NgIf className="component_saving" cond={props.needSaving === true && props.isLast === true}>
* *
</NgIf> </NgIf>
); );
@ -111,7 +98,7 @@ const Saving = (props) => {
const Separator = (props) => { const Separator = (props) => {
return ( return (
<NgIf cond={props.isLast === false} style={{position: 'relative', top: '3px', display: 'inline'}}> <NgIf cond={props.isLast === false} className="component_separator">
<img width="16" height="16" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAYAAAA7bUf6AAAA30lEQVQ4T63T7Q2CMBAG4OuVPdQNcAPdBCYwDdclCAQ3ACfRDXQDZQMHgNRcAoYApfWjv0jIPX3b3gn4wxJjI03TUAhRBkGwV0o9ffaYIEVRrJumuQHA3ReaILxzl+bCkNZ660ozi/QQIl4BoCKieAmyIlyU53lkjCld0CIyhIwxSmt9nEvkRLgoyzIuPggh4iRJqjHkhXTQAwBWUsqNUoq/38sL+TlJf7lf38ngdU5EFNme2adPFgGGrR2LiGcAqIko/LhjeXbatuVOraWUO58hnJ1iRKx8AetxXPHH/1+y62USursaSgAAAABJRU5ErkJggg=="/> <img width="16" height="16" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAYAAAA7bUf6AAAA30lEQVQ4T63T7Q2CMBAG4OuVPdQNcAPdBCYwDdclCAQ3ACfRDXQDZQMHgNRcAoYApfWjv0jIPX3b3gn4wxJjI03TUAhRBkGwV0o9ffaYIEVRrJumuQHA3ReaILxzl+bCkNZ660ozi/QQIl4BoCKieAmyIlyU53lkjCld0CIyhIwxSmt9nEvkRLgoyzIuPggh4iRJqjHkhXTQAwBWUsqNUoq/38sL+TlJf7lf38ngdU5EFNme2adPFgGGrR2LiGcAqIko/LhjeXbatuVOraWUO58hnJ1iRKx8AetxXPHH/1+y62USursaSgAAAABJRU5ErkJggg=="/>
</NgIf> </NgIf>
); );
@ -127,41 +114,29 @@ export class PathElementWrapper extends React.Component {
onClick(){ onClick(){
if(this.props.isLast === false){ if(this.props.isLast === false){
this.props.emit('file.select', this.props.path.full, 'directory') this.props.emit('file.select', this.props.path.full, 'directory');
} }
} }
toggleHover(shouldHover){ toggleHover(shouldHover){
if(('ontouchstart' in window) === false){ if(('ontouchstart' in window) === false){
this.setState({hover: shouldHover}) this.setState({hover: shouldHover});
} }
} }
limitSize(str){ limitSize(str){
if(str.length > 30){ if(str.length > 30){
return str.substring(0,23)+'...' return str.substring(0,23)+'...';
} }
return str; return str;
} }
render(){ render(){
let style = { let className = "component_path-element-wrapper";
cursor: this.props.isLast ? '' : 'pointer', if(this.state.hover){ className += " hover"; }
background: this.state.hover && this.props.isLast !== true? '#f5f5f5' : 'inherit', if(this.props.highlight) { className += " highlight";}
borderRadius: '1px',
fontSize: '18px',
display: 'inline-block',
padding: '4px 5px',
fontWeight: this.props.isLast ? '100': ''
};
if(this.props.highlight === true){
style.background = '#c5e2f1';
style.border = '2px solid #9AD1ED';
style.borderRadius = '2px';
style.padding = '2px 20px';
}
return ( return (
<li onClick={this.onClick.bind(this)} style={style} onMouseEnter={this.toggleHover.bind(this, true)} onMouseLeave={this.toggleHover.bind(this, false)}> <li className={className} onClick={this.onClick.bind(this)} onMouseEnter={this.toggleHover.bind(this, true)} onMouseLeave={this.toggleHover.bind(this, false)}>
{this.limitSize(this.props.path.label)} {this.limitSize(this.props.path.label)}
<Saving isLast={this.props.isLast} needSaving={this.props.needSaving} isSaving={false} /> <Saving isLast={this.props.isLast} needSaving={this.props.needSaving} isSaving={false} />
</li> </li>
@ -177,8 +152,12 @@ export class PathElement extends PathElementWrapper {
} }
render(highlight = false){ render(highlight = false){
let className = "component_path-element";
if(this.props.isLast){
className += " is-last";
}
return ( return (
<div style={{display: 'inline-block', color: this.props.isLast? '#6f6f6f' : 'inherit'}}> <div className={className}>
<PathElementWrapper highlight={highlight} {...this.props} /> <PathElementWrapper highlight={highlight} {...this.props} />
</div> </div>
); );

View file

@ -1,3 +1,76 @@
.component_breadcrumb{
.breadcrumb{
background: white;
margin: 0 0 0px 0;
padding: 8px 0;
box-shadow: 0 4px 5px 0 rgba(0,0,0,0.14), 0 1px 10px 0 rgba(0,0,0,0.12), 0 2px 4px -1px rgba(0,0,0,0.2);
z-index: 1000;
position: relative;
ul{
list-style-type: none;
margin: 0 auto;
width: 95%;
max-width: 800px;
padding: 0;
span, div, li{display: inline-block;}
}
}
.component_logout{
float: right;
display: inline-block;
margin: 0 0px;
padding: 3px 0;
.component_icon{
height: 20px;
}
}
.component_saving{
display: inline;
padding: 0 3px;
}
.component_separator{
position: relative;
top: 3px;
display: inline;
}
.component_path-element{
display: inline-block;
color: var(--light);
cursor: pointer;
&.is-last{
cursor: inherit;
color: var(--color);
font-weight: inherit;
.component_path-element-wrapper.hover{
cursor: inherit;
background: inherit!important;
}
}
.component_path-element-wrapper{
font-size: 18px;
display: inline-block;
padding: 2px 5px;
border-radius: 2px;
&.hover{
background: var(--super-light);
}
&.highlight{
background: var(--emphasis-primary);
border: 2px solid var(--primary);
padding: 0px 20px;
box-sizing: border-box;
}
}
}
}
/* ANIMATION */
.component_breadcrumb{ .component_breadcrumb{
.breadcrumb-leave{ .breadcrumb-leave{
display: inline-block; display: inline-block;

View file

@ -28,8 +28,9 @@ export class Card extends React.Component {
} }
render() { render() {
const _className = this.props.className ? this.props.className+" box" : "box";
return ( return (
<div {...this.props} className={this.props.className+" box"}> <div {...this.props} className={_className}>
{this.props.children} {this.props.children}
</div> </div>
); );

View file

@ -1,17 +1,13 @@
import React from 'react'; import React from 'react';
import { NgIf } from './'; import { NgIf } from './';
import "./error.scss";
export const Error = (props) => { export const Error = (props) => {
let style = props.style || {};
style.textAlign = 'center';
style.marginTop = '50px';
style.fontSize = '25px';
style.fontStyle = 'italic';
style.fontWeight = 100;
return ( return (
<div style={style}> <div className="component_error">
{props.err.message || "Oups something went wrong :/"} {props.err.message || "Oups something went wrong :/"}
<NgIf cond={props.err.trace !== undefined} style={{fontSize: '12px', maxWidth: '500px', margin: '10px auto 0 auto'}}> <NgIf cond={props.err.trace !== undefined} className="trace">
{JSON.stringify(props.err.trace)} {JSON.stringify(props.err.trace)}
</NgIf> </NgIf>
</div> </div>

View file

@ -0,0 +1,13 @@
.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;
}
}

View file

@ -23,6 +23,9 @@ export class Modal extends React.Component {
this.setState({marginTop: this._marginTop()}); this.setState({marginTop: this._marginTop()});
} }
componentWillUnmount(){
}
_marginTop(){ _marginTop(){
let size = 300; let size = 300;
const $box = document.querySelector('#modal-box > div'); const $box = document.querySelector('#modal-box > div');
@ -35,7 +38,6 @@ export class Modal extends React.Component {
} }
render() { render() {
return ( return (
<ReactCSSTransitionGroup transitionName="modal" <ReactCSSTransitionGroup transitionName="modal"
transitionLeaveTimeout={300} transitionLeaveTimeout={300}

View file

@ -62,7 +62,9 @@
// box // box
.modal-appear > div > div, .modal-enter > div > div{ .modal-appear > div > div, .modal-enter > div > div{
opacity: 0; opacity: 0;
transform-origin: top center;
transform: translateY(10px); transform: translateY(10px);
} }
.modal-appear.modal-appear-active > div > div, .modal-enter.modal-enter-active > div > div{ .modal-appear.modal-appear-active > div > div, .modal-enter.modal-enter-active > div > div{
opacity: 1; opacity: 1;

View file

@ -10,8 +10,13 @@ export class NgIf extends React.Component {
let clean_prop = Object.assign({}, this.props); let clean_prop = Object.assign({}, this.props);
delete clean_prop.cond; delete clean_prop.cond;
delete clean_prop.children; delete clean_prop.children;
delete clean_prop.type;
if(this.props.cond){ if(this.props.cond){
return <div {...clean_prop}>{this.props.children}</div>; if(this.props.type === "inline"){
return <span {...clean_prop}>{this.props.children}</span>;
}else{
return <div {...clean_prop}>{this.props.children}</div>;
}
}else{ }else{
return null; return null;
} }

View file

@ -1,74 +1,38 @@
let cache = {}; export function http_get(url, type = 'json'){
return new Promise((done, err) => {
// cleanup expired cache var xhr = new XMLHttpRequest();
setInterval(() => { xhr.withCredentials = true;
for(let key in cache){ xhr.onreadystatechange = function() {
if(cache[key].date < new Date().getTime()){ if (xhr.readyState === XMLHttpRequest.DONE) {
delete cache[key]; if(xhr.status === 200){
} if(type === 'json'){
} try{
}, 120*1000) let data = JSON.parse(xhr.responseText);
if(data.status === 'ok'){
export function invalidate(url){ done(data);
if(url === undefined){ cache = {}; } }else if(data.status === 'redirect'){
else if(typeof url === 'string'){ if(data.to === 'logout'){location.pathname = "/logout";}
if(cache[url]){ }else{
delete cache[url]; err(data);
}
}else if(typeof url.exec === 'function'){ // regexp
for(let key in cache){
if(url.exec(key)){
delete cache[key];
}
}
}else{
throw 'invalidation error';
}
}
export function http_get(url, cache_expire = 0, type = 'json'){
if(cache_expire > 0 && cache[url] && cache[url].date > new Date().getTime()){
return new Promise((done) => done(cache[url].data));
}else{
if(cache[url]){ delete cache[url]; }
return new Promise((done, err) => {
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.onreadystatechange = function() {
if (xhr.readyState === XMLHttpRequest.DONE) {
if(xhr.status === 200){
if(type === 'json'){
try{
let data = JSON.parse(xhr.responseText);
if(data.status === 'ok'){
if(cache_expire > 0){
cache[url] = {data: data.results || data.result, date: new Date().getTime() + cache_expire * 1000};
}
done(data.results || data.result);
}else if(data.status === 'redirect'){
if(data.to === 'logout'){location.pathname = "/logout";}
}else{
err(data);
}
}catch(error){
err({message: 'oups', trace: error});
} }
}else{ }catch(error){
done(xhr.responseText); err({message: 'oups', trace: error});
} }
}else{ }else{
if(navigator.onLine === false){ done(xhr.responseText);
err({status: xhr.status, code: "CONNECTION_LOST", message: 'Connection Lost'}); }
}else{ }else{
err({status: xhr.status, message: xhr.responseText || 'Oups something went wrong'}); if(navigator.onLine === false){
} err({status: xhr.status, code: "CONNECTION_LOST", message: 'Connection Lost'});
}else{
err({status: xhr.status, message: xhr.responseText || 'Oups something went wrong'});
} }
} }
} }
xhr.open('GET', url, true); }
xhr.send(null); xhr.open('GET', url, true);
}); xhr.send(null);
} });
} }
@ -88,7 +52,7 @@ export function http_post(url, data, type = 'json'){
try{ try{
let data = JSON.parse(xhr.responseText); let data = JSON.parse(xhr.responseText);
if(data.status === 'ok'){ if(data.status === 'ok'){
done(data.results || data.result); done(data);
}else if(data.status === 'redirect'){ }else if(data.status === 'redirect'){
if(data.to === 'logout'){location.pathname = "/logout";} if(data.to === 'logout'){location.pathname = "/logout";}
}else{ }else{
@ -120,7 +84,7 @@ export function http_delete(url){
try{ try{
let data = JSON.parse(xhr.responseText); let data = JSON.parse(xhr.responseText);
if(data.status === 'ok'){ if(data.status === 'ok'){
done(data.results || data.result); done(data);
}else if(data.status === 'redirect'){ }else if(data.status === 'redirect'){
if(data.to === 'logout'){location.pathname = "/logout";} if(data.to === 'logout'){location.pathname = "/logout";}
}else{ }else{

184
client/helpers/cache.js Normal file
View file

@ -0,0 +1,184 @@
"use strict";
// window.indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB;
// window.IDBTransaction = window.IDBTransaction || window.webkitIDBTransaction || window.msIDBTransaction || {READ_WRITE: "readwrite"}; // This line should only be needed if it is needed to support the object's constants for older browsers
// window.IDBKeyRange = window.IDBKeyRange || window.webkitIDBKeyRange || window.msIDBKeyRange;
function Data(){
this.FILE_PATH = "file_path";
this.FILE_CONTENT = "file_content";
this.db_version = 'v1.1';
this.db = null;
this.intervalId = window.setInterval(this._vacuum.bind(this), 5000);
this._init();
}
Data.prototype._init = function(){
const request = window.indexedDB.open('nuage', 1);
request.onupgradeneeded = (e) => this._setup(e.target.result);
this.db = new Promise((done, err) => {
request.onsuccess = (e) => {
done(e.target.result);
}
request.onerror = err;
});
}
Data.prototype._setup = function(db){
let store;
if(!db.objectStoreNames.contains(this.FILE_PATH)){
store = db.createObjectStore(this.FILE_PATH, {keyPath: "path"});
}
//store.createIndex("stale", ["last_access"])
if(!db.objectStoreNames.contains(this.FILE_CONTENT)){
store = db.createObjectStore(this.FILE_CONTENT, {keyPath: "path"});
}
//store.createIndex("stale", ["last_access"])
}
Data.prototype._vacuum = function(){
}
/*
* Fetch a record using its path, can be whether a file path or content
*/
Data.prototype.get = function(type, path, _should_update = true){
if(type !== this.FILE_PATH && type !== this.FILE_CONTENT) return Promise.reject({});
return this.db.then((db) => {
const tx = db.transaction(type, "readwrite");
const store = tx.objectStore(type);
const query = store.get(path);
return new Promise((done, error) => {
query.onsuccess = (e) => {
let data = query.result || null;
done(data);
if(data && _should_update === true){
requestAnimationFrame(() => {
data.last_access = new Date();
if(!data.access_count) data.access_count = 0;
data.access_count += 1;
this.put(type, data.path, data);
});
}
};
tx.onerror = error;
});
});
}
Data.prototype.put = function(type, path, data){
if(type !== this.FILE_PATH && type !== this.FILE_CONTENT) return Promise.reject({});
return this.get(type, path, false)
.then((res) => {
let new_data;
if(res === null){
new_data = data;
new_data.last_update = new Date();
new_data.path = path;
}else{
new_data = Object.assign(res, data);
new_data.last_update = new Date();
}
return this.db.then((db) => {
const tx = db.transaction(type, "readwrite");
const store = tx.objectStore(type);
return new Promise((done, error) => {
let request = store.put(new_data);
request.onsuccess = () => done(new_data.result || new_data.results);
request.onerror = (e) => error(e);
tx.onerror = (e) => error(e);
tx.oncomplete = () => done(new_data.result || new_data.results);
});
});
});
}
Data.prototype.remove = function(type, path, exact = true){
return this.db.then((db) => {
const tx = db.transaction(type, "readwrite");
const store = tx.objectStore(type);
if(exact === true){
const req = store.delete(path);
return new Promise((done, err) => {
req.onsuccess = () => done();
req.onerror = err;
});
}else{
const request = store.openCursor();
return new Promise((done, err) => {
request.onsuccess = function(event) {
const cursor = event.target.result;
if(cursor){
if(cursor.value.path.indexOf(path) === 0){
store.delete(cursor.value.path);
}
cursor.continue();
}else{
done();
}
};
});
}
});
}
Data.prototype.update_path = function(updater_fn){
this.db.then((db) => {
const tx = db.transaction(this.FILE_PATH, "readwrite");
const store = tx.objectStore(this.FILE_PATH);
const request = store.openCursor();
request.onsuccess = function(event) {
const cursor = event.target.result;
if(cursor){
updater_fn(cursor.value, store)
cursor.continue();
}
};
});
}
Data.prototype.update_content = function(updater_fn){
this.db.then((db) => {
const tx = db.transaction(this.FILE_CONTENT, "readwrite");
const store = tx.objectStore(this.FILE_CONTENT);
const request = store.openCursor();
request.onsuccess = function(event) {
const cursor = event.target.result;
if(cursor){
const action = updater_fn(cursor.value, store);
cursor.continue();
}
};
});
}
Data.prototype.destroy = function(){
this.db.then((db) => db.close())
clearTimeout(this.intervalId);
window.indexedDB.deleteDatabase('nuage');
this._init();
}
// // test
// cache = new Data();
// cache.put(cache.FILE_PATH, '/', {a:3});
// cache.get(cache.FILE_PATH, '/').then((r) => {
// console.log(r);
// cache.remove(cache.FILE_PATH, '/');
// cache.get(cache.FILE_PATH, '/').then((r) => {
// console.log(r);
// //cache.destroy();
// });
// });
export const cache = new Data();
window.test = cache;

19
client/helpers/events.js Normal file
View file

@ -0,0 +1,19 @@
function Event(){
this.fns = [];
}
Event.prototype.subscribe = function(name, fn){
if(!name || typeof fn !== 'function') return;
this.fns.push({key: name, fn: fn});
}
Event.prototype.unsubscribe = function(name){
this.fns = this.fns.filter((data) => {
return data.key === name ? false : true;
});
}
Event.prototype.emit = function(name, payload){
this.fns.map((data) => {
if(data.key === name) data.fn(payload);
});
}
export const event = new Event();

View file

@ -1,7 +1,9 @@
export { URL_HOME, goToHome, URL_FILES, goToFiles, URL_VIEWER, goToViewer, URL_LOGIN, goToLogin, URL_LOGOUT, goToLogout } from './navigate'; export { URL_HOME, goToHome, URL_FILES, goToFiles, URL_VIEWER, goToViewer, URL_LOGIN, goToLogin, URL_LOGOUT, goToLogout } from './navigate';
export { opener } from './mimetype'; export { opener } from './mimetype';
export { debounce, throttle } from './backpressure'; export { debounce, throttle } from './backpressure';
export { encrypt, decrypt } from './crypto' export { encrypt, decrypt } from './crypto';
export { event } from './events';
export { cache } from './cache';
export { pathBuilder } from './path'; export { pathBuilder } from './path';
export { memory } from './memory'; export { memory } from './memory';
export { prepare } from './navigate'; export { prepare } from './navigate';

View file

@ -13,3 +13,5 @@ if ('serviceWorker' in navigator) {
window.onload = () => { window.onload = () => {
ReactDOM.render(<Router/>, document.getElementById('main')); ReactDOM.render(<Router/>, document.getElementById('main'));
}; };
window.log = function(){console.log.apply(this, arguments)};

View file

@ -1,75 +1,360 @@
import { http_get, http_post, invalidate, prepare } from '../helpers/'; "use strict";
import { http_get, http_post, prepare } from '../helpers/';
import Path from 'path'; import Path from 'path';
import { Observable } from 'rxjs/Observable';
import { cache } from '../helpers/';
class FileSystem{ class FileSystem{
ls(path, cache = 120){ constructor(){
let url = '/api/files/ls?path='+prepare(path); this.obs = null;
invalidate(path) this.current_path = null;
return http_get(url, cache); }
ls(path, internal = false){
this.current_path = path;
this.obs && this.obs.complete();
return Observable.create((obs) => {
this.obs = obs;
this._ls_from_cache(path);
let keep_pulling_from_http = false;
const fetch_from_http = (_path) => {
return this._ls_from_http(_path)
.then(() => new Promise((done, err) => {
window.setTimeout(() => done(), 2000);
}))
.then(() => {
return keep_pulling_from_http === true? fetch_from_http(_path) : Promise.resolve();
});
};
fetch_from_http(path);
return () => {
keep_pulling_from_http = false;
};
});
}
_ls_from_http(path){
const url = '/api/files/ls?path='+prepare(path);
return http_get(url).then((response) => {
return cache.get(cache.FILE_PATH, path, false).then((_files) => {
if(_files && _files.results){
let _files_virtual_to_keep = _files.results.filter((file) => file.icon === 'loading');
// update file results
for(let i=0; i<_files_virtual_to_keep.length; i++){
for(let j=0; j<response.results.length; j++){
if(response.results[j].name === _files_virtual_to_keep[i].name){
response.results[j] = Object.assign({}, _files_virtual_to_keep[i]);
_files_virtual_to_keep[i] = null;
break;
}
}
}
// add stuff that didn't exist in our response
_files_virtual_to_keep = _files_virtual_to_keep.filter((e) => e);
response.results = response.results.concat(_files_virtual_to_keep);
}
// publish
cache.put(cache.FILE_PATH, path, {results: response.results});
if(this.current_path === path) this.obs && this.obs.next(response.results);
});
}).catch((_err) => {
// TODO: user is in offline mode, notify
console.log(_err);
});
}
_ls_from_cache(path){
return cache.get(cache.FILE_PATH, path).then((_files) => {
if(_files && _files.results){
if(this.current_path === path){
this.obs && this.obs.next(_files.results);
}
};
return Promise.resolve();
});
} }
rm(path){ rm(path){
let url = '/api/files/rm?path='+prepare(path); const url = '/api/files/rm?path='+prepare(path);
invalidate_ls(path), false; this._replace(path, 'loading');
invalidate_cat(path, false); return http_get(url)
return http_get(url); .then((res) => {
if(res.status === 'ok'){
this._remove(path);
cache.remove(cache.FILE_CONTENT, path, false);
cache.remove(cache.FILE_PATH, Path.dirname(path) + "/", false);
}else{
this._replace(path, 'error');
}
});
} }
mv(from, to){ cat(path){
let url = '/api/files/mv?from='+prepare(from)+"&to="+prepare(to); const url = '/api/files/cat?path='+prepare(path);
invalidate_ls(from); return http_get(url, 'raw')
invalidate_ls(to); .then((res) => cache.put(cache.FILE_CONTENT, path, {result: res}))
invalidate_cat(from); .catch((res) => {
return http_get(url); return cache.get(cache.FILE_CONTENT, path)
} .then((_res) => {
if(!_res || !_res.result) return Promise.reject(_res);
cat(path, cache = 60){ return Promise.resolve(_res.result);
let url = '/api/files/cat?path='+prepare(path); })
return http_get(url, cache, 'raw') .catch(() => Promise.reject(res));
});
} }
url(path){ url(path){
let url = '/api/files/cat?path='+prepare(path); const url = '/api/files/cat?path='+prepare(path);
return Promise.resolve(url); return Promise.resolve(url);
} }
save(path, file){ save(path, file){
invalidate_ls(path); const url = '/api/files/cat?path='+prepare(path);
invalidate_cat(path); let formData = new window.FormData();
let url = '/api/files/cat?path='+prepare(path);
let formData = new FormData();
formData.append('file', file); formData.append('file', file);
return http_post(url, formData, 'multipart'); this._replace(path, 'loading');
cache.put(cache.FILE_CONTENT, path, file);
return http_post(url, formData, 'multipart')
.then((res)=> {
res.status === 'ok'? this._replace(path) : this._replace(path, 'error');
return Promise.resolve(res);
});
} }
mkdir(path){ mkdir(path){
let url = '/api/files/mkdir?path='+prepare(path); const url = '/api/files/mkdir?path='+prepare(path);
invalidate_ls(path); this._add(path, 'loading');
return http_get(url); cache.remove(cache.FILE_PATH, Path.dirname(path) + "/");
return http_get(url)
.then((res) => {
return res.status === 'ok'? this._replace(path) : this._replace(path, 'error');
});
} }
touch(path, file){ touch(path, file){
invalidate_ls(path); this._add(path, 'loading');
let req;
if(file){ if(file){
let url = '/api/files/cat?path='+prepare(path); const url = '/api/files/cat?path='+prepare(path);
let formData = new FormData(); let formData = new window.FormData();
formData.append('file', file); formData.append('file', file);
return http_post(url, formData, 'multipart'); req = http_post(url, formData, 'multipart');
}else{ }else{
let url = '/api/files/touch?path='+prepare(path); const url = '/api/files/touch?path='+prepare(path);
return http_get(url) req = http_get(url);
} }
return req
.then((res) => {
return res.status === 'ok'? this._replace(path) : this._replace(path, 'error');
});
}
mv(from, to){
const url = '/api/files/mv?from='+prepare(from)+"&to="+prepare(to);
ui_before_request(from, to)
.then(() => this._ls_from_cache(Path.dirname(from)+"/"))
.then(() => http_get(url)
.then((res) => {
if(res.status === 'ok'){
ui_when_success(from, to)
.then(() => this._ls_from_cache(Path.dirname(from)+"/"));
}else{
ui_when_fail(from, to)
.then(() => this._ls_from_cache(Path.dirname(from)+"/"));
}
return Promise.resolve(res);
}));
function ui_before_request(from, to){
return update_from()
.then((file) => {
if(Path.dirname(from) !== Path.dirname(to)){
return update_to(file);
}
return Promise.resolve();
});
function update_from(){
return cache.get(cache.FILE_PATH, Path.dirname(from)+"/", false)
.then((res_from) => {
let _file = {name: Path.basename(from), type: /\/$/.test(from) ? 'directory' : 'file'};
res_from.results = res_from.results.map((file) => {
if(file.name === Path.basename(from)){
file.name = Path.basename(to);
file.icon = 'loading';
_file = file;
}
return file;
});
return cache.put(cache.FILE_PATH, Path.dirname(from)+"/", res_from)
.then(() => Promise.resolve(_file));
});
}
function update_to(file){
return cache.get(cache.FILE_PATH, Path.dirname(to)+"/", false).then((res_to) => {
if(!res_to || !res_to.results) return Promise.resolve();
res_to.results.push(file);
return cache.put(cache.FILE_PATH, Path.dirname(to)+"/", res_to);
});
}
}
function ui_when_fail(from, to){
return update_from()
.then((file) => {
if(Path.dirname(from) !== Path.dirname(to)){
return update_to();
}
return Promise.resolve();
});
function update_from(){
return cache.get(cache.FILE_PATH, Path.dirname(from)+"/", false)
.then((res_from) => {
if(!res_from || !res_from.results) return Promise.reject();
res_from.results = res_from.results.map((file) => {
if(file.name === Path.basename(from)){
file.icon = 'error';
}
return file;
});
return cache.put(cache.FILE_PATH, Path.dirname(from)+"/", res_from)
.then(() => Promise.resolve());
});
}
function update_to(){
return cache.get(cache.FILE_PATH, Path.dirname(to)+"/", false)
.then((res_to) => {
if(!res_to || !res_to.results) return Promise.resolve();
res_to.results = res_to.results.filter((file) => {
if(file.name === Path.basename(to)){
return false;
}
return true;
});
return cache.put(cache.FILE_PATH, Path.dirname(from)+"/", res_to);
});
}
}
function ui_when_success(from, to){
if(Path.dirname(from) === Path.dirname(to)){
this._replace(Path.dirname(from)+"/"+Path.basename(to), null);
return Promise.resolve();
}else{
return update_from()
.then(update_to)
.then(update_related);
}
function update_from(){
return cache.get(cache.FILE_PATH, Path.dirname(from)+"/", false).then((res_from) => {
if(!res_from || !res_from.results) return Promise.resolve();
res_from.results = res_from.results.filter((file) => {
if(file.name === Path.basename(to)){
return false;
}
return true;
});
return cache.put(cache.FILE_PATH, Path.dirname(from)+"/", res_from);
});
}
function update_to(){
return cache.get(cache.FILE_PATH, Path.dirname(to)+"/", false).then((res_to) => {
const target_already_exist = res_to && res_to.results ? true : false;
if(target_already_exist){
res_to.results = res_to.results.map((file) => {
if(file.name === Path.basename(to)){
delete file.icon;
}
return file;
});
return cache.put(cache.FILE_PATH, Path.dirname(to)+"/", res_to);
}else{
const data = {results: [{
name: Path.basename(to),
type: /\/$/.test(to) ? 'directory' : 'file',
time: (new Date()).getTime()
}]};
return cache.put(cache.FILE_PATH, Path.dirname(to)+"/", data);
}
});
}
function update_related(){
// manage nested directories when we try to rename a directory
if(/\/$/.test(from) === true){
return cache.update_path((data) => {
if(data.path !== Path.dirname(to) + "/" && data.path !== Path.dirname(from) + "/" && data.path.indexOf(Path.dirname(from) + "/") === 0){
const old_path = data.path;
data.path = data.path.replace(Path.dirname(from) + "/", Path.dirname(to) + "/");
return cache.remove(cache.FILE_PATH, old_path)
.then(() => cache.put(cache.FILE_PATH, data.path, data));
}
return Promise.resolve();
});
}
}
}
}
_replace(path, icon){
return cache.get(cache.FILE_PATH, Path.dirname(path) + "/", false)
.then((res) => {
if(!res) return Promise.resolve();
let files = res.results.map((file) => {
if(file.name === Path.basename(path)){
if(!icon) delete file.icon;
if(icon) file.icon = icon;
}
return file;
});
res.results = files;
return cache.put(cache.FILE_PATH, Path.dirname(path) + "/", res)
.then((res) => {
this._ls_from_cache(Path.dirname(path)+"/");
return Promise.resolve(res);
});
});
}
_add(path, icon){
return cache.get(cache.FILE_PATH, Path.dirname(path) + "/", false)
.then((res) => {
if(!res) return Promise.resolve();
let file = {
name: Path.basename(path),
type: /\/$/.test(path) ? 'directory' : 'file',
};
if(icon) file.icon = icon;
res.results.push(file);
return cache.put(cache.FILE_PATH, Path.dirname(path) + "/", res)
.then((res) => {
this._ls_from_cache(Path.dirname(path)+"/");
return Promise.resolve(res);
});
});
}
_remove(path){
return cache.get(cache.FILE_PATH, Path.dirname(path) + "/", false)
.then((res) => {
if(!res) return Promise.resolve();
let files = res.results.filter((file) => {
return file.name === Path.basename(path) ? false : true;
});
res.results = files;
return cache.put(cache.FILE_PATH, Path.dirname(path) + "/", res)
.then((res) => {
this._ls_from_cache(Path.dirname(path)+"/");
return Promise.resolve(res);
});
});
} }
} }
function invalidate_ls(path, exact = true){
let url = '/api/files/ls?path='.replace(/([^a-zA-Z0-9])/g, '\\$1');
let reg = new RegExp(url + prepare(Path.dirname(path)+'.*'));
return invalidate(reg);
}
function invalidate_cat(path, exact = true){
let url = '/api/files/cat?path='.replace(/([^a-zA-Z0-9])/g, '\\$1');
let reg = new RegExp(url + prepare(path)+ (exact? '' : '.*'));
return invalidate(reg);
}
export const Files = new FileSystem(); export const Files = new FileSystem();
window.Files = Files;

View file

@ -5,8 +5,8 @@ import './connectpage.scss';
import { Session } from '../model/'; import { Session } from '../model/';
import { Container, NgIf, Loader, Notification } from '../components/'; import { Container, NgIf, Loader, Notification } from '../components/';
import { ForkMe, RememberMe, Credentials, Form } from './connectpage/'; import { ForkMe, RememberMe, Credentials, Form } from './connectpage/';
import { invalidate } from '../helpers/'; import { cache } from '../helpers/';
import config from '../../config.js'; import config from '../../config_client';
import { Alert, Prompt } from '../components/'; import { Alert, Prompt } from '../components/';
@ -48,7 +48,7 @@ export class ConnectPage extends React.Component {
this.setState({loading: true}); this.setState({loading: true});
Session.authenticate(params) Session.authenticate(params)
.then((ok) => { .then((ok) => {
invalidate(); cache.destroy();
const path = params.path && /^\//.test(params.path)? /\/$/.test(params.path) ? params.path : params.path+'/' : '/'; const path = params.path && /^\//.test(params.path)? /\/$/.test(params.path) ? params.path : params.path+'/' : '/';
this.props.history.push('/files'+path); this.props.history.push('/files'+path);
}) })

View file

@ -1,3 +1,4 @@
.component_page_connect{ .component_page_connect{
background: var(--primary); background: var(--primary);
height: 100%;
} }

View file

@ -7,7 +7,7 @@ import Path from 'path';
import './filespage.scss'; import './filespage.scss';
import { Files } from '../model/'; import { Files } from '../model/';
import { NgIf, Loader, Error, Uploader, EventReceiver } from '../components/'; import { NgIf, Loader, Error, Uploader, EventReceiver } from '../components/';
import { debounce, goToFiles, goToViewer } from '../helpers/'; import { debounce, goToFiles, goToViewer, event } from '../helpers/';
import { BreadCrumb, FileSystem } from './filespage/'; import { BreadCrumb, FileSystem } from './filespage/';
@EventReceiver @EventReceiver
@ -25,13 +25,12 @@ export class FilesPage extends React.Component {
this.resetHeight = debounce(this.resetHeight.bind(this), 100); this.resetHeight = debounce(this.resetHeight.bind(this), 100);
this.goToFiles = goToFiles.bind(null, this.props.history); this.goToFiles = goToFiles.bind(null, this.props.history);
this.goToViewer = goToViewer.bind(null, this.props.history); this.goToViewer = goToViewer.bind(null, this.props.history);
} this.observers = {ls: null};
componentWillMount(){
this.onPathUpdate(this.state.path, 'directory', true);
} }
componentDidMount(){ componentDidMount(){
this.onPathUpdate(this.state.path, 'directory');
// subscriptions // subscriptions
this.props.subscribe('file.select', this.onPathUpdate.bind(this)); this.props.subscribe('file.select', this.onPathUpdate.bind(this));
this.props.subscribe('file.upload', this.onUpload.bind(this)); this.props.subscribe('file.upload', this.onUpload.bind(this));
@ -53,6 +52,7 @@ export class FilesPage extends React.Component {
this.props.unsubscribe('file.delete'); this.props.unsubscribe('file.delete');
this.props.unsubscribe('file.refresh'); this.props.unsubscribe('file.refresh');
window.removeEventListener("resize", this.resetHeight); window.removeEventListener("resize", this.resetHeight);
if(this.observers.ls) this.observers.ls.unsubscribe();
} }
hideError(){ hideError(){
@ -60,24 +60,26 @@ export class FilesPage extends React.Component {
} }
onRefresh(path = this.state.path){ onRefresh(path = this.state.path){
this.setState({error: false}); if(this.observers.ls) this.observers.ls.unsubscribe();
return Files.ls(path).then((files) => { this.observers.ls = Files.ls(path).subscribe((files) => {
this.setState({files: files, loading: false}); this.setState({files: files, loading: false})
}).catch((error) => { }, (error) => {
console.log("ERROR", error);
this.setState({error: error}); this.setState({error: error});
}); });
this.setState({error: false});
} }
onPathUpdate(path, type = 'directory', withLoader = true){ onPathUpdate(path, type = 'directory'){
window.path = this.props.history; window.timestamp = new Date();
if(type === 'file'){ if(type === 'file'){
this.props.history.push('/view'+path); this.props.history.push('/view'+path);
}else{ }else{
this.setState({path: path, loading: withLoader}); this.setState({path: path, loading: true});
this.onRefresh(path)
if(path !== this.state.path){ if(path !== this.state.path){
this.props.history.push('/files'+path); this.props.history.push('/files'+path);
} }
return this.onRefresh(path);
} }
} }

View file

@ -1,3 +0,0 @@
.component_existingthing{
}

View file

@ -6,8 +6,8 @@ import Path from 'path';
import "./filesystem.scss"; import "./filesystem.scss";
import { Container, NgIf } from '../../components/'; import { Container, NgIf } from '../../components/';
import { NewThing } from './newthing'; import { NewThing } from './thing-new';
import { ExistingThing } from './existingthing'; import { ExistingThing } from './thing-existing';
import { FileZone } from './filezone'; import { FileZone } from './filezone';
@DropTarget('__NATIVE_FILE__', {}, (connect, monitor) => ({ @DropTarget('__NATIVE_FILE__', {}, (connect, monitor) => ({
@ -44,40 +44,43 @@ export class FileSystem extends React.Component {
} }
function sortByType(files){ function sortByType(files){
return files.sort((fileA, fileB) => { return files.sort((fileA, fileB) => {
let idA = ['deleting', 'moving'].indexOf(fileA.state), if(fileA.icon === 'loading' && fileB.icon !== 'loading'){return +1;}
idB = ['deleting', 'moving'].indexOf(fileB.state); else if(fileA.icon !== 'loading' && fileB.icon === 'loading'){return -1;}
if(idA !== -1 && idB !== -1){ return 0; }
else if(idA !== -1 && idB === -1){ return +1; }
else if(idA === -1 && idB !== -1){ return -1; }
else{ else{
if(['directory', 'link'].indexOf(fileA.type) !== -1 && ['directory', 'link'].indexOf(fileB.type) !== -1 ){ return 0; } if(['directory', 'link'].indexOf(fileA.type) === -1 && ['directory', 'link'].indexOf(fileB.type) !== -1){
else if(['directory', 'link'].indexOf(fileA.type) !== -1 && ['directory', 'link'].indexOf(fileB.type) === -1){ return -1; } return +1;
else if(['directory', 'link'].indexOf(fileA.type) === -1 && ['directory', 'link'].indexOf(fileB.type) !== -1){ return +1; } }else if(['directory', 'link'].indexOf(fileA.type) !== -1 && ['directory', 'link'].indexOf(fileB.type) === -1){
else{ return fileA.name.toLowerCase() > fileB.name.toLowerCase(); } return -1;
}else{
if(fileA.name[0] === "." && fileB.name[0] !== ".") return +1;
else if(fileA.name[0] !== "." && fileB.name[0] === ".") return -1;
else{
return fileA.name.toLowerCase() > fileB.name.toLowerCase() ? +1 : -1;
}
}
} }
}); });
} }
function sortByName(files){ function sortByName(files){
return files.sort((fileA, fileB) => { return files.sort((fileA, fileB) => {
let idA = ['deleting', 'moving'].indexOf(fileA.state), if(fileA.icon === 'loading' && fileB.icon !== 'loading'){return +1;}
idB = ['deleting', 'moving'].indexOf(fileB.state); else if(fileA.icon !== 'loading' && fileB.icon === 'loading'){return -1;}
else{
if(idA !== -1 && idB !== -1){ return 0; } if(fileA.name[0] === "." && fileB.name[0] !== ".") return +1;
else if(idA !== -1 && idB === -1){ return +1; } else if(fileA.name[0] !== "." && fileB.name[0] === ".") return -1;
else if(idA === -1 && idB !== -1){ return -1; } else{
else{ return fileA.name.toLowerCase() > fileB.name.toLowerCase(); } return fileA.name.toLowerCase() > fileB.name.toLowerCase() ? +1 : -1;
}
}
}); });
} }
function sortByDate(files){ function sortByDate(files){
return files.sort((fileA, fileB) => { return files.sort((fileA, fileB) => {
let idA = ['deleting', 'moving'].indexOf(fileA.state), if(fileA.icon === 'loading' && fileB.icon !== 'loading'){return +1;}
idB = ['deleting', 'moving'].indexOf(fileB.state); else if(fileA.icon !== 'loading' && fileB.icon === 'loading'){return -1;}
else{
if(idA !== -1 && idB !== -1){ return 0; } return fileB.time - fileA.time;
else if(idA !== -1 && idB === -1){ return +1; } }
else if(idA === -1 && idB !== -1){ return -1; }
else{ return fileB.time - fileA.time; }
}); });
} }
} }

View file

@ -14,7 +14,7 @@
height: 100%; height: 100%;
.list{ .list{
clear: both; clear: both;
padding-bottom: 150px; padding-bottom: 30px;
} }
.error{ .error{

View file

@ -3,11 +3,12 @@ import PropTypes from 'prop-types';
import { DropTarget } from 'react-dnd'; import { DropTarget } from 'react-dnd';
import { EventEmitter } from '../../components/'; import { EventEmitter } from '../../components/';
import './filezone.scss';
@EventEmitter @EventEmitter
@DropTarget('__NATIVE_FILE__', { @DropTarget('__NATIVE_FILE__', {
drop(props, monitor){ drop(props, monitor){
let files = monitor.getItem().files let files = monitor.getItem().files;
props.emit('file.upload', props.path, files); props.emit('file.upload', props.path, files);
} }
}, (connect, monitor) => ({ }, (connect, monitor) => ({
@ -16,29 +17,18 @@ import { EventEmitter } from '../../components/';
})) }))
export class FileZone extends React.Component{ export class FileZone extends React.Component{
constructor(props){ constructor(props){
super(props) super(props);
} }
render(){ render(){
let style = {
border: '2px dashed',
padding: '25px 0',
marginBottom: '10px',
textAlign: 'center',
fontWeight: 'bold'
}
if(this.props.fileIsOver){
style.background = '#B4EBFF';
style.border = '2px dashed #9AD1ED';
style.color = 'white'
}
return this.props.connectDropFile( return this.props.connectDropFile(
<div style={style}> <div className={"component_filezone "+(this.props.fileIsOver ? "hover" : "")}>
DROP HERE TO UPLOAD DROP HERE TO UPLOAD
</div> </div>
); );
} }
} }
FileZone.PropTypes = { FileZone.PropTypes = {
path: PropTypes.string.isRequired path: PropTypes.string.isRequired
} }

View file

@ -0,0 +1,11 @@
.component_filezone{
border: 2px dashed;
padding: 25px 0;
margin-bottom: 10px;
text-align: center;
font-weight: bold;
&.hover{
background: var(--emphasis-primary);
border: 2px dashed var(--primary);
}
}

View file

@ -1,84 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Card, NgIf, Icon, EventEmitter } from '../../components/';
import { pathBuilder } from '../../helpers/';
@EventEmitter
export class NewThing extends React.Component {
constructor(props){
super(props);
this.state = {
name: null,
type: null,
message: null,
icon: null
}
}
onNew(type){
this.setState({type: type, name: '', icon: type})
}
onDelete(){
this.setState({type: null, name: null, icon: null})
}
onSave(e){
e.preventDefault();
if(this.state.name !== null){
this.setState({icon: 'loading'})
this.props.emit('file.create', pathBuilder(this.props.path, this.state.name, this.state.type), this.state.type)
.then((ok) => this.props.emit('file.refresh', this.props.path))
.then((ok) => {
this.onDelete();
return Promise.resolve('ok');
})
.catch(err => {
if(err && err.code === 'CANCELLED'){ return }
this.setState({message: err.message, icon: 'error'})
})
}
}
onSortUpdate(e){
this.props.onSortUpdate(e.target.value);
}
render(){
return (
<div>
<div style={{fontSize: '15px', lineHeight: '15px', height: '15px', marginTop: '5px', color: 'rgba(0,0,0,0.4)', margin: '0 0 10px 0'}}>
<NgIf cond={this.props.accessRight.can_create_file === true} onClick={this.onNew.bind(this, 'file')} style={{marginRight: '15px', cursor: 'pointer', display: 'inline'}}>New File</NgIf>
<NgIf cond={this.props.accessRight.can_create_directory === true} onClick={this.onNew.bind(this, 'directory')} style={{cursor: 'pointer', display: 'inline'}}>New Directory</NgIf>
<select value={this.props.sort} onChange={this.onSortUpdate.bind(this)} style={{float: 'right', color: 'rgba(0,0,0,0.4)', background: 'none', borderRadius: '5px', outline: 'none', border: '1px solid rgba(0,0,0,0.4)', fontSize: '12px'}}>
<option value="type">Sort By Type</option>
<option value="date">Sort By Date</option>
<option value="name">Sort By Name</option>
</select>
</div>
<NgIf cond={this.state.type !== null}>
<Card>
<Icon style={{width: '25px', height: '25px'}} name={this.state.icon} />
<form onSubmit={this.onSave.bind(this)} style={{display: 'inline'}}>
<input onChange={(e) => this.setState({name: e.target.value})} value={this.state.name} style={{outline: 'none'}} type="text" autoFocus/>
</form>
<NgIf cond={this.state.message !== null} style={{color: 'rgba(0,0,0,0.4)', fontSize: '0.9em', paddingLeft: '10px', display: 'inline'}}>
{this.state.message}
</NgIf>
<div style={{float: 'right', height: '22px'}}>
<Icon style={{width: '25px', height: '25px'}} name="delete" onClick={this.onDelete.bind(this)} />
</div>
</Card>
</NgIf>
</div>
)
};
}
NewThing.PropTypes = {
accessRight: PropTypes.obj,
onCreate: PropTypes.func.isRequired,
onSortUpdate: PropTypes.func.isRequired,
sort: PropTypes.string.isRequired,
}

View file

@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { DragSource, DropTarget } from 'react-dnd'; import { DragSource, DropTarget } from 'react-dnd';
import './existingthing.scss'; import './thing.scss';
import { Card, NgIf, Icon, EventEmitter, Prompt } from '../../components/'; import { Card, NgIf, Icon, EventEmitter, Prompt } from '../../components/';
import { pathBuilder } from '../../helpers/'; import { pathBuilder } from '../../helpers/';
@ -15,24 +15,13 @@ const fileSource = {
}; };
}, },
canDrag(props, monitor){ canDrag(props, monitor){
// would have been great to use forbid dragging while there's some actions happenning return props.file.icon === 'loading'? false : true;
// but react-dnd won't give us the component in argument :(
return true;
}, },
endDrag(props, monitor, component){ endDrag(props, monitor, component){
if(monitor.didDrop() && component.state.icon !== 'loading'){ if(monitor.didDrop() && component.state.icon !== 'loading'){
let result = monitor.getDropResult(); let result = monitor.getDropResult();
if(result.action === 'rename'){ if(result.action === 'rename'){
component.setState({icon: 'loading', message: null}, function(){ props.emit.apply(component, ['file.rename'].concat(result.args));
props.emit.apply(component, ['file.rename'].concat(result.args))
.then((ok) => {
component.setState({appear: false});
})
.catch(err => {
if(err && err.code === 'CANCELLED'){ return; }
component.setState({icon: 'error', message: err.message});
});
});
}else{ }else{
throw 'unknown action'; throw 'unknown action';
} }
@ -88,10 +77,8 @@ export class ExistingThing extends React.Component {
constructor(props){ constructor(props){
super(props); super(props);
this.state = { this.state = {
appear: true,
hover: null, hover: null,
message: null, message: null,
icon: props.file.type,
filename: props.file.name, filename: props.file.name,
delete_request: false, delete_request: false,
delete_message: "Confirm by tapping \""+this._confirm_delete_text()+"\"", delete_message: "Confirm by tapping \""+this._confirm_delete_text()+"\"",
@ -99,38 +86,25 @@ export class ExistingThing extends React.Component {
}; };
} }
componentWillReceiveProps(props){
this.setState({
filename: props.file.name,
message: props.file.message || null
});
}
onSelect(){ onSelect(){
if(this.state.icon !== 'loading'){ if(this.state.icon !== 'loading' && this.state.icon !== 'error'){
this.props.emit('file.select', pathBuilder(this.props.path, this.props.file.name, this.props.file.type), this.props.file.type) this.props.emit(
.catch((err) => { 'file.select',
if(err && err.code === 'CANCELLED'){ return; } pathBuilder(this.props.path, this.props.file.name, this.props.file.type),
this.setState({icon: 'error', message: err.message}); this.props.file.type
}); );
} }
} }
onRename(newFilename){ onRename(newFilename){
let oldFilename = this.props.file.name; if(this.state.icon !== 'loading' && this.state.icon !== 'error'){
this.setState({icon: 'loading', filename: newFilename}); this.props.emit(
this.props.emit( 'file.rename',
'file.rename', pathBuilder(this.props.path, this.props.file.name),
pathBuilder(this.props.path, oldFilename), pathBuilder(this.props.path, newFilename),
pathBuilder(this.props.path, newFilename), this.props.file.type
this.props.file.type );
) }
.then((ok) => this.props.emit('file.refresh', this.props.path))
.catch((err) => {
if(err && err.code === 'CANCELLED'){ return; }
this.setState({icon: 'error', message: err.message, filename: oldFilename});
});
} }
onDeleteRequest(filename){ onDeleteRequest(filename){
@ -143,12 +117,7 @@ export class ExistingThing extends React.Component {
'file.delete', 'file.delete',
pathBuilder(this.props.path, this.props.file.name), pathBuilder(this.props.path, this.props.file.name),
this.props.file.type this.props.file.type
).then((ok) => { );
this.setState({appear: false});
}).catch((err) => {
if(err && err.code === 'CANCELLED'){ return; }
this.setState({icon: 'error', message: err.message});
});
}else{ }else{
this.setState({delete_error: "Doesn't match"}); this.setState({delete_error: "Doesn't match"});
} }
@ -163,33 +132,32 @@ export class ExistingThing extends React.Component {
render(highlight){ render(highlight){
const { connectDragSource, connectDropFile, connectDropNativeFile } = this.props; const { connectDragSource, connectDropFile, connectDropNativeFile } = this.props;
let dragStyle = {whiteSpace: 'nowrap'}; let className = "";
if(this.props.isDragging) { dragStyle.opacity = 0.15; } if(this.props.isDragging) {
className += "is-dragging ";
}
if(this.state.hover === true){ if(this.state.hover === true){
dragStyle.background = '#f5f5f5'; className += "mouse-is-hover ";
} }
if((this.props.fileIsOver && this.props.canDropFile) || (this.props.nativeFileIsOver && this.props.canDropNativeFile)) { if((this.props.fileIsOver && this.props.canDropFile) || (this.props.nativeFileIsOver && this.props.canDropNativeFile)) {
dragStyle.background = '#c5e2f1'; className += "file-is-hover ";
} }
className = className.trim();
return connectDragSource(connectDropNativeFile(connectDropFile( return connectDragSource(connectDropNativeFile(connectDropFile(
<div className="component_existingthing"> <div className="component_thing">
<NgIf cond={this.state.appear}> <Card className={this.state.hover} onClick={this.onSelect.bind(this)} onMouseEnter={() => this.setState({hover: true})} onMouseLeave={() => this.setState({hover: false})} className={className}>
<Card onClick={this.onSelect.bind(this)} onMouseEnter={() => this.setState({hover: true})} onMouseLeave={() => this.setState({hover: false})} style={dragStyle}> <DateTime show={this.state.hover !== true || this.state.icon === 'loading'} timestamp={this.props.file.time} />
<DateTime show={this.state.hover !== true || this.state.icon === 'loading'} timestamp={this.props.file.time} background={dragStyle.background}/> <Updater filename={this.props.file.name}
<Updater filename={this.state.filename} icon={this.props.file.icon || this.props.file.type}
icon={this.props.file.virtual? this.props.file.icon : this.state.icon} can_move={this.props.file.can_move !== false}
can_move={this.props.file.can_move !== false} can_delete={this.props.file.can_delete !== false}
can_delete={this.props.file.can_delete !== false} show={this.state.hover === true && this.state.icon !== 'loading' && !('ontouchstart' in window)}
background={dragStyle.background} onRename={this.onRename.bind(this)}
show={this.state.hover === true && this.state.icon !== 'loading' && !('ontouchstart' in window)} onDelete={this.onDeleteRequest.bind(this)} />
onRename={this.onRename.bind(this)} <FileSize type={this.props.file.type} size={this.props.file.size} />
onDelete={this.onDeleteRequest.bind(this)} /> <Message message={this.state.message} />
<FileSize type={this.props.file.type} size={this.props.file.size} /> </Card>
<Message message={this.state.message} />
</Card>
</NgIf>
<Prompt appear={this.state.delete_request} error={this.state.delete_error} message={this.state.delete_message} onCancel={this.onDeleteCancel.bind(this)} onSubmit={this.onDeleteConfirm.bind(this)}/> <Prompt appear={this.state.delete_request} error={this.state.delete_error} message={this.state.delete_message} onCancel={this.onDeleteCancel.bind(this)} onSubmit={this.onDeleteConfirm.bind(this)}/>
</div> </div>
))); )));
@ -239,31 +207,26 @@ class Updater extends React.Component {
} }
render(){ render(){
const style = {
inline: {display: 'inline'},
el: {float: 'right', color: '#6f6f6f', height: '22px', background: this.props.background || 'white', margin: '0 -10px', padding: '0 10px', position: 'relative'},
margin: {marginRight: '10px'}
};
return ( return (
<div style={{display: 'inline'}}> <span className="component_updater">
<NgIf cond={this.props.show} style={style.el}> <NgIf className="action" cond={this.props.show}>
<NgIf cond={this.props.can_move} style={style.inline}> <NgIf cond={this.props.can_move} type="inline">
<Icon name="edit" onClick={this.onRenameRequest.bind(this)} style={style.margin} style={{width: '25px', height: '25px'}} /> <Icon name="edit" onClick={this.onRenameRequest.bind(this)} className="component_updater--icon" />
</NgIf> </NgIf>
<NgIf cond={this.props.can_delete !== false} style={style.inline}> <NgIf cond={this.props.can_delete !== false} type="inline">
<Icon name="delete" onClick={this.onDelete.bind(this)} style={{width: '25px', height: '25px'}} /> <Icon name="delete" onClick={this.onDelete.bind(this)} className="component_updater--icon"/>
</NgIf> </NgIf>
</NgIf> </NgIf>
<Icon style={{width: '25px', height: '25px'}} name={this.props.icon} /> <Icon className="component_updater--icon" name={this.props.icon} />
<span style={{padding: '5px', lineHeight: '22px'}}> <span className="file-details">
<NgIf style={{display: 'inline'}} cond={this.state.editing === null}>{this.props.filename}</NgIf> <NgIf cond={this.state.editing === null} type='inline'>{this.props.filename}</NgIf>
<NgIf style={{display: 'inline'}} cond={this.state.editing !== null}> <NgIf cond={this.state.editing !== null} type='inline'>
<form onClick={this.preventSelect} onSubmit={this.onRename.bind(this)} style={{display: 'inline'}}> <form onClick={this.preventSelect} onSubmit={this.onRename.bind(this)}>
<input value={this.state.editing} onChange={(e) => this.setState({editing: e.target.value})} autoFocus /> <input value={this.state.editing} onChange={(e) => this.setState({editing: e.target.value})} autoFocus />
</form> </form>
</NgIf> </NgIf>
</span> </span>
</div> </span>
); );
} }
} }
@ -283,10 +246,8 @@ const DateTime = (props) => {
} }
} }
const style = {float: 'right', color: '#6f6f6f', lineHeight: '25px', background: props.background || 'white', margin: '0 -10px', padding: '0 10px', position: 'relative'};
return ( return (
<NgIf cond={props.show} style={style}> <NgIf cond={props.show} className="component_datetime">
<span>{displayTime(props.timestamp)}</span> <span>{displayTime(props.timestamp)}</span>
</NgIf> </NgIf>
); );
@ -294,30 +255,30 @@ const DateTime = (props) => {
const FileSize = (props) => { const FileSize = (props) => {
function displaySize(bytes){ function displaySize(bytes){
if(bytes < 1024){ if(Number.isNaN(bytes) || bytes === undefined){
return bytes+'B'; return "";
}else if(bytes < 1024){
return "("+bytes+'B'+")";
}else if(bytes < 1048576){ }else if(bytes < 1048576){
return Math.round(bytes/1024*10)/10+'KB'; return "("+Math.round(bytes/1024*10)/10+'KB'+")";
}else if(bytes < 1073741824){ }else if(bytes < 1073741824){
return Math.round(bytes/(1024*1024)*10)/10+'MB'; return "("+Math.round(bytes/(1024*1024)*10)/10+'MB'+")";
}else if(bytes < 1099511627776){ }else if(bytes < 1099511627776){
return Math.round(bytes/(1024*1024*1024)*10)/10+'GB'; return "("+Math.round(bytes/(1024*1024*1024)*10)/10+'GB'+")";
}else{ }else{
return Math.round(bytes/(1024*1024*1024*1024))+'TB'; return "("+Math.round(bytes/(1024*1024*1024*1024))+'TB'+")";
} }
} }
const style = {color: '#6f6f6f', fontSize: '0.85em'};
return ( return (
<NgIf cond={props.type === 'file'} style={{display: 'inline-block'}}> <NgIf type="inline" className="component_filesize" cond={props.type === 'file'}>
<span style={style}>({displaySize(props.size)})</span> <span>{displaySize(props.size)}</span>
</NgIf> </NgIf>
); );
} }
const Message = (props) => { const Message = (props) => {
const style = {color: 'rgba(0,0,0,0.4)', fontSize: '0.9em', paddingLeft: '10px', display: 'inline'};
return ( return (
<NgIf cond={props.message !== null} style={style}> <NgIf cond={props.message !== null} className="component_message" type="inline">
- {props.message} - {props.message}
</NgIf> </NgIf>
); );

View file

@ -0,0 +1,85 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Card, NgIf, Icon, EventEmitter } from '../../components/';
import { pathBuilder } from '../../helpers/';
import "./thing.scss";
@EventEmitter
export class NewThing extends React.Component {
constructor(props){
super(props);
this.state = {
name: null,
type: null,
message: null,
icon: null
};
}
onNew(type){
if(this.state.type === type){
this.onDelete();
}else{
this.setState({type: type, name: '', icon: type});
}
}
onDelete(){
this.setState({type: null, name: null, icon: null});
}
onSave(e){
e.preventDefault();
if(this.state.name !== null){
this.props.emit('file.create', pathBuilder(this.props.path, this.state.name, this.state.type), this.state.type);
}
}
onSortUpdate(e){
this.props.onSortUpdate(e.target.value);
}
render(){
return (
<div>
<div className="menubar no-select">
<NgIf cond={this.props.accessRight.can_create_file === true} onClick={this.onNew.bind(this, 'file')} type="inline">New File</NgIf>
<NgIf cond={this.props.accessRight.can_create_directory === true} onClick={this.onNew.bind(this, 'directory')} type="inline">New Directory</NgIf>
<select value={this.props.sort} onChange={this.onSortUpdate.bind(this)}>
<option value="type">Sort By Type</option>
<option value="date">Sort By Date</option>
<option value="name">Sort By Name</option>
</select>
</div>
<NgIf cond={this.state.type !== null} className="component_thing">
<Card className="mouse-is-hover">
<Icon className="component_updater--icon" name={this.state.icon} />
<span className="file-details">
<form onSubmit={this.onSave.bind(this)}>
<input onChange={(e) => this.setState({name: e.target.value})} value={this.state.name} type="text" autoFocus/>
</form>
</span>
<NgIf className="component_message" cond={this.state.message !== null}>
{this.state.message}
</NgIf>
<span className="component_updater">
<div className="action">
<div>
<Icon className="component_updater--icon" name="delete" onClick={this.onDelete.bind(this)} />
</div>
</div>
</span>
</Card>
</NgIf>
</div>
);
};
}
NewThing.PropTypes = {
accessRight: PropTypes.obj,
onCreate: PropTypes.func.isRequired,
onSortUpdate: PropTypes.func.isRequired,
sort: PropTypes.string.isRequired
}

View file

@ -0,0 +1,82 @@
.menubar{
font-size: 15px;
line-height: 15px;
height: 15px;
margin-top: 5px;
color: var(--light);
margin: 0 0 10px 0;
> span{cursor: pointer; margin-right: 15px;}
select{
float: right;
color: var(--light);
background: none;
border-radius: 3px;
outline: none;
border: 1px solid var(--light);
font-size: 12px;
}
}
.component_thing{
.file-is-hover{
background: var(--emphasis-primary);
}
.mouse-is-hover{
background: var(--super-light);
}
.file-is-dragging{
opacity: 0.15;
}
.component_icon{
width: 25px;
height: 25px;
}
form{
display: inline;
input{
outline: none;
}
}
.component_updater{
.action{
float: right;
color: #6f6f6f;
line-height: 25px;
margin: 0 -10px;
padding: 0 10px;
position: relative;
.component_icon{
padding: 1px 0;
box-sizing: border-box;
}
}
}
.component_datetime{
float: right;
color: var(--light);
line-height: 25px;
margin: 0 -10px;
padding: 0 10px;
position: relative;
}
.component_filesize{
span{
color: var(--light);
font-size: 0.85em;
}
}
.component_message{
color: var(--light);
font-size: 0.9em;
padding-left: 10px;
}
.file-details{
padding: 0 5px;
line-height: 22px;
white-space: nowrap;
}
}

View file

@ -2,7 +2,7 @@ import React from 'react';
import { Session } from '../model/'; import { Session } from '../model/';
import { Loader } from '../components/'; import { Loader } from '../components/';
import { invalidate } from '../helpers/'; import { cache } from '../helpers/';
export class LogoutPage extends React.Component { export class LogoutPage extends React.Component {
constructor(props){ constructor(props){
@ -10,9 +10,9 @@ export class LogoutPage extends React.Component {
} }
componentDidMount(){ componentDidMount(){
invalidate();
Session.logout() Session.logout()
.then((res) => { .then((res) => {
cache.destroy();
this.props.history.push('/'); this.props.history.push('/');
}) })
.catch((res) => { .catch((res) => {

View file

@ -118,7 +118,7 @@ export class ViewerPage extends React.Component {
<IDE needSaving={this.needSaving.bind(this)} <IDE needSaving={this.needSaving.bind(this)}
isSaving={this.state.isSaving} isSaving={this.state.isSaving}
onSave={this.save.bind(this)} onSave={this.save.bind(this)}
content={this.state.data} content={this.state.data || ''}
filename={this.state.filename}/> filename={this.state.filename}/>
</NgIf> </NgIf>
<NgIf cond={this.state.opener === 'image'} style={style}> <NgIf cond={this.state.opener === 'image'} style={style}>

View file

@ -55,6 +55,7 @@ export class Editor extends React.Component {
minFoldSize: 1 minFoldSize: 1
} }
}); });
window.mirror = editor;
if(CodeMirror.afterInit){ if(CodeMirror.afterInit){
CodeMirror.afterInit(editor); CodeMirror.afterInit(editor);

View file

@ -61,3 +61,9 @@ div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
color: #6f6f6f; color: #6f6f6f;
text-shadow: none; text-shadow: none;
} }
/* BUGFIX */
// https://github.com/codemirror/CodeMirror/issues/5056
.CodeMirror-cursor {
width: 1px !important;
}

View file

@ -19,18 +19,20 @@ import { Bundle, URL_HOME, URL_FILES, URL_VIEWER, URL_LOGIN, URL_LOGOUT } from
export default class AppRouter extends React.Component { export default class AppRouter extends React.Component {
render() { render() {
return ( return (
<BrowserRouter> <div>
<div> <BrowserRouter>
<Switch> <div>
<Route exact path="/" component={HomePage} /> <Switch>
<Route path="/login" component={ConnectPage} /> <Route exact path="/" component={HomePage} />
<Route path="/files/:path*" component={FilesPage} /> <Route path="/login" component={ConnectPage} />
<Route path="/view/:path*" component={ViewerPage} /> <Route path="/files/:path*" component={FilesPage} />
<Route path="/logout" component={LogoutPage} /> <Route path="/view/:path*" component={ViewerPage} />
<Route component={NotFoundPage} /> <Route path="/logout" component={LogoutPage} />
</Switch> <Route component={NotFoundPage} />
</div> </Switch>
</BrowserRouter> </div>
</BrowserRouter>
</div>
); );
} }
} }

3
config_client.js Normal file
View file

@ -0,0 +1,3 @@
module.exports = {
fork_button: true
}

View file

@ -22,7 +22,5 @@ module.exports = {
clientID: "dropbox_client_id", clientID: "dropbox_client_id",
redirectURI: "application_url/login" redirectURI: "application_url/login"
}, },
server_secret: 'not_so_secret_key', secret_key: 'not_so_secret_key'
// APPLICATION CONFIG
fork_button: true
} }

2687
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -18,14 +18,9 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"aws-sdk": "^2.59.0", "aws-sdk": "^2.59.0",
"babel-polyfill": "^6.23.0",
"body-parser": "^1.17.2", "body-parser": "^1.17.2",
"codemirror": "^5.26.0",
"cookie-parser": "^1.4.3", "cookie-parser": "^1.4.3",
"cors": "^2.8.3",
"crypto": "0.0.3", "crypto": "0.0.3",
"dropbox": "^2.5.3",
"ejs": "^2.5.6",
"express": "^4.15.3", "express": "^4.15.3",
"express-winston": "^2.4.0", "express-winston": "^2.4.0",
"ftp": "^0.3.10", "ftp": "^0.3.10",
@ -35,7 +30,50 @@
"node-ssh": "^4.2.2", "node-ssh": "^4.2.2",
"nodegit": "^0.18.3", "nodegit": "^0.18.3",
"path": "^0.12.7", "path": "^0.12.7",
"pdfjs-dist": "^1.8.426", "request": "^2.81.0",
"request-promise": "^4.2.1",
"scp2": "^0.5.0",
"ssh2-sftp-client": "^1.1.0",
"stream-to-string": "^1.1.0",
"string-to-stream": "^1.1.0",
"webdav-fs": "^1.0.0",
"winston": "^2.3.1",
"winston-couchdb": "^0.6.3"
},
"devDependencies": {
"assert": "^1.4.1",
"babel-cli": "^6.11.4",
"babel-core": "^6.13.2",
"babel-loader": "^6.2.10",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-polyfill": "^6.23.0",
"babel-preset-es2015": "^6.13.2",
"babel-preset-react": "^6.11.1",
"babel-preset-stage-2": "^6.24.1",
"babelify": "^8.0.0",
"browserify": "^16.1.1",
"chai": "^4.1.2",
"codemirror": "^5.26.0",
"cors": "^2.8.3",
"css-loader": "^0.28.10",
"dropbox": "^2.5.3",
"ejs": "^2.5.6",
"html-loader": "^0.4.5",
"html-webpack-plugin": "^2.28.0",
"http-server": "^0.9.0",
"karma": "^2.0.0",
"karma-babel-preprocessor": "^7.0.0",
"karma-browserify": "^5.2.0",
"karma-chai": "^0.1.0",
"karma-chrome-launcher": "^2.2.0",
"karma-firefox-launcher": "^1.1.0",
"karma-mocha": "^1.3.0",
"karma-webpack": "^2.0.13",
"less-loader": "^4.0.6",
"mocha": "^5.0.4",
"node-sass": "^4.7.2",
"nodemon": "^1.17.1",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"react": "^15.3.2", "react": "^15.3.2",
"react-addons-css-transition-group": "^15.6.2", "react-addons-css-transition-group": "^15.6.2",
@ -46,40 +84,17 @@
"react-draggable": "^2.2.6", "react-draggable": "^2.2.6",
"react-router": "^4.1.1", "react-router": "^4.1.1",
"react-router-dom": "^4.1.1", "react-router-dom": "^4.1.1",
"request": "^2.81.0", "requirejs": "^2.3.5",
"request-promise": "^4.2.1", "rx-lite": "^4.0.8",
"rxjs": "^5.4.0", "rxjs": "^5.4.0",
"scp2": "^0.5.0",
"ssh2-sftp-client": "^1.1.0",
"stream-to-string": "^1.1.0",
"string-to-stream": "^1.1.0",
"video.js": "^5.19.2",
"wavesurfer.js": "^1.4.0",
"webdav-fs": "^1.0.0",
"winston": "^2.3.1",
"winston-couchdb": "^0.6.3"
},
"devDependencies": {
"babel-cli": "^6.11.4",
"babel-core": "^6.13.2",
"babel-loader": "^6.2.10",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-plugin-transform-decorators-legacy": "^1.3.4",
"babel-preset-es2015": "^6.13.2",
"babel-preset-react": "^6.11.1",
"babel-preset-stage-2": "^6.24.1",
"css-loader": "^0.28.10",
"html-loader": "^0.4.5",
"html-webpack-plugin": "^2.28.0",
"http-server": "^0.9.0",
"less-loader": "^4.0.6",
"node-sass": "^4.7.2",
"nodemon": "^1.17.1",
"sass-loader": "^6.0.6", "sass-loader": "^6.0.6",
"sass-variable-loader": "^0.1.2", "sass-variable-loader": "^0.1.2",
"style-loader": "^0.20.2", "style-loader": "^0.20.2",
"url-loader": "^0.6.2", "url-loader": "^0.6.2",
"video.js": "^5.19.2",
"videojs-sublime-skin": "^1.0.3", "videojs-sublime-skin": "^1.0.3",
"watchify": "^3.11.0",
"wavesurfer.js": "^1.4.0",
"webpack": "^2.7.0", "webpack": "^2.7.0",
"webpack-bundle-analyzer": "^2.8.2", "webpack-bundle-analyzer": "^2.8.2",
"webpack-dev-server": "^3.1.0" "webpack-dev-server": "^3.1.0"

4
server/bootstrap.js vendored
View file

@ -1,11 +1,11 @@
var bodyParser = require('body-parser'), var bodyParser = require('body-parser'),
cookieParser = require('cookie-parser'), cookieParser = require('cookie-parser'),
cors = require('cors'), cors = require('cors'),
config = require('../config'), config = require('../config_server'),
express = require('express'), express = require('express'),
winston = require('winston'), winston = require('winston'),
expressWinston = require('express-winston'); expressWinston = require('express-winston');
require('winston-couchdb'); require('winston-couchdb');
var app = express(); var app = express();

View file

@ -26,7 +26,7 @@ app.get('/ls', function(req, res){
}) })
.catch(function(err){ .catch(function(err){
res.send({status: 'error', message: err.message || 'cannot fetch files', trace: err}) res.send({status: 'error', message: err.message || 'cannot fetch files', trace: err})
}) });
}else{ }else{
res.send({status: 'error', message: 'unknown path'}) res.send({status: 'error', message: 'unknown path'})
} }
@ -52,7 +52,7 @@ app.get('/cat', function(req, res){
// create/update a file // create/update a file
// https://github.com/pillarjs/multiparty // https://github.com/pillarjs/multiparty
app.post('/cat', function(req, res){ app.post('/cat', function(req, res){
var form = new multiparty.Form(), var form = new multiparty.Form(),
path = decodeURIComponent(req.query.path); path = decodeURIComponent(req.query.path);

View file

@ -3,7 +3,7 @@
var http = require('request-promise'), var http = require('request-promise'),
http_stream = require('request'), http_stream = require('request'),
Path = require('path'), Path = require('path'),
config = require('../../../config'), config = require('../../../config_server'),
toString = require('stream-to-string'), toString = require('stream-to-string'),
Readable = require('stream').Readable; Readable = require('stream').Readable;

View file

@ -2,7 +2,7 @@
// https://developers.google.com/apis-explorer/?hl=en_GB#p/drive/v3/ // https://developers.google.com/apis-explorer/?hl=en_GB#p/drive/v3/
var google = require('googleapis'), var google = require('googleapis'),
googleAuth = require('google-auth-library'), googleAuth = require('google-auth-library'),
config = require('../../../config'), config = require('../../../config_server'),
Stream = require('stream'); Stream = require('stream');
var client = google.drive('v3'); var client = google.drive('v3');

View file

@ -1,4 +1,4 @@
const CACHE_NAME = 'v1.0'; const CACHE_NAME = 'v1.1';
const DELAY_BEFORE_SENDING_CACHE = 2000; const DELAY_BEFORE_SENDING_CACHE = 2000;
/* /*
@ -9,20 +9,10 @@ self.addEventListener('fetch', function(event){
if(is_a_ressource(event.request)){ if(is_a_ressource(event.request)){
return event.respondWith(smartCacheStrategy(event.request)); return event.respondWith(smartCacheStrategy(event.request));
}else if(is_an_api_call(event.request)){ }else if(is_an_api_call(event.request)){
// TODO COOL FEATURE: https://github.com/mickael-kerjean/nuage/issues/11
// basically, it's all about cache invalidation sniffing inside event.request
if(event.request.method === "GET"){
if(navigator.onLine === false){
return event.respondWith(smartCacheStrategy(event.request));
}else{
return event.respondWith(networkFirstStrategy(event.request));
}
}
return event; return event;
}else if(is_an_index(event.request)){ }else if(is_an_index(event.request)){
return event.respondWith(smartCacheStrategy(event.request)) return event.respondWith(smartCacheStrategy(event.request))
}else{ }else{
//console.log("WTF? ", event);
return event; return event;
} }
}); });

View file

@ -1,6 +1,6 @@
const crypto = require('crypto'), const crypto = require('crypto'),
algorithm = 'aes-256-cbc', algorithm = 'aes-256-cbc',
password = require('../../config.js')['server_secret']; password = require('../../config_server')['secret_key'];
module.exports = { module.exports = {
encrypt: function(obj){ encrypt: function(obj){

5
test/client/test.js Normal file
View file

@ -0,0 +1,5 @@
describe('Helpers::event', () => {
it('test', () => {
});
});

20
test/helper_crypto.js Normal file
View file

@ -0,0 +1,20 @@
// import assert from 'assert';
// import { encrypt, decrypt } from '../client/helpers/';
// describe("Helper::crypto", function() {
// it("can encrypt", function() {
// const key = "SUPER_KEY";
// const obj = {a: 3, b:4}
// const encrypted_obj = encrypt(obj, key);
// assert.ok(encrypted_obj);
// assert.notEqual(obj, encrypted_obj);
// });
// it("can decrypt", function() {
// const key = "SUPER_KEY";
// const obj = {a: 3, b:4}
// const encrypted_obj = encrypt(obj,key);
// const decrypted_obj = decrypt(encrypted_obj, key);
// assert.equal(JSON.stringify(obj), JSON.stringify(decrypted_obj));
// });
// });

44
test/helper_event.js Normal file
View file

@ -0,0 +1,44 @@
// import assert from 'assert';
// import { event } from '../client/helpers/';
// describe("Helper::event", function() {
// afterEach(function(){
// event.fns = [];
// });
// it("can register", function() {
// assert.equal(0, event.fns.length);
// event.subscribe("event::test", function(){});
// assert.equal(1, event.fns.length);
// });
// it("can unregister", function() {
// assert.equal(0, event.fns.length);
// event.subscribe("event::test", function(){});
// assert.equal(1, event.fns.length);
// event.unsubscribe("event::test");
// assert.equal(0, event.fns.length);
// });
// it("can emit", function(done){
// event.subscribe('event::test', function(){
// done();
// });
// event.emit("event::test")
// });
// it("can emit multiple times", function(done){
// let count = 0;
// event.subscribe('event::test', function(){
// count += 1;
// if(count === 3){
// done();
// }else{
// assert.equal(true, count < 3);
// }
// });
// event.emit("event::test");
// event.emit("event::test");
// event.emit("event::test");
// });
// });

24
test/index.html Normal file
View file

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html>
<head>
<title>Mocha Tests</title>
<link rel="stylesheet" href="../node_modules/mocha/mocha.css">
</head>
<body>
<div id="mocha"></div>
<script src="../node_modules/mocha/mocha.js"></script>
<script src="../node_modules/chai/chai.js"></script>
<script>mocha.setup('bdd')</script>
<!-- Helpers -->
<!--<script type="module" src="/client/helpers/events.js"></script>-->
<script type="module" src="../client/helpers/crypto.js"></script>
<!-- load your test files here -->
<script>
mocha.run();
</script>
</body>
</html>

29
test/karma-init.js Normal file
View file

@ -0,0 +1,29 @@
var tests = [];
for (var file in window.__karma__.files) {
if (window.__karma__.files.hasOwnProperty(file)) {
if (/Spec\.js$/.test(file)) {
tests.push(file);
}
}
}
requirejs.config({
baseUrl: '/base/src',
paths: {
'jquery': '../lib/jquery',
'underscore': '../lib/underscore',
},
shim: {
'underscore': {
exports: '_'
}
},
// ask Require.js to load these files (all our tests)
deps: tests,
// start test run, once Require.js is done
callback: window.__karma__.start
});