Refactored ProjectListItem, client-side file resize feature

pull/339/head
Piero Toffanin 2017-11-22 14:53:29 -05:00
rodzic 7815514ee9
commit 8703af0dae
6 zmienionych plików z 217 dodań i 134 usunięć

Wyświetl plik

@ -2,17 +2,20 @@ import '../css/NewTaskPanel.scss';
import React from 'react';
import EditTaskForm from './EditTaskForm';
import PropTypes from 'prop-types';
import Storage from '../classes/Storage';
class NewTaskPanel extends React.Component {
static defaultProps = {
uploading: false,
name: ""
name: "",
filesCount: 0,
showResize: false
};
static propTypes = {
onSave: PropTypes.func.isRequired,
name: PropTypes.string,
uploading: PropTypes.bool
filesCount: PropTypes.number,
showResize: PropTypes.bool
};
constructor(props){
@ -20,30 +23,43 @@ class NewTaskPanel extends React.Component {
this.state = {
name: props.name,
editing: true,
editTaskFormLoaded: false
editTaskFormLoaded: false,
resize: Storage.getItem('do_resize') !== null ? Storage.getItem('do_resize') == "1" : true,
resizeSize: parseInt(Storage.getItem('resize_size')) || 2048
};
this.save = this.save.bind(this);
this.edit = this.edit.bind(this);
this.handleFormTaskLoaded = this.handleFormTaskLoaded.bind(this);
this.getTaskInfo = this.getTaskInfo.bind(this);
this.setResize = this.setResize.bind(this);
this.handleResizeSizeChange = this.handleResizeSizeChange.bind(this);
}
save(e){
e.preventDefault();
this.setState({editing: false});
this.taskForm.saveLastPresetToStorage();
Storage.setItem('resize_size', this.state.resizeSize);
Storage.setItem('do_resize', this.state.resize ? "1" : "0");
if (this.props.onSave) this.props.onSave(this.getTaskInfo());
}
getTaskInfo(){
return this.taskForm.getTaskInfo();
return Object.assign(this.taskForm.getTaskInfo(), {
resizeTo: (this.state.resize && this.state.resizeSize > 0) ? this.state.resizeSize : null
});
}
edit(e){
e.preventDefault();
this.setState({editing: true});
setResize(flag){
return e => {
this.setState({resize: flag});
}
}
handleResizeSizeChange(e){
// Remove all non-digit characters
let n = parseInt(e.target.value.replace(/[^\d]*/g, ""));
if (isNaN(n)) n = "";
this.setState({resizeSize: n});
}
handleFormTaskLoaded(){
@ -51,36 +67,59 @@ class NewTaskPanel extends React.Component {
}
render() {
if (this.props.uploading || this.state.editing){
// Done editing, but still uploading
return (
<div className="new-task-panel theme-background-highlight">
<div className={"form-horizontal " + (this.state.editing ? "" : "hide")}>
<p>{this.props.uploading ?
"Your images are being uploaded. In the meanwhile, check these additional options:"
: "Please check these additional options:"}</p>
<EditTaskForm
onFormLoaded={this.handleFormTaskLoaded}
ref={(domNode) => { if (domNode) this.taskForm = domNode; }}
/>
{this.state.editTaskFormLoaded ?
<div className="form-group">
<div className="col-sm-offset-2 col-sm-10 text-right">
<button type="submit" className="btn btn-primary" onClick={this.save}><i className="glyphicon glyphicon-saved"></i> {this.props.uploading ? "Save" : "Start Processing"}</button>
</div>
return (
<div className="new-task-panel theme-background-highlight">
<div className="form-horizontal">
<p>{this.props.filesCount} files selected. Please check these additional options:</p>
<EditTaskForm
onFormLoaded={this.handleFormTaskLoaded}
ref={(domNode) => { if (domNode) this.taskForm = domNode; }}
/>
<div className="form-group">
<label className="col-sm-2 control-label">Resize Images</label>
<div className="col-sm-10">
<div className="btn-group">
<button type="button" className="btn btn-default dropdown-toggle" data-toggle="dropdown">
{this.state.resize ?
"Yes" : "Skip"} <span className="caret"></span>
</button>
<ul className="dropdown-menu">
<li>
<a href="javascript:void(0);"
onClick={this.setResize(true)}>
<i style={{opacity: this.state.resize ? 1 : 0}} className="fa fa-check"></i> Yes</a>
</li>
<li>
<a href="javascript:void(0);"
onClick={this.setResize(false)}>
<i style={{opacity: !this.state.resize ? 1 : 0}} className="fa fa-check"></i> Skip</a>
</li>
</ul>
</div>
: ""}
<div className={"resize-control " + (!this.state.resize ? "hide" : "")}>
<input
type="number"
step="100"
className="form-control"
onChange={this.handleResizeSizeChange}
value={this.state.resizeSize}
/>
<span>px</span>
</div>
</div>
</div>
<div className={"pull-right " + (!this.state.editing ? "" : "hide")}>
<button type="submit" className="btn btn-primary btn-sm glyphicon glyphicon-pencil" onClick={this.edit}></button>
</div>
<p className={"header " + (!this.state.editing ? "" : "hide")}><strong>Thank you!</strong> Please wait for the upload to complete.</p>
{this.state.editTaskFormLoaded ?
<div className="form-group">
<div className="col-sm-offset-2 col-sm-10 text-right">
<button type="submit" className="btn btn-primary" onClick={this.save}><i className="glyphicon glyphicon-saved"></i> Start Processing</button>
</div>
</div>
: ""}
</div>
);
}else{
return (<div><i className="fa fa-refresh fa-spin fa-fw"></i></div>);
}
</div>
);
}
}

Wyświetl plik

@ -0,0 +1,42 @@
import React from 'react';
import PropTypes from 'prop-types';
class ProgressBar extends React.Component {
static propTypes = {
current: PropTypes.number.isRequired, // processed files so far
total: PropTypes.number.isRequired, // number of files
template: PropTypes.oneOfType([
PropTypes.string,
PropTypes.func
]).isRequired // String or callback for label
}
render() {
let { current, total } = this.props;
if (total == 0) total = 1;
const percentage = ((current / total) * 100).toFixed(2);
const active = percentage < 100 ? "active" : "";
const label = typeof this.props.template === 'string' ?
this.props.template :
this.props.template({current, total, active, percentage});
return (
<div>
<div className="progress">
<div className={'progress-bar progress-bar-success progress-bar-striped ' + active} style={{width: percentage + '%'}}>
{percentage}%
</div>
</div>
{label !== "" ?
<div className="text-left small">
{label}
</div>
: ""}
</div>
);
}
}
export default ProgressBar;

Wyświetl plik

@ -4,6 +4,7 @@ import update from 'immutability-helper';
import TaskList from './TaskList';
import NewTaskPanel from './NewTaskPanel';
import UploadProgressBar from './UploadProgressBar';
import ProgressBar from './ProgressBar';
import ErrorMessage from './ErrorMessage';
import EditProjectDialog from './EditProjectDialog';
import Dropzone from '../vendor/dropzone';
@ -26,7 +27,6 @@ class ProjectListItem extends React.Component {
this.state = {
showTaskList: this.historyNav.isValueInQSList("project_task_open", props.data.id),
updatingTask: false,
upload: this.getDefaultUploadState(),
error: "",
data: props.data,
@ -64,7 +64,6 @@ class ProjectListItem extends React.Component {
}
componentWillUnmount(){
if (this.updateTaskRequest) this.updateTaskRequest.abort();
if (this.deleteProjectRequest) this.deleteProjectRequest.abort();
if (this.refreshRequest) this.refreshRequest.abort();
}
@ -73,13 +72,13 @@ class ProjectListItem extends React.Component {
return {
uploading: false,
editing: false,
resizing: false,
resizedImages: 0,
error: "",
progress: 0,
totalCount: 0,
totalBytes: 0,
totalBytesSent: 0,
savedTaskInfo: false,
taskId: null
totalBytesSent: 0
};
}
@ -115,10 +114,7 @@ class ProjectListItem extends React.Component {
headers: {
[csrf.header]: csrf.token
},
resizeWidth: 2048,
resizeQuality: 1.0
}
});
this.dz.on("totaluploadprogress", (progress, totalBytes, totalBytesSent) => {
@ -128,28 +124,15 @@ class ProjectListItem extends React.Component {
})
.on("addedfiles", files => {
this.setUploadState({
editing: true,
totalCount: files.length
});
// TODO: ask for image resize
this.dz.processQueue();
})
.on("transformstart", () => {
console.log("START");
})
.on("transformcompleted", (n) => {
console.log(n);
// TODO: update state
.on("transformcompleted", (total) => {
this.setUploadState({resizedImages: total});
})
.on("transformend", () => {
console.log("END");
})
.on("processingmultiple", () => {
this.setUploadState({
uploading: true,
editing: true
})
this.setUploadState({resizing: false, uploading: true});
})
.on("completemultiple", (files) => {
// Check
@ -158,24 +141,20 @@ class ProjectListItem extends React.Component {
// All files have uploaded!
if (success){
this.setUploadState({uploading: false});
try{
let response = JSON.parse(files[0].xhr.response);
if (!response.id) throw new Error(`Expected id field, but none given (${response})`);
let taskId = response.id;
this.setUploadState({taskId});
// Update task information (if the user has completed this step)
if (this.state.upload.savedTaskInfo){
this.updateTaskInfo(taskId, this.newTaskPanel.getTaskInfo());
if (this.state.showTaskList){
this.taskList.refresh();
}else{
// Need to wait for user to confirm task options
this.setState({showTaskList: true});
}
this.resetUploadState();
this.refresh();
}catch(e){
this.setUploadState({error: `Invalid response from server: ${e.message}`})
this.setUploadState({error: `Invalid response from server: ${e.message}`, uploading: false})
}
}else{
this.setUploadState({
uploading: false,
@ -187,49 +166,21 @@ class ProjectListItem extends React.Component {
this.resetUploadState();
})
.on("dragenter", () => {
this.resetUploadState();
if (!this.state.upload.uploading && !this.state.upload.resizing){
this.resetUploadState();
}
})
.on("sending", (file, xhr, formData) => {
if (!formData.has("auto_processing_node")){
formData.append('auto_processing_node', "false");
}
const taskInfo = this.dz._taskInfo;
if (!formData.has("name")) formData.append("name", taskInfo.name);
if (!formData.has("options")) formData.append("options", JSON.stringify(taskInfo.options));
if (!formData.has("processing_node")) formData.append("processing_node", taskInfo.selectedNode.id);
if (!formData.has("auto_processing_node")) formData.append("auto_processing_node", taskInfo.selectedNode.key == "auto");
});
}
}
updateTaskInfo(taskId, taskInfo){
if (!taskId) throw new Error("taskId is not set");
if (!taskInfo) throw new Error("taskId is not set");
this.setUploadState({editing: false});
this.setState({updatingTask: true});
this.updateTaskRequest =
$.ajax({
url: `/api/projects/${this.state.data.id}/tasks/${this.state.upload.taskId}/`,
contentType: 'application/json',
data: JSON.stringify({
name: taskInfo.name,
options: taskInfo.options,
processing_node: taskInfo.selectedNode.id,
auto_processing_node: taskInfo.selectedNode.key == "auto"
}),
dataType: 'json',
type: 'PATCH'
}).done((json) => {
if (this.state.showTaskList){
this.taskList.refresh();
}else{
this.setState({showTaskList: true});
}
this.refresh();
}).fail(() => {
this.setUploadState({error: "Could not update task information. Plese try again."});
}).always(() => {
this.setState({updatingTask: false});
});
}
setRef(prop){
return (domNode) => {
if (domNode != null) this[prop] = domNode;
@ -272,12 +223,19 @@ class ProjectListItem extends React.Component {
}
handleTaskSaved(taskInfo){
this.setUploadState({savedTaskInfo: true});
this.dz._taskInfo = taskInfo; // Allow us to access the task info from dz
// Has the upload finished?
if (!this.state.upload.uploading && this.state.upload.taskId !== null){
this.updateTaskInfo(this.state.upload.taskId, taskInfo);
// Update dropzone settings
if (taskInfo.resizeTo !== null){
this.dz.options.resizeWidth = taskInfo.resizeTo;
this.dz.options.resizeQuality = 1.0;
this.setUploadState({resizing: true, editing: false});
}else{
this.setUploadState({uploading: true, editing: false});
}
this.dz.processQueue();
}
handleEditProject(){
@ -374,7 +332,18 @@ class ProjectListItem extends React.Component {
</div>
<i className="drag-drop-icon fa fa-inbox"></i>
<div className="row">
{this.state.upload.editing ? <UploadProgressBar {...this.state.upload}/> : ""}
{this.state.upload.uploading ? <UploadProgressBar {...this.state.upload}/> : ""}
{this.state.upload.resizing ?
<ProgressBar
current={this.state.upload.resizedImages}
total={this.state.upload.totalCount}
template={(info) => `Resized ${info.current} of ${info.total} images. Your browser might slow down during this process.`}
/>
: ""}
{this.state.upload.uploading || this.state.upload.resizing ?
<i className="fa fa-refresh fa-spin fa-fw" />
: ""}
{this.state.upload.error !== "" ?
<div className="alert alert-warning alert-dismissible">
@ -384,17 +353,13 @@ class ProjectListItem extends React.Component {
: ""}
{this.state.upload.editing ?
<NewTaskPanel
uploading={this.state.upload.uploading}
<NewTaskPanel
onSave={this.handleTaskSaved}
ref={this.setRef("newTaskPanel")}
filesCount={this.state.upload.totalCount}
showResize={true}
/>
: ""}
{this.state.updatingTask ?
<span>Updating task information... <i className="fa fa-refresh fa-spin fa-fw"></i></span>
: ""}
{this.state.showTaskList ?
<TaskList
ref={this.setRef("taskList")}

Wyświetl plik

@ -0,0 +1,10 @@
import React from 'react';
import { shallow } from 'enzyme';
import ProgressBar from '../ProgressBar';
describe('<ProgressBar />', () => {
it('renders without exploding', () => {
const wrapper = shallow(<ProgressBar current={2} total={100} template="Hello" />);
expect(wrapper.exists()).toBe(true);
})
});

Wyświetl plik

@ -17,4 +17,23 @@
p.header{
margin: 8px;
}
.resize-control{
display: inline-block;
vertical-align: middle;
margin-left: 8px;
width: 11em;
input{
width: 8em;
display: inline-block;
}
span{
width: 2em;
opacity: 0.6;
font-size: 90%;
display: inline-block;
}
}
}

Wyświetl plik

@ -2493,21 +2493,29 @@ var Dropzone = function (_Emitter) {
// Clumsy way of handling asynchronous calls, until I get to add a proper Future library.
var doneCounter = 0;
// Modified for WebODM
_this17.emit("transformstart", files);
var _loop = function _loop(i) {
_this17.options.transformFile.call(_this17, files[i], function (transformedFile) {
transformedFiles[i] = transformedFile;
_this17.emit("transformcompleted", files.length);
if (++doneCounter === files.length) {
_this17.emit("transformend", files);
done(transformedFiles);
}
});
};
// Process in batches based on the available number of cores
var stride = Math.max(1, (navigator.hardwareConcurrency || 2) - 1);
for (var i = 0; i < files.length; i++) {
_loop(i);
var process = function(i, s){
if (files[i + s]){
_this17.options.transformFile.call(_this17, files[i + s], function (transformedFile) {
transformedFiles[i + s] = transformedFile;
_this17.emit("transformcompleted", doneCounter + 1);
if (++doneCounter === files.length) {
_this17.emit("transformend", files);
done(transformedFiles);
}else{
process(i + stride, s);
}
});
}
}
for (var s = 0; s < stride; s++){
process(0, s);
}
}