diff --git a/.drone.yml b/.drone.yml index 9a9e6e3e..e365939b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -53,12 +53,6 @@ steps: - git clone https://mickael-kerjean:$PASSWORD@github.com/mickael-kerjean/filestash-test test - chmod -R 777 ./test/ -# - name: lint_frontend -# image: node:8-alpine -# depends_on: [ test_prepare, build_frontend ] -# commands: -# - npx eslint ./client/ - - name: test_frontend image: node:8-alpine depends_on: [ test_prepare, build_frontend ] diff --git a/.eslintrc.json b/.eslintrc.json index ac2e608d..ce54e092 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -29,13 +29,15 @@ "react/prop-types": [0], "new-cap": [0], "prefer-spread": [0], + "max-len": [0], + "no-invalid-this": [0], + "react/display-name": [0], + "prefer-rest-params": [0], + "react/no-direct-mutation-state": [0], "no-throw-literal": [1], "react/no-find-dom-node": [1], - "no-invalid-this": [1], "react/no-string-refs": [1], - "react/display-name": [1], - "prefer-rest-params": [1], "react/no-deprecated": [1] } } diff --git a/client/components/breadcrumb.js b/client/components/breadcrumb.js index 6a65bb19..450cb5ff 100644 --- a/client/components/breadcrumb.js +++ b/client/components/breadcrumb.js @@ -1,4 +1,3 @@ -// eslint-disable import React from "react"; import PropTypes from "prop-types"; import { Link } from "react-router-dom"; diff --git a/client/components/dropdown.js b/client/components/dropdown.js index 23c31fe2..870215ac 100644 --- a/client/components/dropdown.js +++ b/client/components/dropdown.js @@ -21,6 +21,8 @@ export class Dropdown extends React.Component { } componentDidMount() { + // TODO: soon to be deprecated, we need to refactor that component + /* eslint-disable-next-line react/no-find-dom-node */ this.$dropdown = ReactDOM.findDOMNode(this).querySelector(".dropdown_button"); // This is not really the "react" way of doing things but we needed to use both a // click on the button and on the body (to exit the dropdown). we had issues diff --git a/client/components/mapshot.js b/client/components/mapshot.js index b5206217..ac3d8424 100644 --- a/client/components/mapshot.js +++ b/client/components/mapshot.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, { createRef } from "react"; import { Icon, Loader } from "./"; import { t } from "../locales/"; import "./mapshot.scss"; @@ -12,11 +12,12 @@ export class MapShot extends React.Component { error: false, }; this.onRefresh = this.onRefresh.bind(this); + this.$wrapper = createRef(); } onRefresh() { requestAnimationFrame(() => { - if (this.refs.$wrapper) { + if (this.$wrapper.current) { this.setState({ tile_size: this.calculateSize(), }); @@ -25,8 +26,8 @@ export class MapShot extends React.Component { } calculateSize() { - if (!this.refs.$wrapper) return 0; - return parseInt(this.refs.$wrapper.clientWidth / 3 * 100) / 100; + if (!this.$wrapper.current) return 0; + return parseInt(this.$wrapper.current.clientWidth / 3 * 100) / 100; } @@ -94,7 +95,7 @@ export class MapShot extends React.Component { const link = "https://www.google.com/maps/search/?api=1&query=" + this.props.lat + "," + this.props.lng; return ( -
{ - cache = new DataFromIndexedDB(); - indexedDB.deleteDatabase("_indexedDB"); - }; -} +export const cache = "indexedDB" in window && window.indexedDB !== null ? + new DataFromIndexedDB() : new DataFromMemory(); diff --git a/client/pages/adminpage/backend.js b/client/pages/adminpage/backend.js index c81b6619..00c8e618 100644 --- a/client/pages/adminpage/backend.js +++ b/client/pages/adminpage/backend.js @@ -1,4 +1,3 @@ -/* eslint-disable max-len */ import React, { useState, useEffect } from "react"; import { FormBuilder, Icon, Input, Alert, Loader } from "../../components/"; import { Backend, Config, Middleware } from "../../model/"; @@ -365,6 +364,7 @@ function AuthenticationMiddleware({ authentication_available, authentication_ena .map((a) => a.trim()); const { identity_provider, attribute_mapping } = formSpec; const selected = backend_enabled.map((b) => { + const type = Object.keys(b)[0]; return { label: b[type].label.value, type: Object.keys(b)[0], diff --git a/client/pages/adminpage/setup.js b/client/pages/adminpage/setup.js index b464e7d9..1070f1d1 100644 --- a/client/pages/adminpage/setup.js +++ b/client/pages/adminpage/setup.js @@ -1,4 +1,3 @@ -/* eslint-disable max-len */ import React, { createRef } from "react"; import ReactCSSTransitionGroup from "react-addons-css-transition-group"; diff --git a/client/pages/connectpage/forkme.js b/client/pages/connectpage/forkme.js index 90a8bb22..12f58ec2 100644 --- a/client/pages/connectpage/forkme.js +++ b/client/pages/connectpage/forkme.js @@ -1,4 +1,3 @@ -/* eslint-disable max-len */ import React from "react"; import "./forkme.scss"; import { t } from "../../locales/"; diff --git a/client/pages/filespage.js b/client/pages/filespage.js index 21416108..12405221 100644 --- a/client/pages/filespage.js +++ b/client/pages/filespage.js @@ -1,37 +1,36 @@ -import React from 'react'; -import { DragDropContext } from 'react-dnd'; -import HTML5Backend from 'react-dnd-html5-backend-filedrop'; -import { SelectableGroup } from 'react-selectable'; +import React, { createRef } from "react"; +import { DragDropContext } from "react-dnd"; +import HTML5Backend from "react-dnd-html5-backend-filedrop"; +import { SelectableGroup } from "react-selectable"; -import './filespage.scss'; -import './error.scss'; -import { Files } from '../model/'; -import { sort, onCreate, onRename, onMultiRename, onDelete, onMultiDelete, onMultiDownload, onUpload, onSearch } from './filespage.helper'; -import { NgIf, NgShow, Loader, EventReceiver, LoggedInOnly, ErrorPage } from '../components/'; -import { notify, debounce, goToFiles, goToViewer, event, settings_get, settings_put } from '../helpers/'; -import { BreadCrumb, FileSystem, FrequentlyAccess, Submenu } from './filespage/'; -import { MobileFileUpload } from './filespage/filezone'; -import InfiniteScroll from 'react-infinite-scroller'; -import { t } from '../locales/'; +import "./filespage.scss"; +import "./error.scss"; +import { Files } from "../model/"; +import { + sort, onCreate, onRename, onMultiRename, onDelete, onMultiDelete, + onMultiDownload, onUpload, onSearch, +} from "./filespage.helper"; +import { NgIf, NgShow, Loader, EventReceiver, LoggedInOnly, ErrorPage } from "../components/"; +import { notify, settings_get, settings_put } from "../helpers/"; +import { BreadCrumb, FileSystem, FrequentlyAccess, Submenu } from "./filespage/"; +import { MobileFileUpload } from "./filespage/filezone"; +import InfiniteScroll from "react-infinite-scroller"; +import { t } from "../locales/"; const PAGE_NUMBER_INIT = 2; const LOAD_PER_SCROLL = 48; // usefull when user press the back button while keeping the current context -let LAST_PAGE_PARAMS = { +const LAST_PAGE_PARAMS = { path: null, scroll: 0, - page_number: PAGE_NUMBER_INIT + page_number: PAGE_NUMBER_INIT, }; -@ErrorPage -@LoggedInOnly -@EventReceiver -@DragDropContext(HTML5Backend) -export class FilesPage extends React.Component { - constructor(props){ +export class FilesPageComponent extends React.Component { + constructor(props) { super(props); - if(props.match.url.slice(-1) != "/"){ + if (props.match.url.slice(-1) != "/") { this.props.history.push(props.match.url + "/"); } this.state = { @@ -46,20 +45,21 @@ export class FilesPage extends React.Component { metadata: null, frequents: null, page_number: PAGE_NUMBER_INIT, - loading: true + loading: true, }; + this.$scroll = createRef(); this.observers = []; this.toggleHiddenFilesVisibilityonCtrlK = this.toggleHiddenFilesVisibilityonCtrlK.bind(this); } - componentDidMount(){ + componentDidMount() { this.onRefresh(this.state.path, "directory"); // subscriptions - this.props.subscribe("file.create", function(){ + this.props.subscribe("file.create", function() { return onCreate.apply(this, arguments).then(() => { - if(this.state.metadata && this.state.metadata.refresh_on_create === true){ + if (this.state.metadata && this.state.metadata.refresh_on_create === true) { this.onRefresh(this.state.path, "directory"); } return Promise.resolve(); @@ -88,31 +88,34 @@ export class FilesPage extends React.Component { this._cleanupListeners(); LAST_PAGE_PARAMS.path = this.state.path; - LAST_PAGE_PARAMS.scroll = this.refs.$scroll.scrollTop; + LAST_PAGE_PARAMS.scroll = this.$scroll.current.scrollTop; LAST_PAGE_PARAMS.page_number = this.state.page_number; } - componentWillReceiveProps(nextProps){ - let new_path = function(path){ - if(path === undefined){ path = "/"; } - if(/\/$/.test(path) === false){ path = path + "/"; } - if(/^\//.test(path) === false){ path = "/"+ path; } + UNSAFE_componentWillReceiveProps(nextProps) { + const new_path = function(path) { + if (path === undefined) path = "/"; + if (/\/$/.test(path) === false) path = path + "/"; + if (/^\//.test(path) === false) path = "/"+ path; return path; - }((nextProps.match.params.path || "").replace(/%23/g, "#").replace(/%3F/g, "?").replace(/%25/g, "%")); - if(new_path !== this.state.path){ - this.setState({path: new_path, loading: true}); + }((nextProps.match.params.path || "") + .replace(/%23/g, "#") + .replace(/%3F/g, "?") + .replace(/%25/g, "%")); + if (new_path !== this.state.path) { + this.setState({ path: new_path, loading: true }); this.onRefresh(new_path); } } - toggleHiddenFilesVisibilityonCtrlK(e){ - if(e.keyCode === 72 && e.ctrlKey === true){ + toggleHiddenFilesVisibilityonCtrlK(e) { + if (e.keyCode === 72 && e.ctrlKey === true) { e.preventDefault(); - this.setState({show_hidden: !this.state.show_hidden}, () => { + this.setState({ show_hidden: !this.state.show_hidden }, () => { settings_put("filespage_show_hidden", this.state.show_hidden); - if(!!this.state.show_hidden){ + if (!!this.state.show_hidden) { notify.send(t("Display hidden files"), "info"); - }else{ + } else { notify.send(t("Hide hidden files"), "info"); } }); @@ -120,10 +123,10 @@ export class FilesPage extends React.Component { } } - onRefresh(path = this.state.path){ + onRefresh(path = this.state.path) { this._cleanupListeners(); const observer = Files.ls(path, this.state.show_hidden).subscribe((res) => { - if(res.status !== "ok"){ + if (res.status !== "ok") { notify.send(res, "error"); return; } @@ -133,26 +136,26 @@ export class FilesPage extends React.Component { selected: [], loading: false, is_search: false, - page_number: function(){ - if(this.state.path === LAST_PAGE_PARAMS.path){ + page_number: function() { + if (this.state.path === LAST_PAGE_PARAMS.path) { return LAST_PAGE_PARAMS.page_number; } return PAGE_NUMBER_INIT; - }.bind(this)() + }.bind(this)(), }, () => { - if(this.state.path === LAST_PAGE_PARAMS.path){ - this.refs.$scroll.scrollTop = LAST_PAGE_PARAMS.scroll; + if (this.state.path === LAST_PAGE_PARAMS.path) { + this.$scroll.current.scrollTop = LAST_PAGE_PARAMS.scroll; } }); }, (error) => this.props.error(error)); this.observers.push(observer); - if(path === "/"){ - Files.frequents().then((s) => this.setState({frequents: s})); + if (path === "/") { + Files.frequents().then((s) => this.setState({ frequents: s })); } } - _cleanupListeners(){ - if(this.observers.length > 0) { + _cleanupListeners() { + if (this.observers.length > 0) { this.observers = this.observers.filter((observer) => { observer.unsubscribe(); return false; @@ -160,47 +163,47 @@ export class FilesPage extends React.Component { } } - onSort(_sort){ + onSort(_sort) { settings_put("filespage_sort", _sort); const same_sort = _sort === this.state.sort; this.setState({ - sort: _sort + sort: _sort, }, () => { requestAnimationFrame(() => { let files = sort(this.state.files, _sort); - if(same_sort && this.state.sort_reverse) files = files.reverse(); + if (same_sort && this.state.sort_reverse) files = files.reverse(); this.setState({ page_number: PAGE_NUMBER_INIT, sort_reverse: same_sort ? !this.state.sort_reverse : true, - files: files + files: files, }); }); }); } - onView(){ + onView() { const _view = this.state.view === "list" ? "grid" : "list"; settings_put("filespage_view", _view); this.setState({ - view: _view + view: _view, }, () => { requestAnimationFrame(() => { this.setState({ - page_number: PAGE_NUMBER_INIT + page_number: PAGE_NUMBER_INIT, }); }); }); } - onSearch(search){ - if(search == null || search.length === 0){ + onSearch(search) { + if (search == null || search.length === 0) { this.onRefresh(); return; } - if(search.length < 2){ + if (search.length < 2) { return; } - if(this._search){ + if (this._search) { this._search.unsubscribe(); } this.setState({ @@ -216,8 +219,8 @@ export class FilesPage extends React.Component { metadata: { can_rename: false, can_delete: false, - can_share: false - } + can_share: false, + }, }); }, (err) => { this.setState({ @@ -225,26 +228,26 @@ export class FilesPage extends React.Component { metadata: { can_rename: false, can_delete: false, - can_share: false - } + can_share: false, + }, }); notify.send(err, "error"); }); } - loadMore(){ + loadMore() { requestAnimationFrame(() => { - let page_number = this.state.page_number + 1; - this.setState({page_number: page_number}); + const page_number = this.state.page_number + 1; + this.setState({ page_number: page_number }); }); } - handleMultiSelect(selectedFiles, e){ - this.setState({selected: selectedFiles.map((f) => f.path)}); + handleMultiSelect(selectedFiles, e) { + this.setState({ selected: selectedFiles.map((f) => f.path) }); } - toggleSelect(path){ + toggleSelect(path) { const idx = this.state.selected.indexOf(path); - if(idx == -1){ + if (idx == -1) { this.setState({ selected: this.state.selected.concat([path]) }); } else { this.state.selected.splice(idx, 1); @@ -253,41 +256,73 @@ export class FilesPage extends React.Component { } render() { - let $moreLoading = (
); - if(this.state.files.length <= this.state.page_number * LOAD_PER_SCROLL){ + let $moreLoading = ( +
+ +
+ ); + if (this.state.files.length <= this.state.page_number * LOAD_PER_SCROLL) { $moreLoading = null; } return (
- - -
-
- 70} - initialLoad={false} useWindow={false} loadMore={this.loadMore.bind(this)} threshold={100}> - - - - - this.onView(value)} onSortUpdate={(value) => this.onSort(value)} accessRight={this.state.metadata || {}} selected={this.state.selected}> - - - - - - - - - -
-
-
-
-
-
+ + +
+
+ 70} + initialLoad={false} useWindow={false} + loadMore={this.loadMore.bind(this)} threshold={100}> + + + + + this.onView(value)} + onSortUpdate={(value) => this.onSort(value)} + accessRight={this.state.metadata || {}} + selected={this.state.selected} /> + + + + + + + + + +
+
+
+
+
+
); } } + +export const FilesPage = ErrorPage( + LoggedInOnly( + EventReceiver( + DragDropContext(HTML5Backend)( + FilesPageComponent, + ), + ), + ), +); diff --git a/client/pages/filespage/breadcrumb.js b/client/pages/filespage/breadcrumb.js index 097bc241..bc9bf2c8 100644 --- a/client/pages/filespage/breadcrumb.js +++ b/client/pages/filespage/breadcrumb.js @@ -1,73 +1,91 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { DropTarget } from 'react-dnd'; +import { DropTarget } from "react-dnd"; -import { EventEmitter, BreadCrumb, PathElement } from '../../components/'; -import { pathBuilder, filetype, basename } from '../../helpers/'; +import { EventEmitter, BreadCrumb, PathElement } from "../../components/"; +import { pathBuilder, filetype, basename } from "../../helpers/"; export class BreadCrumbTargettable extends BreadCrumb { - constructor(props){ + constructor(props) { super(props); } - render(){ + render() { return super.render(Element); } } -const fileTarget = { - canDrop(props){ - return props.isLast ? false : true; - }, - drop(props, monitor, component){ - let src = monitor.getItem(); - if(props.currentSelection.length === 0){ - const from = pathBuilder(src.path, src.name, src.type); - const to = pathBuilder(props.path.full, src.name, src.type); - return {action: 'rename', args: [from, to, src.type], ctx: 'breadcrumb'}; - } else { - return {action: 'rename.multiple', args: props.currentSelection.map((selectionPath) => { - const from = selectionPath; - const to = pathBuilder( - props.path.full, - "./"+basename(selectionPath), - filetype(selectionPath) - ); - return [from, to]; - })}; - } - } -} -const nativeFileTarget = { - canDrop(props){ - return props.isLast ? false : true; - }, - drop: function(props, monitor){ - let files = monitor.getItem(); - props.emit('file.upload', props.path.full, files); - } +const HOCDropTargetForVirtualFile = (Cmp) => { + const fileTarget = { + canDrop(props) { + return props.isLast ? false : true; + }, + drop(props, monitor, component) { + const src = monitor.getItem(); + if (props.currentSelection.length === 0) { + const from = pathBuilder(src.path, src.name, src.type); + const to = pathBuilder(props.path.full, src.name, src.type); + return { action: "rename", args: [from, to, src.type], ctx: "breadcrumb" }; + } else { + return { + action: "rename.multiple", + args: props.currentSelection.map((selectionPath) => { + const from = selectionPath; + const to = pathBuilder( + props.path.full, + "./"+basename(selectionPath), + filetype(selectionPath), + ); + return [from, to]; + }), + }; + } + }, + }; + + return DropTarget( + "file", + fileTarget, + (connect, monitor) => ({ + connectDropTarget: connect.dropTarget(), + isOver: monitor.isOver(), + canDrop: monitor.canDrop(), + }), + )(Cmp); }; -@EventEmitter -@DropTarget('__NATIVE_FILE__', nativeFileTarget, (connect, monitor) => ({ - connectNativeFileDropTarget: connect.dropTarget(), - isNativeFileOver: monitor.isOver(), - canDropFile: monitor.canDrop() -})) -@DropTarget('file', fileTarget, (connect, monitor) => ({ - connectDropTarget: connect.dropTarget(), - isOver: monitor.isOver(), - canDrop: monitor.canDrop() -})) -class Element extends PathElement { - constructor(props){ - super(props) +const HOCDropTargetForFSFile = (Cmp) => { + const nativeFileTarget = { + canDrop(props) { + return props.isLast ? false : true; + }, + drop: function(props, monitor) { + const files = monitor.getItem(); + props.emit("file.upload", props.path.full, files); + }, + }; + + return DropTarget( + "__NATIVE_FILE__", + nativeFileTarget, + (connect, monitor) => ({ + connectNativeFileDropTarget: connect.dropTarget(), + isNativeFileOver: monitor.isOver(), + canDropFile: monitor.canDrop(), + }), + )(Cmp); +}; + +class ElementComponent extends PathElement { + constructor(props) { + super(props); } - render(){ - let highlight = (this.props.isOver && this.props.canDrop ) || (this.props.isNativeFileOver && this.props.canDropFile); + render() { + const highlight = (this.props.isOver && this.props.canDrop ) || + (this.props.isNativeFileOver && this.props.canDropFile); return this.props.connectNativeFileDropTarget(this.props.connectDropTarget( - super.render(highlight) + super.render(highlight), )); } } + +const Element = EventEmitter(HOCDropTargetForVirtualFile(HOCDropTargetForFSFile(ElementComponent))); diff --git a/client/pages/filespage/filesystem.js b/client/pages/filespage/filesystem.js index 848fb3aa..f57c9f54 100644 --- a/client/pages/filespage/filesystem.js +++ b/client/pages/filespage/filesystem.js @@ -1,60 +1,84 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; -import { DropTarget } from 'react-dnd'; - -import Path from 'path'; +import React from "react"; +import PropTypes from "prop-types"; +import ReactCSSTransitionGroup from "react-addons-css-transition-group"; +import { DropTarget } from "react-dnd"; import "./filesystem.scss"; -import { Container, NgIf, Icon } from '../../components/'; -import { NewThing } from './thing-new'; -import { ExistingThing } from './thing-existing'; -import { FileZone } from './filezone'; -import { t } from '../../locales/'; +import { Container, NgIf, Icon } from "../../components/"; +import { NewThing } from "./thing-new"; +import { ExistingThing } from "./thing-existing"; +import { FileZone } from "./filezone"; +import { t } from "../../locales/"; -@DropTarget('__NATIVE_FILE__', {}, (connect, monitor) => ({ - connectDropFile: connect.dropTarget(), - fileIsOver: monitor.isOver() -})) -export class FileSystem extends React.PureComponent { +const HOCDropTargetForFSFile = (Cmp) => { + return DropTarget( + "__NATIVE_FILE__", + {}, + (connect, monitor) => ({ + connectDropFile: connect.dropTarget(), + fileIsOver: monitor.isOver(), + }), + )(Cmp); +}; + +class FileSystemComponent extends React.PureComponent { render() { return this.props.connectDropFile(
- - this.props.onView(value)} onSortUpdate={(value) => {this.props.onSort(value);}} accessRight={this.props.metadata || {}}> - - - - 0}> - - { - this.props.files.map((file, index) => { - if(file.type === 'directory' || file.type === 'file' || file.type === 'link' || file.type === 'bucket'){ - return ( ); - } - return null; - }) - } - - - -

- -

-

{ t("There is nothing here") }

-
-
-
+ + this.props.onView(value)} + onSortUpdate={(value) => this.props.onSort(value)} + accessRight={this.props.metadata || {}} /> + + + + 0}> + + { + this.props.files.map((file, index) => { + if (file.type === "directory" || file.type === "file" || + file.type === "link" || file.type === "bucket") { + return ( + + ); + } + return null; + }) + } + + + +

+ +

+

{ t("There is nothing here") }

+
+
+
, ); } } -FileSystem.propTypes = { +FileSystemComponent.propTypes = { path: PropTypes.string.isRequired, files: PropTypes.array.isRequired, metadata: PropTypes.object.isRequired, sort: PropTypes.string.isRequired, view: PropTypes.string.isRequired, onView: PropTypes.func.isRequired, - onSort: PropTypes.func.isRequired + onSort: PropTypes.func.isRequired, }; + +export const FileSystem = HOCDropTargetForFSFile(FileSystemComponent); diff --git a/client/pages/filespage/filezone.js b/client/pages/filespage/filezone.js index d2c6e5db..987b1af3 100644 --- a/client/pages/filespage/filezone.js +++ b/client/pages/filespage/filezone.js @@ -1,65 +1,59 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; -import { DropTarget } from 'react-dnd'; +import React from "react"; +import ReactCSSTransitionGroup from "react-addons-css-transition-group"; +import { DropTarget } from "react-dnd"; -import { EventEmitter, Icon } from '../../components/'; -import { t } from '../../locales/'; -import './filezone.scss'; +import { EventEmitter, Icon } from "../../components/"; +import { t } from "../../locales/"; +import "./filezone.scss"; -@EventEmitter -@DropTarget('__NATIVE_FILE__', { - drop(props, monitor){ - props.emit('file.upload', props.path, monitor.getItem()); - } -}, (connect, monitor) => ({ - connectDropFile: connect.dropTarget(), - fileIsOver: monitor.isOver() -})) -export class FileZone extends React.Component{ - constructor(props){ - super(props); - } - - render(){ - return this.props.connectDropFile( -
- { t("DROP HERE TO UPLOAD") } -
- ); - } -} -FileZone.propTypes = { - path: PropTypes.string.isRequired +function FileZoneComponent({ connectDropFile, fileIsOver }) { + return connectDropFile( +
+ { t("DROP HERE TO UPLOAD") } +
, + ); } -@EventEmitter -export class MobileFileUpload extends React.Component{ - constructor(props){ - super(props); +const HOCDropTargetForFSFile = (Cmp) => { + return DropTarget( + "__NATIVE_FILE__", { + drop(props, monitor) { + props.emit("file.upload", props.path, monitor.getItem()); + }, + }, + (connect, monitor) => ({ + connectDropFile: connect.dropTarget(), + fileIsOver: monitor.isOver(), + }), + )(Cmp); +}; + +export const FileZone = EventEmitter(HOCDropTargetForFSFile(FileZoneComponent)); + +function MobileFileUploadComponent({ emit, path, accessRight }) { + if (!window.CONFIG["upload_button"] && /(Android|iPad|iPhone)/.test(navigator.userAgent) === false) { + return null; + } else if (accessRight.can_create_file === false || accessRight.can_create_directory === false) { + return null; } - onUpload(e){ - this.props.emit("file.upload", this.props.path, e); - } - render(){ - if(!window.CONFIG["upload_button"] && /(Android|iPad|iPhone)/.test(navigator.userAgent) === false){ - return null; - } else if(this.props.accessRight.can_create_file === false || this.props.accessRight.can_create_directory === false) { - return null; - } - - return ( - -
+ const onUpload = (e) => { + emit("file.upload", path, e); + }; + return ( + +
- - + +
-
-
- ); - } +
+
+ ); } + +export const MobileFileUpload = EventEmitter(MobileFileUploadComponent); diff --git a/client/pages/filespage/frequently_access.js b/client/pages/filespage/frequently_access.js index 55d69cfd..89271135 100644 --- a/client/pages/filespage/frequently_access.js +++ b/client/pages/filespage/frequently_access.js @@ -1,47 +1,48 @@ -import React from 'react'; -import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import React from "react"; +import ReactCSSTransitionGroup from "react-addons-css-transition-group"; -import { Container, Icon, NgIf } from '../../components/'; -import { Link } from 'react-router-dom'; -import Path from 'path'; -import { t } from '../../locales/'; +import { Container, Icon, NgIf } from "../../components/"; +import { Link } from "react-router-dom"; +import Path from "path"; +import { t } from "../../locales/"; -import './frequently_access.scss'; +import "./frequently_access.scss"; -export class FrequentlyAccess extends React.Component { - constructor(props){ - super(props); - } - - render(){ - return ( -
- - - 0}> - {t("Quick Access")} -
- { - this.props.files && this.props.files.map(function(path, index){ - return ( - - -
{Path.basename(path)}
- - ); - }) - } -
-
- - - { t("Frequently access folders will be shown here") } - +export function FrequentlyAccess({ files }) { + return ( +
+ + + 0}> + {t("Quick Access")} +
+ { + files && files.map((path, index) => { + return ( + + +
{Path.basename(path)}
+ + ); + }) + } +
+
+ + + { t("Frequently access folders will be shown here") } +
-
-
- ); - } +
+
+ ); } diff --git a/client/pages/filespage/index.js b/client/pages/filespage/index.js index e1ada558..7852b1c6 100644 --- a/client/pages/filespage/index.js +++ b/client/pages/filespage/index.js @@ -1,4 +1,4 @@ -export { FileSystem } from './filesystem'; -export { Submenu } from './submenu'; -export { BreadCrumbTargettable as BreadCrumb } from './breadcrumb'; -export { FrequentlyAccess } from './frequently_access'; +export { FileSystem } from "./filesystem"; +export { Submenu } from "./submenu"; +export { BreadCrumbTargettable as BreadCrumb } from "./breadcrumb"; +export { FrequentlyAccess } from "./frequently_access"; diff --git a/client/pages/filespage/share.js b/client/pages/filespage/share.js index 7fe09bdf..599cb66f 100644 --- a/client/pages/filespage/share.js +++ b/client/pages/filespage/share.js @@ -1,20 +1,22 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React, { createRef } from "react"; -import { NgIf, Icon, Button } from '../../components/'; -import { Share } from '../../model/'; -import { randomString, notify, absoluteToRelative, copyToClipboard, filetype } from '../../helpers/'; -import { t } from '../../locales/'; -import './share.scss'; +import { NgIf, Icon } from "../../components/"; +import { Share } from "../../model/"; +import { + randomString, notify, absoluteToRelative, copyToClipboard, filetype, +} from "../../helpers/"; +import { t } from "../../locales/"; +import "./share.scss"; export class ShareComponent extends React.Component { - constructor(props){ + constructor(props) { super(props); this.state = this.resetState(); this.state.existings = []; + this.$input = createRef(); } - resetState(){ + resetState() { return { role: null, path: null, @@ -28,78 +30,77 @@ export class ShareComponent extends React.Component { can_read: null, can_write: null, can_upload: null, - show_advanced: false + show_advanced: false, }; } - componentDidMount(){ + componentDidMount() { Share.all(this.props.path).then((existings) => { this.refreshModal(); this.setState({ existings: existings.sort((a, b) => { return a.path.split("/").length > b.path.split("/").length; }), - role: existings.length === 0 ? window.CONFIG["share_default_access"] : null + role: existings.length === 0 ? window.CONFIG["share_default_access"] : null, }); }); } - updateState(key, value){ - if(key === "role"){ + updateState(key, value) { + if (key === "role") { this.setState(this.resetState()); } - if(this.state[key] === value){ - this.setState({[key]: null}); - }else{ - this.setState({[key]: value}); + if (this.state[key] === value) { + this.setState({ [key]: null }); + } else { + this.setState({ [key]: value }); } - if((key === "role" && value) || (key === "show_advanced")){ + if ((key === "role" && value) || (key === "show_advanced")) { this.refreshModal(); } } - refreshModal(){ - window.dispatchEvent(new Event('resize')); + refreshModal() { + window.dispatchEvent(new Event("resize")); } - onLoad(link){ - let st = Object.assign({}, link); + onLoad(link) { + const st = Object.assign({}, link); st.show_advanced = false; st.link_id = st.id; st.role = (st.role || "").toLowerCase(); this.setState(st); } - onDeleteLink(link_id){ - let removed = null, - i = 0; + onDeleteLink(link_id) { + let removed = null; + let i = 0; - for(i=0; i < this.state.existings.length; i++){ - if(this.state.existings[i].id === link_id){ + for (i=0; i < this.state.existings.length; i++) { + if (this.state.existings[i].id === link_id) { removed = Object.assign({}, this.state.existings[i]); break; } } - if(removed !== null){ + if (removed !== null) { this.state.existings.splice(i, 1); - this.setState({existings: this.state.existings}); + this.setState({ existings: this.state.existings }); } - return Share.remove(link_id) - .catch((err) => { - this.setState({existings: [removed].concat(this.state.existings)}); - notify.send(err, "error"); - }); + return Share.remove(link_id).catch((err) => { + this.setState({ existings: [removed].concat(this.state.existings) }); + notify.send(err, "error"); + }); } - copyLinkInClipboard(link){ + copyLinkInClipboard(link) { copyToClipboard(link); notify.send(t("The link was copied in the clipboard"), "INFO"); } - onRegisterLink(e){ - this.copyLinkInClipboard(this.refs.$input.value); + onRegisterLink(e) { + this.copyLinkInClipboard(this.$input.current.value); const link = { role: this.state.role, @@ -108,46 +109,47 @@ export class ShareComponent extends React.Component { url: this.state.url, users: this.state.users || null, password: this.state.password || null, - expire: function(e){ - if(typeof e === "string") + expire: function(e) { + if (typeof e === "string") { return new Date(e).getTime(); + } return null; }(this.state.expire), can_manage_own: this.state.can_manage_own, can_share: this.state.can_share, - can_read: function(r){ - if(r === "viewer") return true; - else if(r === "editor") return true; + can_read: function(r) { + if (r === "viewer") return true; + else if (r === "editor") return true; return false; }(this.state.role), - can_write: function(r){ - if(r === "editor") return true; + can_write: function(r) { + if (r === "editor") return true; return false; }(this.state.role), - can_upload: function(r){ - if(r === "uploader") return true; - else if(r === "editor") return true; + can_upload: function(r) { + if (r === "uploader") return true; + else if (r === "editor") return true; return false; - }(this.state.role) + }(this.state.role), }; - let links = [link]; - for(let i=0; i { - if(this.state.url !== null && this.state.url !== this.state.id){ + if (this.state.url !== null && this.state.url !== this.state.id) { this.onDeleteLink(this.state.id); } return Promise.resolve(); @@ -161,32 +163,32 @@ export class ShareComponent extends React.Component { } - render(){ - const beautifulPath = function(from, to){ + render() { + const beautifulPath = function(from, to) { to = from.replace(/\/$/, "") + to; - if(filetype(from) === "directory"){ - from = from.split("/") - from = from.slice(0, from.length - 1) - from = from.join("/") + if (filetype(from) === "directory") { + from = from.split("/"); + from = from.slice(0, from.length - 1); + from = from.join("/"); } - let p = absoluteToRelative(from, to); + const p = absoluteToRelative(from, to); return p.length < to.length ? p : to; }; - const urlify = function(str){ - if(typeof str !== "string") return ""; + const urlify = function(str) { + if (typeof str !== "string") return ""; str = str.replace(/\s+/g, "+"); str = str.replace(/[^a-zA-Z0-9\+-_]/g, "_"); str = str.replace(/_+/g, "_"); return str; }; - const datify = function(str){ - if(!str) return str; + const datify = function(str) { + if (!str) return str; const d = new Date(str); // old browser not implementing input[type=date] elements // may return invalid date, - if(isNaN(d.getDate())) return str; + if (isNaN(d.getDate())) return str; const pad2 = (a) => ("00"+a).slice(-2); const pad4 = (a) => ("0000"+a).slice(-4); @@ -195,73 +197,126 @@ export class ShareComponent extends React.Component { return (
-

{ t("Create a New Link") }

+

{ t("Create a New Link") }

-
- { this.props.type === "file" ? null : -
- { t("Uploader") } -
- } -
- { t("Viewer") } -
-
- { t("Editor") } -
-
- - 0}> -

{ t("Existing Links") }

-
5 ? '90px' : 'inherit'}}> - { - this.state.existings && this.state.existings.map((link, i) => { - return ( -
- - { t(link.role) } - - {beautifulPath(this.props.path, link.path)} - - -
- ); - }) - } -
-
- - -

{ t("Restrictions") }

-
- - +
+ { this.props.type === "file" ? null : +
+ { t("Uploader") } +
+ } +
+ { t("Viewer") } +
+
+ { t("Editor") } +
-

- { t("Advanced") } - - -

-
- - - - - - this.updateState('url', urlify(val))} inputType="text"/> - -
+ 0}> +

{ t("Existing Links") }

+
5 ? "90px" : "inherit" }}> + { + this.state.existings && this.state.existings.map((link, i) => { + return ( +
+ + { t(link.role) } + + + { beautifulPath(this.props.path, link.path) } + + + +
+ ); + }) + } +
+
-
- -
- -
-
- + +

{ t("Restrictions") }

+
+ + +
+ +

+ { t("Advanced") } + + +

+
+ + + + + + this.updateState("url", urlify(val))} + inputType="text" /> + +
+ +
+ +
+ +
+
+
); } @@ -269,7 +324,7 @@ export class ShareComponent extends React.Component { const SuperCheckbox = (props) => { const onCheckboxTick = (e) => { - if(props.inputType === undefined){ + if (props.inputType === undefined) { return props.onChange(e.target.checked ? true : false); } return props.onChange(e.target.checked ? "" : null); @@ -277,26 +332,23 @@ const SuperCheckbox = (props) => { const onValueChange = (e) => { props.onChange(e.target.value); }; - const _is_expended = function(val){ + const _is_expended = function(val) { return val === null || val === undefined || val === false ? false : true; }(props.value); return (
- - - - + + + +
); }; -// SuperCheckbox.propTypes = { -// label: PropTypes.string.isRequired, -// onChange: PropTypes.func.isRequired, -// inputType: PropTypes.string, -// placeholder: PropTypes.string, -// value: PropTypes.boolean -// }; diff --git a/client/pages/filespage/submenu.js b/client/pages/filespage/submenu.js index ecef855e..e6c1969c 100644 --- a/client/pages/filespage/submenu.js +++ b/client/pages/filespage/submenu.js @@ -1,177 +1,228 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import React, { createRef } from "react"; +import PropTypes from "prop-types"; +import ReactCSSTransitionGroup from "react-addons-css-transition-group"; -import { Card, NgIf, Icon, EventEmitter, Dropdown, DropdownButton, DropdownList, DropdownItem, Container } from '../../components/'; -import { pathBuilder, debounce, prompt } from '../../helpers/'; -import { t } from '../../locales/'; +import { + NgIf, Icon, EventEmitter, Dropdown, DropdownButton, DropdownList, + DropdownItem, Container, +} from "../../components/"; +import { debounce, prompt } from "../../helpers/"; +import { t } from "../../locales/"; import "./submenu.scss"; -@EventEmitter -export class Submenu extends React.Component { - constructor(props){ +class SubmenuComponent extends React.Component { + constructor(props) { super(props); this.state = { search_input_visible: false, - search_keyword: "" + search_keyword: "", }; + this.$input = createRef(); this.onSearchChange_Backpressure = debounce(this.onSearchChange, 400); this._onKeyPress = (e) => { - if(e.keyCode === 27){ // escape key + if (e.keyCode === 27) { // escape key this.setState({ search_keyword: "", - search_input_visible: false + search_input_visible: false, }); - if(this.refs.$input) this.refs.$input.blur(); + if (this.$input.current) this.$input.current.blur(); this.props.onSearch(null); - }else if(e.ctrlKey && e.keyCode === 70){ // 'Ctrl F' shortcut to search + } else if (e.ctrlKey && e.keyCode === 70) { // 'Ctrl F' shortcut to search e.preventDefault(); this.setState({ - search_input_visible: true + search_input_visible: true, }); - if(this.refs.$input) this.refs.$input.focus(); - }else if(e.altKey && (e.keyCode === 49 || e.keyCode === 50)){ // 'alt 1' 'alt 2' shortcut + if (this.$input.current) this.$input.current.focus(); + } else if (e.altKey && (e.keyCode === 49 || e.keyCode === 50)) { // 'alt 1' 'alt 2' shortcut e.preventDefault(); this.onViewChange(); } }; } - componentDidMount(){ + componentDidMount() { window.addEventListener("keydown", this._onKeyPress); } - componentWillUnmount(){ + componentWillUnmount() { window.removeEventListener("keydown", this._onKeyPress); } - onNew(type){ + onNew(type) { this.props.emit("new::"+type); } - onDelete(arrayOfPaths){ + onDelete(arrayOfPaths) { prompt.now( t("Confirm by typing") + " \"remove\"", (answer) => { - if(answer !== "remove"){ + if (answer !== "remove") { return Promise.resolve(); } this.props.emit("file.delete.multiple", arrayOfPaths); return Promise.resolve(); }, - () => { /* click on cancel */ } + () => {/* click on cancel */}, ); } - onDownload(arrayOfPaths){ + onDownload(arrayOfPaths) { this.props.emit("file.download.multiple", arrayOfPaths); } - onViewChange(){ + onViewChange() { requestAnimationFrame(() => this.props.onViewUpdate()); } - onSortChange(e){ + onSortChange(e) { this.props.onSortUpdate(e); } - onSearchChange(search, e){ + onSearchChange(search, e) { this.props.onSearch(search.trim()); } - onSearchToggle(){ - if(new Date () - this.search_last_toggle < 200){ + onSearchToggle() { + if (new Date() - this.search_last_toggle < 200) { // avoid bluring event cancelling out the toogle return; } - this.refs.$input.focus(); - this.setState({search_input_visible: !this.state.search_input_visible}, () => { - if(this.state.search_input_visible == false){ + if (this.$input.current) this.$input.current.focus(); + this.setState({ + search_input_visible: !this.state.search_input_visible, + }, () => { + if (this.state.search_input_visible == false) { this.props.onSearch(null); - this.setState({search_keyword: ""}); + this.setState({ search_keyword: "" }); } }); } - closeIfEmpty(){ - if(this.state.search_keyword.trim().length > 0) return; + closeIfEmpty() { + if (this.state.search_keyword.trim().length > 0) return; this.search_last_toggle = new Date(); this.setState({ search_input_visible: false, - search_keyword: "" + search_keyword: "", }); this.props.onSearch(null); } - onSearchKeypress(s, backpressure = true, e){ - if(backpressure){ + onSearchKeypress(s, backpressure = true, e) { + if (backpressure) { this.onSearchChange_Backpressure(s); - }else{ + } else { this.onSearchChange(s); } - this.setState({search_keyword: s}); + this.setState({ search_keyword: s }); - if(e && e.preventDefault){ + if (e && e.preventDefault) { e.preventDefault(); } } - render(){ + render() { return (
- -
- - { window.innerWidth < 410 && t("New File").length > 10 ? t("New File", null, "NEW_FILE::SHORT") : t("New File") } - - - { window.innerWidth < 410 && t("New Directory").length > 10 ? t("New Directory", null, "NEW_DIRECTORY::SHORT") : t("New Directory") } - - 0} type="inline" onMouseDown={this.onDownload.bind(this, this.props.selected)}> - - { t("Download") } - - - 0 && this.props.accessRight.can_delete !== false} type="inline" onMouseDown={this.onDelete.bind(this, this.props.selected)}> - - { t("Remove") } - - + +
+ + { window.innerWidth < 410 && t("New File").length > 10 ? t("New File", null, "NEW_FILE::SHORT") : t("New File") } + + + { window.innerWidth < 410 && t("New Directory").length > 10 ? t("New Directory", null, "NEW_DIRECTORY::SHORT") : t("New Directory") } + + 0} + type="inline" + onMouseDown={this.onDownload.bind(this, this.props.selected)}> + + { t("Download") } + + + 0 && this.props.accessRight.can_delete !== false} + type="inline" + onMouseDown={this.onDelete.bind(this, this.props.selected)}> + + { t("Remove") } + + - - - - - - { t("Sort By Type") } - { t("Sort By Date") } - { t("Sort By Name") } - - -
-
-
this.onSearchKeypress(this.state.search_keyword, false, e)}> - - - this.onSearchKeypress(e.target.value, true)} type="text" id="search" placeholder={ t("search") } name="search" autoComplete="off" /> - - -
-
-
-
+ + + + + + + { t("Sort By Type") } + + + { t("Sort By Date") } + + + { t("Sort By Name") } + + + +
+ +
+
+
this.onSearchKeypress(this.state.search_keyword, false, e)}> + + + this.onSearchKeypress(e.target.value, true)} + type="text" + id="search" + placeholder={ t("search") } + name="search" + autoComplete="off" /> + + +
+
+
+
); }; } -Submenu.propTypes = { +SubmenuComponent.propTypes = { accessRight: PropTypes.object, onSortUpdate: PropTypes.func.isRequired, - sort: PropTypes.string.isRequired + sort: PropTypes.string.isRequired, }; + +export const Submenu = EventEmitter(SubmenuComponent); diff --git a/client/pages/filespage/thing-existing.js b/client/pages/filespage/thing-existing.js index 6dfa85f7..0af30a40 100644 --- a/client/pages/filespage/thing-existing.js +++ b/client/pages/filespage/thing-existing.js @@ -1,247 +1,277 @@ -import React from 'react'; -import path from 'path'; -import PropTypes from 'prop-types'; -import { Link } from 'react-router-dom'; -import { DragSource, DropTarget } from 'react-dnd'; -import { createSelectable } from 'react-selectable'; +import React, { createRef } from "react"; +import path from "path"; +import { Link } from "react-router-dom"; +import { DragSource, DropTarget } from "react-dnd"; +import { createSelectable } from "react-selectable"; -import './thing.scss'; -import { Card, NgIf, Icon, EventEmitter, Button, img_placeholder } from '../../components/'; -import { pathBuilder, basename, filetype, prompt, alert, leftPad, getMimeType, debounce, memory } from '../../helpers/'; -import { Files } from '../../model/'; -import { ShareComponent } from './share'; -import { t } from '../../locales/'; +import "./thing.scss"; +import { Card, NgIf, Icon, EventEmitter, img_placeholder } from "../../components/"; +import { pathBuilder, basename, filetype, prompt, alert, leftPad, getMimeType, debounce, memory } from "../../helpers/"; +import { Files } from "../../model/"; +import { ShareComponent } from "./share"; +import { t } from "../../locales/"; -const fileSource = { - beginDrag(props, monitor, component) { - return { - path: props.path, - name: props.file.name, - type: props.file.type - }; - }, - canDrag(props, monitor){ - if (props.metadata.can_move === false){ - return false; - } - if(props.file.icon === "loading") return false; - else if(props.selected === false && props.currentSelection.length > 0) return false; - return true; - }, - endDrag(props, monitor, component){ - if(monitor.didDrop() && component.state.icon !== 'loading'){ - let result = monitor.getDropResult(); - if(result.action === 'rename'){ - props.emit.apply(component, ['file.rename'].concat(result.args)); - }else if(result.action === "rename.multiple"){ - props.emit.call(component, "file.rename.multiple", result.args); - }else{ - throw 'unknown action'; + +const canDrop = (props, monitor) => { + const file = monitor.getItem(); + if (props.file.type !== "directory") return false; + else if (file.name === props.file.name) return false; + else if (props.file.icon === "loading") return false; + else if (props.selected === true) return false; + return true; +}; + +const HOCDropTargetForFsFile = (Cmp) => { + const nativeFileTarget = { + canDrop, + drop(props, monitor) { + const path = pathBuilder(props.path, props.file.name, "directory"); + props.emit( + "file.upload", + path, + monitor.getItem(), + ); + }, + }; + + return DropTarget( + "__NATIVE_FILE__", + nativeFileTarget, + (connect, monitor) => ({ + connectDropNativeFile: connect.dropTarget(), + nativeFileIsOver: monitor.isOver(), + canDropNativeFile: monitor.canDrop(), + }), + )(Cmp); +}; + +const HOVDropTargetForVirtualFile = (Cmp) => { + const fileTarget = { + canDrop, + drop(props, monitor, component) { + const src = monitor.getItem(); + const dest = props.file; + + if (props.currentSelection.length === 0) { + const from = pathBuilder(props.path, src.name, src.type); + const to = pathBuilder(props.path, "./"+dest.name+"/"+src.name, src.type); + return { + action: "rename", + args: [from, to, src.type], + ctx: "existingfile", + }; + } else { + return { + action: "rename.multiple", + args: props.currentSelection.map((selectionPath) => { + const from = selectionPath; + const to = pathBuilder( + props.path, + "./"+dest.name+"/"+basename(selectionPath), + filetype(selectionPath), + ); + return [from, to]; + }), + }; } - } - } + }, + }; + return DropTarget( + "file", + fileTarget, + (connect, monitor) => ({ + connectDropFile: connect.dropTarget(), + fileIsOver: monitor.isOver(), + canDropFile: monitor.canDrop(), + }), + )(Cmp); }; -const fileTarget = { - canDrop(props, monitor){ - let file = monitor.getItem(); - if(props.file.type !== "directory") return false; - else if(file.name === props.file.name) return false; - else if(props.file.icon === "loading") return false; - else if(props.selected === true) return false; - return true; - }, - drop(props, monitor, component){ - let src = monitor.getItem(); - let dest = props.file; +const HOVDropSourceForVirtualFile = (Cmp) => { + const fileSource = { + beginDrag(props, monitor, component) { + return { + path: props.path, + name: props.file.name, + type: props.file.type, + }; + }, + canDrag(props, monitor) { + if (props.metadata.can_move === false) { + return false; + } + if (props.file.icon === "loading") return false; + else if (props.selected === false && props.currentSelection.length > 0) return false; + return true; + }, + endDrag(props, monitor, component) { + if (monitor.didDrop() && component.state.icon !== "loading") { + const result = monitor.getDropResult(); + if (result.action === "rename") { + props.emit.apply(component, ["file.rename"].concat(result.args)); + } else if (result.action === "rename.multiple") { + props.emit.call(component, "file.rename.multiple", result.args); + } else { + throw new Error("unknown action"); + } + } + }, + }; - if(props.currentSelection.length === 0){ - const from = pathBuilder(props.path, src.name, src.type); - const to = pathBuilder(props.path, './'+dest.name+'/'+src.name, src.type); - return {action: 'rename', args: [from, to, src.type], ctx: 'existingfile'}; - } else { - return {action: 'rename.multiple', args: props.currentSelection.map((selectionPath) => { - const from = selectionPath; - const to = pathBuilder( - props.path, - "./"+dest.name+"/"+basename(selectionPath), - filetype(selectionPath) - ); - return [ from, to ]; - })}; - } - } + return DragSource( + "file", + fileSource, + (connect, monitor) => ({ + connectDragSource: connect.dragSource(), + isDragging: monitor.isDragging(), + }), + )(Cmp); }; -const nativeFileTarget = { - canDrop: fileTarget.canDrop, - drop(props, monitor){ - let path = pathBuilder(props.path, props.file.name, 'directory'); - props.emit('file.upload', path, monitor.getItem()); - } -}; - - -@createSelectable -@EventEmitter -@DropTarget('__NATIVE_FILE__', nativeFileTarget, (connect, monitor) => ({ - connectDropNativeFile: connect.dropTarget(), - nativeFileIsOver: monitor.isOver(), - canDropNativeFile: monitor.canDrop() -})) -@DropTarget('file', fileTarget, (connect, monitor) => ({ - connectDropFile: connect.dropTarget(), - fileIsOver: monitor.isOver(), - canDropFile: monitor.canDrop() -})) -@DragSource('file', fileSource, (connect, monitor) => ({ - connectDragSource: connect.dragSource(), - isDragging: monitor.isDragging() -})) -export class ExistingThing extends React.Component { - constructor(props){ +class ExistingThingComponent extends React.Component { + constructor(props) { super(props); this.state = { hover: null, filename: props.file.name, is_renaming: false, - preview: null + preview: null, }; } - shouldComponentUpdate(nextProps, nextState){ - if(nextState.hover !== this.state.hover || - nextState.is_renaming !== this.state.is_renaming || - nextProps.view !== this.props.view || - this.state.preview !== nextState.preview || - this.props.fileIsOver !== nextProps.fileIsOver || - this.props.canDropFile !== nextProps.canDropFile || - this.props.nativeFileIsOver !== nextProps.nativeFileIsOver || - this.props.canDropNativeFile !== nextProps.canDropNativeFile || - this.props.selected !== nextProps.selected - ){ - return true; - } + shouldComponentUpdate(nextProps, nextState) { + if (nextState.hover !== this.state.hover || + nextState.is_renaming !== this.state.is_renaming || + nextProps.view !== this.props.view || + this.state.preview !== nextState.preview || + this.props.fileIsOver !== nextProps.fileIsOver || + this.props.canDropFile !== nextProps.canDropFile || + this.props.nativeFileIsOver !== nextProps.nativeFileIsOver || + this.props.canDropNativeFile !== nextProps.canDropNativeFile || + this.props.selected !== nextProps.selected) return true; return false; } - componentDidMount(){ + componentDidMount() { this.updateThumbnail(this.props); } - componentWillReceiveProps(props){ - if(props.view !== this.props.view){ + UNSAFE_componentWillReceiveProps(props) { + if (props.view !== this.props.view) { this.updateThumbnail(props); } } - updateThumbnail(props){ - if(props.view === "grid" && props.icon !== "loading"){ + updateThumbnail(props) { + if (props.view === "grid" && props.icon !== "loading") { const type = getMimeType(props.file.path).split("/")[0]; - if(type === "image"){ + if (type === "image") { Files.url(props.file.path).then((url) => { - this.setState({preview: url+"&thumbnail=true"}); + this.setState({ preview: url+"&thumbnail=true" }); }); } } } - onRename(newFilename){ - if(typeof newFilename === "string"){ + onRename(newFilename) { + if (typeof newFilename === "string") { this.props.emit( - 'file.rename', + "file.rename", pathBuilder(this.props.path, this.props.file.name, this.props.file.type), pathBuilder(this.props.path, newFilename, this.props.file.type), - this.props.file.type + this.props.file.type, ); } - this.setState({is_renaming: false}); + this.setState({ is_renaming: false }); } - onRenameRequest(force){ + onRenameRequest(force) { let new_state = !this.state.is_renaming; - if(typeof force === "boolean"){ + if (typeof force === "boolean") { new_state = force; } - this.setState({is_renaming: new_state}); + this.setState({ is_renaming: new_state }); } - onDeleteRequest(filename){ + onDeleteRequest(filename) { prompt.now( t("Confirm by typing") +" \""+this._confirm_delete_text()+"\"", (answer) => { // click on ok - if(answer === this._confirm_delete_text()){ - this.setState({icon: 'loading'}); + if (answer === this._confirm_delete_text()) { + this.setState({ icon: "loading" }); this.props.emit( - 'file.delete', + "file.delete", pathBuilder(this.props.path, this.props.file.name, this.props.file.type), - this.props.file.type + this.props.file.type, ); return Promise.resolve(); - }else{ + } else { return Promise.reject(t("Doesn't match")); } }, - () => { /* click on cancel */ }); + () => {/* click on cancel */}, + ); } - onDeleteConfirm(answer){ - if(answer === this._confirm_delete_text()){ - this.setState({icon: 'loading', delete_request: false}); + onDeleteConfirm(answer) { + if (answer === this._confirm_delete_text()) { + this.setState({ icon: "loading", delete_request: false }); this.props.emit( - 'file.delete', + "file.delete", pathBuilder(this.props.path, this.props.file.name, this.props.file.type), - this.props.file.type + this.props.file.type, ); - }else{ - this.setState({delete_error: t("Doesn't match")}); + } else { + this.setState({ delete_error: t("Doesn't match") }); } } - onDeleteCancel(){ - this.setState({delete_request: false}); + onDeleteCancel() { + this.setState({ delete_request: false }); } - onShareRequest(filename){ + onShareRequest(filename) { alert.now( , - (ok) => {} + (ok) => {}, ); } - onThingClick(e){ - if(e.ctrlKey === true){ + onThingClick(e) { + if (e.ctrlKey === true) { e.preventDefault(); this.props.emit( "file.select", - pathBuilder(this.props.path, this.props.file.name, this.props.file.type) + pathBuilder(this.props.path, this.props.file.name, this.props.file.type), ); } } - _confirm_delete_text(){ - return this.props.file.name.length > 16? this.props.file.name.substring(0, 10).toLowerCase() : this.props.file.name; + _confirm_delete_text() { + return this.props.file.name.length > 16? + this.props.file.name.substring(0, 10).toLowerCase() : + this.props.file.name; } - render(highlight){ + render(highlight) { const { connectDragSource, connectDropFile, connectDropNativeFile } = this.props; let className = ""; - if(this.props.isDragging) { + if (this.props.isDragging) { className += "is-dragging "; } - 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)) { className += "file-is-hover "; } - if(this.state.is_renaming){ + if (this.state.is_renaming) { className += "highlight "; } - if(this.props.file.icon === 'loading'){ + if (this.props.file.icon === "loading") { className += "loading "; } - if(this.state.preview){ + if (this.state.preview) { className += "preview "; } className = className.trim(); - let fileLink = this.props.file.link + const fileLink = this.props.file.link .replace(/%2F/g, "/") .replace(/\%/g, "%2525") // Hack to get the Link Component to work .replace(/\?/g, "%3F") @@ -249,94 +279,136 @@ export class ExistingThing extends React.Component { return connectDragSource(connectDropNativeFile(connectDropFile(
- - - - + + + - - -
-
-
-
+ onRenameCancel={this.onRenameRequest.bind(this, false)} /> + + +
+ + +
, ))); } } -export default function ToggleableLink(props) { +export const ExistingThing = createSelectable( + EventEmitter( + HOCDropTargetForFsFile( + HOVDropTargetForVirtualFile( + HOVDropSourceForVirtualFile( + ExistingThingComponent, + ), + ), + ), + ), +); + +export default function ToggleableLink(props) { const { disabled, ...rest } = props; - return disabled ? props.children : {props.children}; + return disabled ? + props.children : + {props.children}; } class Filename extends React.Component { - constructor(props){ + constructor(props) { super(props); this.state = { - filename: props.filename + filename: props.filename, }; } - - onInputFocus(e){ + onInputFocus(e) { let value = e.target.value.split("."); - if(value.length > 1){ + if (value.length > 1) { value.pop(); } value = value.join("."); e.target.setSelectionRange(0, value.length); } - onRename(e){ + onRename(e) { e.preventDefault(); e.stopPropagation(); this.props.onRename(this.state.filename); } - onCancel(){ - this.setState({filename: this.props.filename}); + onCancel() { + this.setState({ filename: this.props.filename }); this.props.onRenameCancel(); } - preventSelect(e){ + preventSelect(e) { e.preventDefault(); } - render(){ - const [fileWithoutExtension, fileExtension] = function(filename){ + render() { + const [fileWithoutExtension, fileExtension] = function(filename) { const fname = filename.split("."); - if(fname.length < 2){ + if (fname.length < 2) { return [filename, ""]; } const ext = fname.pop(); - if(window.CONFIG.mime[ext] === undefined){ + if (window.CONFIG.mime[ext] === undefined) { return [filename, ""]; } return [fname.join("."), "." + ext]; }(this.state.filename); + return ( - - - { fileWithoutExtension }{ this.props.hide_extension ? null : {fileExtension} } - - - -
- this.setState({filename: e.target.value})} onBlur={this.onCancel.bind(this)} onFocus={this.onInputFocus.bind(this)} autoFocus /> -
-
-
+ + + { + fileWithoutExtension + }{ + this.props.hide_extension ? null : + {fileExtension} + } + + + +
+ this.setState({ filename: e.target.value })} + onBlur={this.onCancel.bind(this)} + onFocus={this.onInputFocus.bind(this)} + autoFocus /> +
+
+
); } @@ -360,145 +432,158 @@ const ActionButton = (props) => { return (
- - - - - - - - - + + + + + + + + +
); -} +}; const DateTime = (props) => { - function displayTime(timestamp){ - if(timestamp){ - let t = new Date(timestamp); + function displayTime(timestamp) { + if (timestamp) { + const t = new Date(timestamp); return t.getFullYear() + "-" + leftPad((t.getMonth() + 1).toString(), 2) + "-" + leftPad(t.getDate().toString(), 2); - }else{ - return ''; + } else { + return ""; } } - if(props.show === false){ + if (props.show === false) { return null; } return ( - {displayTime(props.timestamp)} + {displayTime(props.timestamp)} ); }; const FileSize = (props) => { - function displaySize(bytes){ - if(bytes === -1) return ""; - if(Number.isNaN(bytes) || bytes === undefined){ + function displaySize(bytes) { + if (bytes === -1) return ""; + if (Number.isNaN(bytes) || bytes === undefined) { return ""; - }else if(bytes < 1024){ - return "("+bytes+'B'+")"; - }else if(bytes < 1048576){ - return "("+Math.round(bytes/1024*10)/10+'KB'+")"; - }else if(bytes < 1073741824){ - return "("+Math.round(bytes/(1024*1024)*10)/10+'MB'+")"; - }else if(bytes < 1099511627776){ - return "("+Math.round(bytes/(1024*1024*1024)*10)/10+'GB'+")"; - }else{ - return "("+Math.round(bytes/(1024*1024*1024*1024))+'TB'+")"; + } else if (bytes < 1024) { + return "("+bytes+"B)"; + } else if (bytes < 1048576) { + return "("+Math.round(bytes/1024*10)/10+"KB)"; + } else if (bytes < 1073741824) { + return "("+Math.round(bytes/(1024*1024)*10)/10+"MB)"; + } else if (bytes < 1099511627776) { + return "("+Math.round(bytes/(1024*1024*1024)*10)/10+"GB)"; + } else { + return "("+Math.round(bytes/(1024*1024*1024*1024))+"TB)"; } } return ( - - {displaySize(props.size)} + + {displaySize(props.size)} ); }; -const Message = (props) => { - return ( - - - {props.message} - - ); -}; - - -class Image extends React.Component{ - constructor(props){ +class Image extends React.Component { + constructor(props) { super(props); } - render(){ - if(this.props.preview && this.props.view === "grid"){ + render() { + if (this.props.preview && this.props.view === "grid") { return ( -
- +
+
); } const ext = path.extname(this.props.path).replace(/^\./, ""); - const img = this.props.icon === "file" ? "file" : "folder"; return ( - - - {ext} - + + + {ext} + ); } }; - class LazyLoadImage extends React.Component { - constructor(props){ + constructor(props) { super(props); this.state = { appear: false, - error: false + error: false, }; this.$scroll = document.querySelector(props.scroller); this.onScroll = debounce(this.onScroll.bind(this), 250); + this.$el = createRef(); } - componentDidMount(){ - if(!this.$scroll){ throw("No scroll detected on LazyLoadImage"); } - this.$scroll.addEventListener("scroll", this.onScroll, {passive: true}); + componentDidMount() { + if (!this.$scroll) throw new Error("No scroll detected on LazyLoadImage"); + this.$scroll.addEventListener("scroll", this.onScroll, { passive: true }); this.onScroll(); } - componentWillUnmount(){ + componentWillUnmount() { this.$scroll.removeEventListener("scroll", this.onScroll); } - onScroll(){ - if(!this.refs.$el) return this.componentWillUnmount(); - const dim_el = this.refs.$el.getBoundingClientRect(); - if(dim_el.top + dim_el.height > 0 && dim_el.top < window.innerHeight){ + onScroll() { + if (!this.$el.current) return this.componentWillUnmount(); + const dim_el = this.$el.current.getBoundingClientRect(); + if (dim_el.top + dim_el.height > 0 && dim_el.top < window.innerHeight) { this.componentWillUnmount(); memory.set(this.props.src, true); - this.setState({appear: true}); + this.setState({ appear: true }); } } - onError(){ - this.setState({error: true}); + onError() { + this.setState({ error: true }); } - render(){ - if((this.props.preview || memory.get(this.props.src) === null) || this.state.error === true){ + render() { + if ((this.props.preview || memory.get(this.props.src) === null) || this.state.error === true) { return ( - + ); } return ( - + ); } } diff --git a/client/pages/filespage/thing-new.js b/client/pages/filespage/thing-new.js index a87920bf..6baf90c9 100644 --- a/client/pages/filespage/thing-new.js +++ b/client/pages/filespage/thing-new.js @@ -1,27 +1,25 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React from "react"; +import PropTypes from "prop-types"; -import { Card, NgIf, Icon, EventEmitter, EventReceiver } from '../../components/'; -import { pathBuilder } from '../../helpers/'; +import { Card, NgIf, Icon, EventEmitter, EventReceiver } from "../../components/"; +import { pathBuilder } from "../../helpers/"; import "./thing.scss"; -@EventEmitter -@EventReceiver -export class NewThing extends React.Component { - constructor(props){ +class NewThingComponent extends React.Component { + constructor(props) { super(props); this.state = { name: null, type: null, - icon: null + icon: null, }; this._onEscapeKeyPress = (e) => { - if(e.keyCode === 27) this.onDelete(); + if (e.keyCode === 27) this.onDelete(); }; } - componentDidMount(){ + componentDidMount() { window.addEventListener("keydown", this._onEscapeKeyPress); this.props.subscribe("new::file", () => { this.onNew("file"); @@ -30,58 +28,78 @@ export class NewThing extends React.Component { this.onNew("directory"); }); } - componentWillUnmount(){ + componentWillUnmount() { window.removeEventListener("keydown", this._onEscapeKeyPress); this.props.unsubscribe("new::file"); this.props.unsubscribe("new::directory"); } - onNew(type){ - if(this.state.type === type){ + onNew(type) { + if (this.state.type === type) { this.onDelete(); - }else{ - this.setState({type: type, name: "", icon: type}); + } else { + this.setState({ + type: type, + name: "", + icon: type, + }); } } - onDelete(){ - this.setState({type: null, name: null, icon: null}); + onDelete() { + this.setState({ + type: null, + name: null, + icon: null, + }); } - onSave(e){ + 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); + if (this.state.name !== null) { + this.props.emit( + "file.create", + pathBuilder(this.props.path, this.state.name, this.state.type), + this.state.type, + ); this.onDelete(); } } - render(){ + render() { return (
- - - - -
- this.setState({name: e.target.value})} value={this.state.name} type="text" autoFocus/> -
-
- -
-
- -
-
-
-
-
+ + + + +
+ this.setState({ name: e.target.value })} + value={this.state.name} + type="text" autoFocus /> +
+
+ +
+
+ +
+
+
+
+
); }; } -NewThing.propTypes = { +NewThingComponent.propTypes = { accessRight: PropTypes.object.isRequired, - sort: PropTypes.string.isRequired + sort: PropTypes.string.isRequired, }; + +export const NewThing = EventEmitter(EventReceiver(NewThingComponent)); diff --git a/client/pages/homepage.js b/client/pages/homepage.js index 3260f0c7..b248eb47 100644 --- a/client/pages/homepage.js +++ b/client/pages/homepage.js @@ -10,7 +10,7 @@ export function HomePageComponent({ error }) { useEffect(() => { const p = new URLSearchParams(location.search); - if(p.get("error")) { + if (p.get("error")) { error(new Error(t(p.get("error")))); return; } diff --git a/client/pages/sharepage.js b/client/pages/sharepage.js index e909505f..d420e651 100644 --- a/client/pages/sharepage.js +++ b/client/pages/sharepage.js @@ -1,16 +1,15 @@ -import React from 'react'; -import { Redirect } from 'react-router'; +import React, { createRef } from "react"; +import { Redirect } from "react-router"; -import { Share } from '../model/'; -import { notify, basename, filetype, findParams } from '../helpers/'; -import { Loader, Input, Button, Container, ErrorPage, Icon, NgIf } from '../components/'; -import { t } from '../locales/'; -import './error.scss'; -import './sharepage.scss'; +import { Share } from "../model/"; +import { notify, basename, filetype, findParams } from "../helpers/"; +import { Loader, Input, Button, Container, ErrorPage, Icon } from "../components/"; +import { t } from "../locales/"; +import "./error.scss"; +import "./sharepage.scss"; -@ErrorPage -export class SharePage extends React.Component { - constructor(props){ +export class SharePageComponent extends React.Component { + constructor(props) { super(props); this.state = { path: null, @@ -19,40 +18,41 @@ export class SharePage extends React.Component { loading: false, isUploader: false, }; + this.$input = createRef(); } - componentDidMount(){ + componentDidMount() { this._proofQuery(this.props.match.params.id).then(() => { - if(this.refs.$input) { - this.refs.$input.ref.focus(); + if (this.$input.current) { + this.$input.current.ref.focus(); } }); } - submitProof(e, type, value){ + submitProof(e, type, value) { e.preventDefault(); - this.setState({loading: true}); - this._proofQuery(this.props.match.params.id, {type: type, value:value}); + this.setState({ loading: true }); + this._proofQuery(this.props.match.params.id, { type: type, value: value }); } - _proofQuery(id, data = {}){ - this.setState({loading: true}); + _proofQuery(id, data = {}) { + this.setState({ loading: true }); return Share.proof(id, data).then((res) => { - if(this.refs.$input) { - this.refs.$input.ref.value = ""; + if (this.$input.current) { + this.$input.current.ref.value = ""; } - let st = { + const st = { key: res.key, path: res.path || null, share: res.id, loading: false, isUploader: res.can_read === false && res.can_write === false && res.can_upload === true, }; - if(res.message){ + if (res.message) { notify.send(res.message, "info"); - }else if(res.error){ + } else if (res.error) { st.error = res.error; - window.setTimeout(() => this.setState({error: null}), 500); + window.setTimeout(() => this.setState({ error: null }), 500); } return new Promise((done) => { this.setState(st, () => done()); @@ -63,22 +63,22 @@ export class SharePage extends React.Component { render() { const marginTop = () => { return { - marginTop: parseInt(window.innerHeight / 3)+'px' + marginTop: `${parseInt(window.innerHeight / 3)}px`, }; }; - let className = this.state.error ? "error rand-"+Math.random().toString() : ""; + const className = this.state.error ? `error rand-${Math.random().toString()}` : ""; - if(this.state.path !== null){ - if(!!findParams("next")){ + if (this.state.path !== null) { + if (!!findParams("next")) { const url = findParams("next"); - if(url[0] === "/"){ + if (url[0] === "/") { requestAnimationFrame(() => { window.location.pathname = url; }); return (
- +
); } @@ -92,57 +92,65 @@ export class SharePage extends React.Component { ); - } else if(filetype(this.state.path) === "directory"){ + } else if (filetype(this.state.path) === "directory") { return ( ); - } else{ + } else { return ( ); } - } else if (this.state.key === null){ + } else if (this.state.key === null) { return (
- +
); - } else if(this.state.key === "code"){ + } else if (this.state.key === "code") { return ( -
this.submitProof(e, "code", this.refs.$input.ref.value)} style={marginTop()}> - - -
+
this.submitProof(e, "code", this.$input.current.ref.value)} + style={marginTop()}> + + +
); - } else if(this.state.key === "password"){ + } else if (this.state.key === "password") { return ( -
this.submitProof(e, "password", this.refs.$input.ref.value)} style={marginTop()}> - - -
+
this.submitProof(e, "password", this.$input.current.ref.value)} + style={marginTop()}> + + +
); - }else if(this.state.key === "email"){ + } else if (this.state.key === "email") { return ( -
this.submitProof(e, "email", this.refs.$input.ref.value)} style={marginTop()}> - - -
+
this.submitProof(e, "email", this.$input.current.ref.value)} style={marginTop()}> + + +
); } return (
-

{ t("Oops!") }

-

{ t("There is nothing in here") }

+

{ t("Oops!") }

+

{ t("There is nothing in here") }

); } } + +export const SharePage = ErrorPage(SharePageComponent); diff --git a/client/pages/viewerpage.js b/client/pages/viewerpage.js index da7b495c..b06c7781 100644 --- a/client/pages/viewerpage.js +++ b/client/pages/viewerpage.js @@ -1,194 +1,213 @@ -import React from 'react'; -import Path from 'path'; +import React from "react"; +import Path from "path"; -import './viewerpage.scss'; -import './error.scss'; -import { Files } from '../model/'; -import { BreadCrumb, Bundle, NgIf, Loader, Container, EventReceiver, EventEmitter, LoggedInOnly , ErrorPage } from '../components/'; -import { debounce, opener, notify } from '../helpers/'; -import { FileDownloader, ImageViewer, PDFViewer, FormViewer } from './viewerpage/'; +import "./viewerpage.scss"; +import "./error.scss"; +import { Files } from "../model/"; +import { + BreadCrumb, Bundle, NgIf, Loader, EventReceiver, LoggedInOnly, ErrorPage, +} from "../components/"; +import { opener, notify } from "../helpers/"; +import { FileDownloader, ImageViewer, PDFViewer, FormViewer } from "./viewerpage/"; const VideoPlayer = (props) => ( - - {(Comp) => } + + {(Comp) => } ); const IDE = (props) => ( - - {(Comp) => } + + {(Comp) => } ); const AudioPlayer = (props) => ( - - {(Comp) => } + + {(Comp) => } ); const Appframe = (props) => ( - - {(Comp) => } + + {(Comp) => } ); -@ErrorPage -@LoggedInOnly -@EventReceiver -export class ViewerPage extends React.Component { - constructor(props){ +export class ViewerPageComponent extends React.Component { + constructor(props) { super(props); this.state = { - path: props.match.url.replace('/view', '').replace(/%23/g, "#") + (location.hash || ""), + path: props.match.url.replace("/view", "").replace(/%23/g, "#") + (location.hash || ""), url: null, - filename: Path.basename(props.match.url.replace('/view', '')) || 'untitled.dat', + filename: Path.basename(props.match.url.replace("/view", "")) || "untitled.dat", opener: null, content: null, needSaving: false, isSaving: false, loading: true, - application_arguments: null + application_arguments: null, }; - this.props.subscribe('file.select', this.onPathUpdate.bind(this)); + this.props.subscribe("file.select", this.onPathUpdate.bind(this)); } - componentWillReceiveProps(props){ + UNSAFE_componentWillReceiveProps(props) { this.setState({ - path: props.match.url.replace('/view', '').replace(/%23/g, "#") + (location.hash || ""), - filename: Path.basename(props.match.url.replace('/view', '')) || 'untitled.dat' - }, () => { this.componentDidMount(); }); + path: props.match.url.replace("/view", "").replace(/%23/g, "#") + (location.hash || ""), + filename: Path.basename(props.match.url.replace("/view", "")) || "untitled.dat", + }, () => this.componentDidMount()); } - componentDidMount(){ + componentDidMount() { const metadata = () => { return new Promise((done, err) => { - let [app_opener, app_args] = opener(this.state.path); + const [app_opener, app_args] = opener(this.state.path); Files.url(this.state.path).then((url) => { this.setState({ url: url, opener: app_opener, - application_arguments: app_args + application_arguments: app_args, }, () => done(app_opener)); - }).catch(error => { + }).catch((error) => { this.props.error(error); err(error); }); }); }; const data_fetch = (app) => { - if(app === "editor" || app === "form"){ + if (app === "editor" || app === "form") { return Promise.all([ Files.cat(this.state.path), - Files.options(this.state.path) + Files.options(this.state.path), ]).then((d) => { const [content, options] = d; this.setState({ content: content, loading: false, - acl: options["allow"] + acl: options["allow"], }); }).catch((err) => { - if(err && err.code === 'BINARY_FILE'){ - this.setState({opener: 'download', loading: false}); - }else{ + if (err && err.code === "BINARY_FILE") { + this.setState({ opener: "download", loading: false }); + } else { this.props.error(err); } }); } - this.setState({loading: false}); + this.setState({ loading: false }); }; return metadata().then(data_fetch); } componentWillUnmount() { - this.props.unsubscribe('file.select'); + this.props.unsubscribe("file.select"); } - save(file){ - this.setState({isSaving: true}); + save(file) { + this.setState({ isSaving: true }); return Files.save(this.state.path, file) .then(() => { - this.setState({isSaving: false, needSaving: false}); + this.setState({ isSaving: false, needSaving: false }); return Promise.resolve(); }) .then(() => { return new Promise((done, err) => { const reader = new FileReader(); reader.onload = () => { - this.setState({content: reader.result}); + this.setState({ content: reader.result }); done(); }; reader.onerror = (e) => { - err({message: 'Internal error 500'}); + err({ message: "Internal error 500" }); }; reader.readAsText(file); }); }) .catch((err) => { - if(err && err.code === 'CANCELLED'){ return; } - this.setState({isSaving: false}); - notify.send(err, 'error'); + if (err && err.code === "CANCELLED") return; + this.setState({ isSaving: false }); + notify.send(err, "error"); return Promise.reject(err); }); } - onPathUpdate(path){ - this.props.history.push('/files'+path); + onPathUpdate(path) { + this.props.history.push("/files"+path); } - needSaving(bool){ + needSaving(bool) { return new Promise((done) => { - this.setState({needSaving: bool}, done); + this.setState({ needSaving: bool }, done); }); } render() { return (
- -
- - - - - - - - - - - - - - - +
+ + + + + + + + + + + + + + + - - - - - - - - - - - - - - -
+
+ + + + + + + + + +
+ + + +
); } } + +export const ViewerPage = ErrorPage(LoggedInOnly(EventReceiver(ViewerPageComponent))); diff --git a/client/pages/viewerpage/editor.js b/client/pages/viewerpage/editor.js index 25a82a85..66d748a8 100644 --- a/client/pages/viewerpage/editor.js +++ b/client/pages/viewerpage/editor.js @@ -1,4 +1,3 @@ -/* eslint-disable no-invalid-this */ import React from "react"; import PropTypes from "prop-types"; import { withRouter } from "react-router"; diff --git a/client/pages/viewerpage/editor/orgmode.js b/client/pages/viewerpage/editor/orgmode.js index dd02c503..e5d818fd 100644 --- a/client/pages/viewerpage/editor/orgmode.js +++ b/client/pages/viewerpage/editor/orgmode.js @@ -1,4 +1,3 @@ -/* eslint-disable max-len, no-invalid-this */ import "codemirror/addon/mode/simple"; import { org_cycle, org_shifttab, org_metaleft, org_metaright, org_meta_return, org_metaup, diff --git a/client/pages/viewerpage/ide.js b/client/pages/viewerpage/ide.js index 4ab16d9f..19c11046 100644 --- a/client/pages/viewerpage/ide.js +++ b/client/pages/viewerpage/ide.js @@ -1,21 +1,22 @@ -import React from 'react'; -import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; -import { withRouter } from 'react-router'; -import { Prompt } from "react-router-dom"; -import { Subject } from 'rxjs/Subject'; +/* eslint-disable react/jsx-no-target-blank */ +import React from "react"; +import ReactCSSTransitionGroup from "react-addons-css-transition-group"; +import { withRouter } from "react-router"; +import { Subject } from "rxjs/Subject"; -import { NgIf, Fab, Icon, Dropdown, DropdownButton, DropdownList, DropdownItem } from '../../components/'; -import { confirm, currentShare } from '../../helpers/'; -import { Editor } from './editor'; -import { MenuBar } from './menubar'; -import { OrgTodosViewer, OrgEventsViewer } from './org_viewer'; -import { t } from '../../locales/'; +import { + NgIf, Fab, Icon, Dropdown, DropdownButton, DropdownList, DropdownItem, +} from "../../components/"; +import { confirm, currentShare } from "../../helpers/"; +import { Editor } from "./editor"; +import { MenuBar } from "./menubar"; +import { OrgTodosViewer, OrgEventsViewer } from "./org_viewer"; +import { t } from "../../locales/"; -import './ide.scss'; +import "./ide.scss"; -@withRouter -export class IDE extends React.Component { - constructor(props){ +class IDEComponent extends React.Component { + constructor(props) { super(props); this.state = { event: new Subject(), @@ -23,16 +24,16 @@ export class IDE extends React.Component { appear_agenda: false, appear_todo: false, mode: null, - folding: null + folding: null, }; } - componentDidMount(){ - this.unblock = this.props.history.block((nextLocation)=>{ - if(this.props.needSaving === false) return true; + componentDidMount() { + this.unblock = this.props.history.block((nextLocation)=> { + if (this.props.needSaving === false) return true; confirm.now( -
- { t("Do you want to save the changes ?") } +
+ { t("Do you want to save the changes ?") }
, () =>{ return this.save() @@ -41,135 +42,167 @@ export class IDE extends React.Component { () => { this.props.needSavingUpdate(false) .then(() => this.props.history.push(nextLocation)); - } + }, ); return false; }); } - componentWillUnmount(){ + componentWillUnmount() { this.unblock(); window.clearInterval(this.state.id); } - save(){ - if(this.props.needSaving === false) return; + save() { + if (this.props.needSaving === false) return; // the ipad is the new IE, they don't support the file object so we got to fallback :( - let blob = new window.Blob([this.state.contentToSave], {type : 'text/plain'}); - return this.props.onSave(blob).then(() => this.props.needSavingUpdate(false)); + return this.props.onSave( + new window.Blob( + [this.state.contentToSave], + { type: "text/plain" }, + ), + ).then(() => this.props.needSavingUpdate(false)); } - onUpdate(property, refresh, value){ + onUpdate(property, refresh, value) { this.setState({ [property]: value }, () => { - if(refresh){ + if (refresh) { this.state.event.next(["refresh"]); } - if(this.props.content === this.state.contentToSave){ + if (this.props.content === this.state.contentToSave) { this.props.needSavingUpdate(false); - }else{ + } else { this.props.needSavingUpdate(true); } }); } /* Org Viewer specific stuff */ - toggleAgenda(force = null){ - this.setState({appear_agenda: force === null ? !this.state.appear_agenda : !!force}); + toggleAgenda(force = null) { + this.setState({ + appear_agenda: force === null ? !this.state.appear_agenda : !!force, + }); } - toggleTodo(force = null){ - this.setState({appear_todo: force === null ? !this.state.appear_todo : !!force}); + toggleTodo(force = null) { + this.setState({ + appear_todo: force === null ? !this.state.appear_todo : !!force, + }); } - onModeChange(){ + onModeChange() { this.state.event.next(["fold"]); } - goTo(lineNumber){ + goTo(lineNumber) { this.state.event.next(["goTo", lineNumber]); } - download(){ + download() { document.cookie = "download=yes; path=/; max-age=120;"; - this.setState({random: Math.random()}); - this.state.id = window.setInterval(() => { - if(/download=yes/.test(document.cookie) === false){ - window.clearInterval(this.state.id); - this.setState({random: Math.random()}); - } - }, 100); + this.setState({ + random: Math.random(), + id: window.setInterval(() => { + if (/download=yes/.test(document.cookie) === false) { + window.clearInterval(this.state.id); + this.setState({ random: Math.random() }); + } + }, 100), + }); } - render(){ - const changeExt = function(filename, ext){ + render() { + const changeExt = function(filename, ext) { return filename.replace(/\.org$/, "."+ext); }; return (
- - - - - - - - - - - - - - this.download()} enable={/download=yes/.test(document.cookie) ? false : true}> - - - - - { t("Save current file") } - { t("Export as {{VALUE}}", "HTML") } - { t("Export as {{VALUE}}", "PDF") } - { t("Export as {{VALUE}}", "Markdown") } - { t("Export as {{VALUE}}", "TXT") } - { t("Export as {{VALUE}}", "Latex") } - { t("Export as {{VALUE}}", "ical") } - { t("Export as {{VALUE}}", "Open office") } - { t("Export as {{VALUE}}", "Beamer") } - - + + + + + + + + + + + + + + this.download()} + enable={/download=yes/.test(document.cookie) ? false : true}> + + + + + { t("Save current file") } + { t("Export as {{VALUE}}", "HTML") } + { t("Export as {{VALUE}}", "PDF") } + { t("Export as {{VALUE}}", "Markdown") } + { t("Export as {{VALUE}}", "TXT") } + { t("Export as {{VALUE}}", "Latex") } + { t("Export as {{VALUE}}", "ical") } + { t("Export as {{VALUE}}", "Open office") } + { t("Export as {{VALUE}}", "Beamer") } + + - - - - - - - - + + + + + + + + - - + - + onModeChange={this.onUpdate.bind(this, "mode", false)} + onFoldChange={this.onUpdate.bind(this, "folding", false)} + onChange={this.onUpdate.bind(this, "contentToSave", false)} /> + - - - - - - - - - - + + + + + + + + + + + + + + - - + +
); } } + +export const IDE = withRouter(IDEComponent); diff --git a/client/pages/viewerpage/image_exif.js b/client/pages/viewerpage/image_exif.js index 74ddb0b7..226308b2 100644 --- a/client/pages/viewerpage/image_exif.js +++ b/client/pages/viewerpage/image_exif.js @@ -1,4 +1,3 @@ -/* eslint-disable max-len, no-invalid-this */ import React from "react"; import EXIF from "exif-js"; import ReactCSSTransitionGroup from "react-addons-css-transition-group"; @@ -209,7 +208,7 @@ class LargeExifClass extends Exif { componentDidMount() { this.refresh_handler(this.props); } - componentWillReceiveProps(props) { + UNSAFE_componentWillReceiveProps(props) { this.refresh_handler(props); } diff --git a/client/pages/viewerpage/imageviewer.js b/client/pages/viewerpage/imageviewer.js index 073c1ac0..500ead13 100644 --- a/client/pages/viewerpage/imageviewer.js +++ b/client/pages/viewerpage/imageviewer.js @@ -1,31 +1,29 @@ -import React from 'react'; -import path from 'path'; -import ReactCSSTransitionGroup from 'react-addons-css-transition-group'; +import React, { createRef } from "react"; +import path from "path"; +import ReactCSSTransitionGroup from "react-addons-css-transition-group"; -import { MenuBar } from './menubar'; -import { Bundle, Icon, NgIf, Loader, EventEmitter, EventReceiver } from '../../components/'; -import { alert } from '../../helpers/'; -import { Pager } from './pager'; -import { t } from '../../locales/'; -import './imageviewer.scss'; -import './pager.scss'; +import { MenuBar } from "./menubar"; +import { Bundle, Icon, NgIf, Loader, EventEmitter, EventReceiver } from "../../components/"; +import { alert } from "../../helpers/"; +import { Pager } from "./pager"; +import { t } from "../../locales/"; +import "./imageviewer.scss"; +import "./pager.scss"; const SmallExif = (props) => ( - {(Comp) => } + {(Comp) => } ); const LargeExif = (props) => ( - {(Comp) => } + {(Comp) => } ); -@EventReceiver -@EventEmitter -export class ImageViewer extends React.Component{ - constructor(props){ +export class ImageViewerComponent extends React.Component { + constructor(props) { super(props); this.state = { preload: null, @@ -35,55 +33,56 @@ export class ImageViewer extends React.Component{ draggable: true, }; this.shortcut= (e) => { - if(e.keyCode === 27){ this.setState({show_exif: false}); } - else if(e.keyCode === 73){ this.setState({show_exif: !this.state.show_exif}); } + if (e.keyCode === 27) this.setState({ show_exif: false }); + else if (e.keyCode === 73) this.setState({ show_exif: !this.state.show_exif }); }; - this.refresh = () => { this.setState({"_": Math.random()}); }; + this.refresh = () => this.setState({ "_": Math.random() }); + this.$container = createRef(); } - componentDidMount(){ + componentDidMount() { this.props.subscribe("media::preload", (preload) => { - this.setState({preload: preload}); + this.setState({ preload: preload }); }); - document.addEventListener('webkitfullscreenchange', this.refresh); - document.addEventListener('mozfullscreenchange', this.refresh); - document.addEventListener('fullscreenchange', this.refresh); - document.addEventListener('keydown', this.shortcut); + document.addEventListener("webkitfullscreenchange", this.refresh); + document.addEventListener("mozfullscreenchange", this.refresh); + document.addEventListener("fullscreenchange", this.refresh); + document.addEventListener("keydown", this.shortcut); } - componentWillUnmount(){ + componentWillUnmount() { this.props.unsubscribe("media::preload"); - document.removeEventListener('webkitfullscreenchange', this.refresh); - document.removeEventListener('mozfullscreenchange', this.refresh); - document.removeEventListener('fullscreenchange', this.refresh); - document.removeEventListener('keydown', this.shortcut); + document.removeEventListener("webkitfullscreenchange", this.refresh); + document.removeEventListener("mozfullscreenchange", this.refresh); + document.removeEventListener("fullscreenchange", this.refresh); + document.removeEventListener("keydown", this.shortcut); } - componentWillReceiveProps(props){ - if(props.data !== this.props.data){ - this.setState({is_loaded: false}); + UNSAFE_componentWillReceiveProps(props) { + if (props.data !== this.props.data) { + this.setState({ is_loaded: false }); } } - toggleExif(){ - if(window.innerWidth < 580){ + toggleExif() { + if (window.innerWidth < 580) { alert.now(); - }else{ + } else { this.setState({ - show_exif: !this.state.show_exif + show_exif: !this.state.show_exif, }); } } - requestFullScreen(){ - if("webkitRequestFullscreen" in document.body){ - this.refs.$container.webkitRequestFullscreen(); - }else if("mozRequestFullScreen" in document.body){ - this.refs.$container.mozRequestFullScreen(); + requestFullScreen() { + if ("webkitRequestFullscreen" in document.body) { + this.$container.current.webkitRequestFullscreen(); + } else if ("mozRequestFullScreen" in document.body) { + this.$container.current.mozRequestFullScreen(); } } - render(){ + render() { const hasExif = (filename) => { const ext = path.extname(filename).toLowerCase().substring(1); return ["jpg", "jpeg", "tiff", "tif"].indexOf(ext) !== -1; @@ -91,200 +90,223 @@ export class ImageViewer extends React.Component{ return (
- - - - - - - - -
-
- this.setState({is_loaded: true})} url={this.props.data} /> -
-
-
-
{ t("Info") }
-
- + + + + + + + + +
+
+ this.setState({ is_loaded: true })} + url={this.props.data} />
-
-
- -
+
+
+
{ t("Info") }
+
+ +
+
+
+ +
+
+ + this.setState({ draggable: files.length > 1 ? true : false })} + next={(e) => this.setState({ preload: e })} />
- this.setState({draggable: files.length > 1 ? true : false})} - next={(e) => this.setState({preload: e})} /> -
- - - + + +
); } } -@EventEmitter -class ImageFancy extends React.Component { - constructor(props){ +export const ImageViewer = EventReceiver(EventEmitter(ImageViewerComponent)); + +class ImageFancyComponent extends React.Component { + constructor(props) { super(props); this.state = { isLoading: true, isError: false, - drag_init: {x: null, t: null}, - drag_current: {x: null, t: null}, - hasAction: false + drag_init: { x: null, t: null }, + drag_current: { x: null, t: null }, + hasAction: false, }; - this.img = new Image(); - this.img.src = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='; + this.img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="; } - componentWillReceiveProps(nextProp){ - if(nextProp.url !== this.props.url){ + UNSAFE_componentWillReceiveProps(nextProp) { + if (nextProp.url !== this.props.url) { this.setState({ isLoading: true, isError: false, - drag_current: {x: 0}, - hasAction: false + drag_current: { x: 0 }, + hasAction: false, }); } } - onLoad(){ - this.setState({isLoading: false}); + onLoad() { + this.setState({ isLoading: false }); this.props.onLoad(); } - onError(w){ - this.setState({isError: true}); + onError(w) { + this.setState({ isError: true }); } - imageDragStart(e){ + imageDragStart(e) { const t = new Date(); - if(e.touches){ + if (e.touches) { this.setState({ - drag_init: {x: e.touches[0].clientX, t: t}, - hasAction: true + drag_init: { x: e.touches[0].clientX, t: t }, + hasAction: true, }); - }else{ + } else { this.setState({ - drag_init: {x: e.pageX, t: t}, - hasAction: true + drag_init: { x: e.pageX, t: t }, + hasAction: true, }); } - if(e.dataTransfer) e.dataTransfer.setDragImage(this.img, 0, 0); + if (e.dataTransfer) e.dataTransfer.setDragImage(this.img, 0, 0); } - imageDragEnd(e){ + imageDragEnd(e) { const drag_end = { - x: function(dragX, touch){ - const t = new Date(); - if(dragX !== null) return dragX; - if(touch && touch[0]){ + x: function(dragX, touch) { + if (dragX !== null) return dragX; + if (touch && touch[0]) { return touch[0].clientX; } return 0; }(e.pageX || null, e.changedTouches || null), - t: new Date() + t: new Date(), }; - const direction = function(x_current, x_init){ - if(x_current.t - x_init.t > 200){ - if(Math.abs(x_current.x - x_init.x) < (window.innerWidth < 500 ? window.innerWidth / 3 : 250)) return "neutral"; + const direction = function(x_current, x_init) { + if (x_current.t - x_init.t > 200 && + Math.abs(x_current.x - x_init.x) < + (window.innerWidth < 500 ? window.innerWidth / 3 : 250)) { + return "neutral"; } return x_current.x > x_init.x ? "right" : "left"; }(drag_end, this.state.drag_init); - if(direction === "left"){ + if (direction === "left") { return this.setState({ - drag_current: {x: - window.innerWidth}, - hasAction: false + drag_current: { x: - window.innerWidth }, + hasAction: false, }, () => { this.props.emit("media::next"); }); - }else if(direction === "right"){ + } else if (direction === "right") { return this.setState({ - drag_current: {x: + window.innerWidth}, - hasAction: false + drag_current: { x: + window.innerWidth }, + hasAction: false, }, () => { this.props.emit("media::previous"); }); } return this.setState({ - drag_current: {x: 0}, - hasAction: false + drag_current: { x: 0 }, + hasAction: false, }); } - imageDrag(e){ - if(e.pageX > 0){ - this.setState({drag_current: {x: e.pageX - this.state.drag_init.x}}); - }else if(e.touches && e.touches[0].clientX > 0){ - this.setState({drag_current: {x: e.touches[0].clientX - this.state.drag_init.x}}); + imageDrag(e) { + if (e.pageX > 0) { + this.setState({ + drag_current: { x: e.pageX - this.state.drag_init.x }, + }); + } else if (e.touches && e.touches[0].clientX > 0) { + this.setState({ + drag_current: { x: e.touches[0].clientX - this.state.drag_init.x }, + }); } } - render(){ - if(this.state.isError){ + render() { + if (this.state.isError) { return ( -
{ t("Can't load this picture") }
+
+
+ { t("Can't load this picture") } +
+
); } - if(this.state.isLoading){ + if (this.state.isLoading) { return (
- - + +
); } return ( - -
- -
+ +
+ +
); } } -class Img extends React.Component{ - constructor(props){ - super(props); - } +const ImageFancy = EventEmitter(ImageFancyComponent); - render(){ - const image_url = (url, size) => { - return url+"&size="+parseInt(Math.max(window.innerWidth*size, window.innerHeight*size)); - }; - if(!this.props.src) return null; - - return ( - - ); - } +function Img({ src, ...props }) { + const image_url = (url, size) => { + return url+"&size="+parseInt(Math.max(window.innerWidth*size, window.innerHeight*size)); + }; + if (!src) return null; + return ( + + ); } diff --git a/client/pages/viewerpage/org_viewer.js b/client/pages/viewerpage/org_viewer.js index e9972643..8f49c6d6 100644 --- a/client/pages/viewerpage/org_viewer.js +++ b/client/pages/viewerpage/org_viewer.js @@ -1,4 +1,3 @@ -/* eslint-disable max-len */ import React from "react"; import { StickyContainer, Sticky } from "react-sticky"; @@ -10,87 +9,100 @@ import { t } from "../../locales/"; import "./org_viewer.scss"; export class OrgEventsViewer extends React.Component { - shouldComponentUpdate(nextProps){ - if(this.props.content !== nextProps.content) return true; - if(this.props.isActive !== nextProps.isActive) return true; + shouldComponentUpdate(nextProps) { + if (this.props.content !== nextProps.content) return true; + if (this.props.isActive !== nextProps.isActive) return true; return false; } - render(){ + render() { const headlines = this.props.isActive ? extractEvents(this.props.content) : []; return ( - + ); } } export class OrgTodosViewer extends React.Component { - shouldComponentUpdate(nextProps){ - if(this.props.content !== nextProps.content) return true; - if(this.props.isActive !== nextProps.isActive) return true; + shouldComponentUpdate(nextProps) { + if (this.props.content !== nextProps.content) return true; + if (this.props.isActive !== nextProps.isActive) return true; return false; } - render(){ + render() { const headlines = this.props.isActive ? extractTodos(this.props.content) : []; return ( - + ); } } - class OrgViewer extends React.Component { - constructor(props){ + constructor(props) { super(props); this.state = { headlines: this.buildHeadlines(props.headlines), content: props.content, search: "", - _: null + _: null, + }; + this.rerender = () => { + this.setState({ _: Math.random() }); }; - this.rerender = () => {this.setState({_: Math.random()});}; this.findResults = debounce(this.findResults.bind(this), 150); } - componentWillReceiveProps(props){ + UNSAFE_componentWillReceiveProps(props) { this.setState({ headlines: this.buildHeadlines(props.headlines), - content: props.content + content: props.content, }); } - buildHeadlines(headlines){ + buildHeadlines(headlines) { return headlines .reduce((acc, headline) => { - if(!acc[headline["key"]]){ acc[headline["key"]] = []; } + if (!acc[headline["key"]]) { + acc[headline["key"]] = []; + } acc[headline["key"]].push(headline); return acc; }, {}); } - onChange(i, j, state){ - this.state.headlines[Object.keys(this.state.headlines)[i]][j].status = state; + onChange(i, j, state) { + const headlines = { ...this.state.headlines }; + headlines[Object.keys(this.state.headlines)[i]][j].status = state; this.setState({ - headlines: this.state.headlines + headlines: this.state.headlines, }); } - onTaskUpdate(type, line, value){ + onTaskUpdate(type, line, value) { const content = this.state.content.split("\n"); - let head_line, item_line, head_status, deadline_line, scheduled_line, insertion_line; - switch(type){ + let head_line; + let item_line; + let head_status; + let deadline_line; + let scheduled_line; + let insertion_line; + switch (type) { case "status": content[line] = content[line].replace(/^(\*+\s)[A-Z]{3,}(\s.*)$/, "$1"+value+"$2"); break; case "subtask": - if(value === "DONE"){ + if (value === "DONE") { content[line] = content[line].replace(/\[.\]/, "[X]"); - }else{ + } else { content[line] = content[line].replace(/\[.\]/, "[ ]"); } break; @@ -99,139 +111,139 @@ class OrgViewer extends React.Component { content[item_line] = content[item_line].replace(/SCHEDULED: <.*?>\s*/, value ? "SCHEDULED: "+orgdate(value)+" " : ""); this.state.headlines[head_status] = this.state.headlines[head_status] .map((todo) => { - if(todo.line === head_line){ - if(value) todo.scheduled.timestamp = new Date(value).toISOString(); + if (todo.line === head_line) { + if (value) todo.scheduled.timestamp = new Date(value).toISOString(); else todo.scheduled = null; } return todo; }); - this.setState({headlines: this.state.headlines}); + this.setState({ headlines: this.state.headlines }); break; case "existing_deadline": [head_line, head_status, item_line] = line; content[item_line] = content[item_line].replace(/DEADLINE: <.*?>\s*/, value ? "DEADLINE: "+orgdate(value) : ""); this.state.headlines[head_status] = this.state.headlines[head_status] .map((todo) => { - if(todo.line === head_line){ - if(value) todo.deadline.timestamp = new Date(value).toISOString(); + if (todo.line === head_line) { + if (value) todo.deadline.timestamp = new Date(value).toISOString(); else todo.deadline = null; } return todo; }); - this.setState({headlines: this.state.headlines}); + this.setState({ headlines: this.state.headlines }); break; case "new_scheduled": [head_line, head_status, deadline_line] = line; - if(deadline_line !== null){ + if (deadline_line !== null) { insertion_line = deadline_line; content[deadline_line] = "SCHEDULED: "+orgdate(value)+" "+content[deadline_line]; - }else{ + } else { insertion_line = head_line + 1; - if(content[insertion_line] === "" && content[insertion_line + 1] === ""){ + if (content[insertion_line] === "" && content[insertion_line + 1] === "") { content[insertion_line] = "SCHEDULED: "+orgdate(value); - }else{ + } else { content.splice( insertion_line, 0, - "SCHEDULED: "+orgdate(value) + "SCHEDULED: "+orgdate(value), ); } } this.state.headlines[head_status] = this.state.headlines[head_status] .map((todo) => { - if(todo.line === head_line){ + if (todo.line === head_line) { todo.scheduled = { line: insertion_line, keyword: "SCHEDULED", active: true, range: null, repeat: null, - timestamp: new Date(value).toISOString() + timestamp: new Date(value).toISOString(), }; } return todo; }); - this.setState({headlines: this.state.headlines}); + this.setState({ headlines: this.state.headlines }); break; case "new_deadline": [head_line, head_status, scheduled_line] = line; - if(scheduled_line !== null){ + if (scheduled_line !== null) { insertion_line = scheduled_line; content[scheduled_line] = content[scheduled_line]+" DEADLINE: "+orgdate(value); - }else{ + } else { insertion_line = head_line + 1; - if(content[insertion_line] === "" && content[insertion_line + 1] === ""){ + if (content[insertion_line] === "" && content[insertion_line + 1] === "") { content[insertion_line] = "DEADLINE: "+orgdate(value); - }else{ + } else { content.splice( insertion_line, 0, - "DEADLINE: "+orgdate(value) + "DEADLINE: "+orgdate(value), ); } this.state.headlines[head_status] = this.state.headlines[head_status] .map((todo) => { - if(todo.line === head_line){ + if (todo.line === head_line) { todo.deadline = { line: insertion_line, keyword: "DEADLINE", active: true, range: null, repeat: null, - timestamp: new Date(value).toISOString() + timestamp: new Date(value).toISOString(), }; } return todo; }); - this.setState({headlines: this.state.headlines}); + this.setState({ headlines: this.state.headlines }); } break; } - this.setState({content: content.join("\n")}); + this.setState({ content: content.join("\n") }); - function orgdate(_date){ + function orgdate(_date) { const date = new Date(_date); return "<"+date.getFullYear()+"-"+leftPad((date.getMonth() + 1).toString(), 2)+"-"+leftPad(date.getDate().toString(), 2)+" "+day(date.getDay())+">"; - function day(n){ - switch(navigator.language.split("-")[0]){ - case "de": ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"][n]; + function day(n) { + switch (navigator.language.split("-")[0]) { + case "de": ["So", "Mo", "Di", "Mi", "Do", "Fr", "Sa"][n]; break; - default: return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][n]; + default: return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][n]; } } } } - navigate(line){ + navigate(line) { this.props.goTo(line); this.onQuit(); } - onQuit(){ + onQuit() { this.props.onUpdate(this.state.content); this.props.onQuit(); } - componentDidMount(){ + componentDidMount() { window.addEventListener("resize", this.rerender); } - componentWillUnmount(){ + componentWillUnmount() { window.removeEventListener("resize", this.rerender); } - search(terms){ - this.setState({search: terms}, () => { + search(terms) { + this.setState({ search: terms }, () => { this.findResults(terms); }); } - findResults(terms){ + findResults(terms) { let headlines = this.props.headlines; - if(terms){ + if (terms) { headlines = this.props.headlines.filter((headline) => { const keywords = terms.split(" "); - const head = function(){ + const head = function() { let str = " "; str += headline["status"] + " "; str += headline["title"] + " "; @@ -246,75 +258,75 @@ class OrgViewer extends React.Component { return keywords.filter((keyword) => new RegExp(" "+keyword, "i").test(head)).length === keywords.length ? true : false; }); } - this.setState({headlines: this.buildHeadlines(headlines)}); + this.setState({ headlines: this.buildHeadlines(headlines) }); } - render(){ + render() { return ( -
- - - -

{this.props.title}

- 0}> - +
+ + + +

{this.props.title}

+ 0}> + + +
+ + { t("empty") } -
- - { t("empty") } - - 0}> - 750 ? 545 : window.innerHeight - 202}}> - { - Object.keys(this.state.headlines).map((list, i) => { - return ( -
- - { - ({style}) => { - return ( -
-

{list} {this.state.headlines[list].length}

+ 0}> + 750 ? 545 : window.innerHeight - 202 }}> + { + Object.keys(this.state.headlines).map((list, i) => { + return ( +
+ + { + ({ style }) => { + return ( +
+

{list} {this.state.headlines[list].length}

+
+ ); + } + } +
+
+ { + this.state.headlines[list].map((headline, j) => { + return ( + + ); + }) + }
- ); - } - } - -
- { - this.state.headlines[list].map((headline, j) => { - return ( - - ); - }) - } -
-
- ); - }) - } -
-
+
+ ); + }) + } + + ); } @@ -322,37 +334,37 @@ class OrgViewer extends React.Component { class Headline extends React.Component { - constructor(props){ + constructor(props) { super(props); this.state = { status: props.todo_status, - properties: false + properties: false, }; } - onMenuAction(key){ - if(key === "navigate"){ + onMenuAction(key) { + if (key === "navigate") { this.props.goTo(); - }else if(key === "properties"){ - this.setState({properties: !this.state.properties}); + } else if (key === "properties") { + this.setState({ properties: !this.state.properties }); } } - onStatusToggle(){ - if(!this.props.todo_status) return; + onStatusToggle() { + if (!this.props.todo_status) return; const new_status = this.state.status === "todo" ? "done" : "todo"; - this.setState({status: new_status}); + this.setState({ status: new_status }); - const new_status_label = function(new_status, initial_status, initial_keyword){ - if(new_status === initial_status) return initial_keyword; + const new_status_label = function(new_status, initial_status, initial_keyword) { + if (new_status === initial_status) return initial_keyword; return new_status === "todo" ? "TODO" : "DONE"; }(new_status, this.props.todo_status, this.props.status); this.props.onTaskUpdate("status", this.props.line, new_status_label); } - onTimeSet(keyword, existing, value){ - if(existing === true){ + onTimeSet(keyword, existing, value) { + if (existing === true) { this.props.onTaskUpdate( "existing_"+keyword, [ @@ -360,127 +372,130 @@ class Headline extends React.Component { this.props.sortKey, this.props[keyword].line, ], - value + value, ); - }else{ + } else { const opposite_keyword = keyword === "scheduled" ? "deadline" : "scheduled"; this.props.onTaskUpdate( "new_"+keyword, [ this.props.line, this.props.sortKey, - this.props[opposite_keyword] && this.props[opposite_keyword].line || null + this.props[opposite_keyword] && this.props[opposite_keyword].line || null, ], - value + value, ); } } - render(){ + render() { const dateInput = (obj) => { - if(!obj || !obj.timestamp) return ""; + if (!obj || !obj.timestamp) return ""; const d = new Date(obj.timestamp); return d.getFullYear()+"-"+leftPad((d.getMonth() + 1).toString(), 2)+"-"+leftPad(d.getDate().toString(), 2); }; return (
-
-
-
- {this.props.title} - - - - - - -
- { - this.props.tags.map((tag, i) => { - return ( - {tag} - ); - }) - } +
+
+
+ {this.props.title} + + + + + + +
+ { + this.props.tags.map((tag, i) => { + return ( + {tag} + ); + }) + } +
+
-
+ + + + + + { t("Navigate") } + { t("Properties") } + +
- - - - - - { t("Navigate") } - { t("Properties") } - - -
- -
- -
-
- -
-
- 0 && this.state.status === "todo" && this.props.type === "todos"} className="subtask_container"> - { - this.props.tasks.map((task, i) => { - return ( - - ); - }) - } - + +
+ +
+
+ +
+
+ 0 && this.state.status === "todo" && this.props.type === "todos"} className="subtask_container"> + { + this.props.tasks.map((task, i) => { + return ( + + ); + }) + } +
); } } class Subtask extends React.Component { - constructor(props){ + constructor(props) { super(props); this.state = this.calculateState(); } - calculateState(){ - return {checked: this.props.status === "DONE"}; + calculateState() { + return { checked: this.props.status === "DONE" }; } - updateState(e){ + updateState(e) { const checked = e.target.checked; - this.setState({checked: checked}, () => { - // We don't want the interface to feel laggy while a task is beeing updated. Updating the content - // and reparsing the result is an expensive operation, this makes it feel like a piece of cake + this.setState({ checked: checked }, () => { + // We don't want the interface to feel laggy while a task is beeing updated. Updating + // the content and reparsing the result is an expensive operation, this makes it feel + // like a piece of cake window.setTimeout(() => { - window.requestAnimationFrame(() => this.props.onStatusChange(checked ? "DONE" : "TODO")); + window.requestAnimationFrame(() => { + this.props.onStatusChange(checked ? "DONE" : "TODO"); + }); }, 0); }); } - render(){ + render() { return (
- +
); } diff --git a/client/pages/viewerpage/org_viewer.scss b/client/pages/viewerpage/org_viewer.scss index 7c67f524..ddc729b8 100644 --- a/client/pages/viewerpage/org_viewer.scss +++ b/client/pages/viewerpage/org_viewer.scss @@ -69,7 +69,7 @@ overflow-y: auto; overflow-x: hidden; -webkit-overflow-scrolling: touch; - padding: 0 10px 70px 10px; + padding: 0 10px 10px 10px; .sticky_header{ z-index: 2; diff --git a/client/pages/viewerpage/pager.js b/client/pages/viewerpage/pager.js index 47be4c57..71cc0608 100644 --- a/client/pages/viewerpage/pager.js +++ b/client/pages/viewerpage/pager.js @@ -1,94 +1,98 @@ -import React from 'react'; -import { Link, withRouter } from 'react-router-dom'; +import React, { createRef } from "react"; +import { Link, withRouter } from "react-router-dom"; -import { Files } from '../../model/'; -import { sort } from '../../pages/filespage.helper.js'; -import { Icon, NgIf, EventReceiver, EventEmitter } from '../../components/'; -import { dirname, basename, settings_get, getMimeType, debounce, gid, appendShareToUrl } from '../../helpers/'; -import './pager.scss'; +import { Files } from "../../model/"; +import { sort } from "../../pages/filespage.helper.js"; +import { Icon, NgIf, EventReceiver, EventEmitter } from "../../components/"; +import { + dirname, basename, settings_get, getMimeType, debounce, appendShareToUrl, +} from "../../helpers/"; +import "./pager.scss"; - -@EventEmitter -@EventReceiver -@withRouter -export class Pager extends React.Component { - constructor(props){ +class PagerComponent extends React.Component { + constructor(props) { super(props); this.state = { files: [], - n: 0 + n: 0, }; + this.$page = createRef(); this.onKeyPress = this.onKeyPress.bind(this); this.onSubmitDebounced = debounce(this.onFormSubmit.bind(this), 1000); } - componentDidMount(){ + componentDidMount() { this.setNavigation(this.props); window.addEventListener("keyup", this.onKeyPress); - this.props.subscribe('media::next', () => { + this.props.subscribe("media::next", () => { this.navigatePage(this.calculateNextPageNumber(this.state.n)); }); - this.props.subscribe('media::previous', () => { + this.props.subscribe("media::previous", () => { this.navigatePage(this.calculatePrevPageNumber(this.state.n)); }); } - componentWillReceiveProps(props){ - if(props.path !== this.props.path){ + UNSAFE_componentWillReceiveProps(props) { + if (props.path !== this.props.path) { this.setNavigation(props); } } - componentWillUnmount(){ + componentWillUnmount() { window.removeEventListener("keyup", this.onKeyPress); - this.props.unsubscribe('media::next'); - this.props.unsubscribe('media::previous'); + this.props.unsubscribe("media::next"); + this.props.unsubscribe("media::previous"); this.hasUnmounted = true; } - navigatePage(n){ - if(this.state.files[n]){ + navigatePage(n) { + if (this.state.files[n]) { const url = appendShareToUrl(this.state.files[n].link); - if(this.refs.$page) this.refs.$page.blur(); - let preload_index = (n >= this.state.n || (this.state.n === this.state.files.length - 1 && n === 0)) ? this.calculateNextPageNumber(n) : this.calculatePrevPageNumber(n); + if (this.$page) this.$page.current.blur(); + const preload_index = ( + n >= this.state.n || + (this.state.n === this.state.files.length - 1 && n === 0) + ) ? this.calculateNextPageNumber(n) : this.calculatePrevPageNumber(n); Files.url(this.state.files[preload_index].path) .then((url) => this.props.emit("media::preload", url)) .then(() => this.props.history.push(url)) .catch(() => {}); } } - calculateNextPageNumber(n){ - if(n + 1 >= this.state.files.length) return 0; + calculateNextPageNumber(n) { + if (n + 1 >= this.state.files.length) return 0; return n + 1; } - calculatePrevPageNumber(n){ - if(n <= 0) return this.state.files.length - 1; + calculatePrevPageNumber(n) { + if (n <= 0) return this.state.files.length - 1; return n - 1; } - setNavigation(props){ + setNavigation(props) { Files._ls_from_cache(dirname(props.path)) .then((f) => { - if(f === null) return Promise.reject({code: "NO_DATA"}); + if (f === null) return Promise.reject({ code: "NO_DATA" }); return Promise.resolve(f); }) - .then((f) => f.results.filter((file) => (isImage(file.name) || isVideo(file.name)) && file.type === "file")) - .then((f) => sort(f, settings_get('filespage_sort') || 'type')) + .then((f) => f.results + .filter((file) => (isImage(file.name) || isVideo(file.name)) && + file.type === "file")) + .then((f) => sort(f, settings_get("filespage_sort") || "type")) .then((f) => findPosition(f, basename(props.path))) .then((res) => { - if(this.hasUnmounted === true) return; - if(this.props.pageChange) this.props.pageChange(res[0]); + if (this.hasUnmounted === true) return; + if (this.props.pageChange) this.props.pageChange(res[0]); this.setState({ files: res[0], - n: res[1] + n: res[1], }); }) .catch(() => {}); const findPosition = (files, filename) => { let i; - for(i=0; i < files.length; i++){ - if(files[i].name === filename){ + for (i=0; i < files.length; i++) { + if (files[i].name === filename) { break; } } @@ -102,59 +106,67 @@ export class Pager extends React.Component { }; } - onFormInputChange(e){ + onFormInputChange(e) { let n = parseInt(e.target.value); - if(Number.isNaN(n)) n = undefined; - else if(n < 1) n = 0; - else if(n > this.state.files.length) n = this.state.files.length - 1; - else{ n = n - 1; } - this.setState({n: n}); - if(n >= 0){ + if (Number.isNaN(n)) n = undefined; + else if (n < 1) n = 0; + else if (n > this.state.files.length) n = this.state.files.length - 1; + else n = n - 1; + + this.setState({ n: n }); + if (n >= 0) { this.onSubmitDebounced(); } } - onFormSubmit(e){ - if(e) e.preventDefault(); + onFormSubmit(e) { + if (e) e.preventDefault(); this.navigatePage(this.state.n); } - onKeyPress(e){ - if(e.target.classList.contains("prevent")) return; - if(e.keyCode === 39){ + onKeyPress(e) { + if (e.target.classList.contains("prevent")) return; + if (e.keyCode === 39) { this.navigatePage(this.calculateNextPageNumber(this.state.n)); - }else if(e.keyCode === 37){ + } else if (e.keyCode === 37) { this.navigatePage(this.calculatePrevPageNumber(this.state.n)); } } - render(){ - let inputWidth = this.state.n === undefined ? 12 : ((this.state.n + 1).toString().length) * 12; + render() { + const inputWidth = this.state.n === undefined ? + 12 : ((this.state.n + 1).toString().length) * 12; const nextLink = () => { const l = this.state.files[this.calculateNextPageNumber(this.state.n)]; - return (((l && l.link) || "") + window.location.search) || '#'; + return (((l && l.link) || "") + window.location.search) || "#"; }; const prevLink = () => { const l = this.state.files[this.calculatePrevPageNumber(this.state.n)]; - return (((l && l.link) || "") + window.location.search) || '#'; + return (((l && l.link) || "") + window.location.search) || "#"; }; const current_page_number = this.state.n === undefined ? "" : this.state.n + 1; return (
-
- 0} type="inline"> - - - - -
+
+ 0} type="inline"> + + + + +
); } } + +export const Pager = EventEmitter(EventReceiver(withRouter(PagerComponent)));