kopia lustrzana https://github.com/OpenDroneMap/WebODM
Task UI improvements, PoC move task dialog
rodzic
4daa261f7d
commit
9a70c07aca
|
@ -37,3 +37,9 @@ class ProjectViewSet(viewsets.ModelViewSet):
|
|||
serializer_class = ProjectSerializer
|
||||
queryset = models.Project.objects.prefetch_related('task_set').filter(deleting=False).order_by('-created_at')
|
||||
ordering_fields = '__all__'
|
||||
|
||||
# Disable pagination when not requesting any page
|
||||
def paginate_queryset(self, queryset):
|
||||
if self.paginator and self.request.query_params.get(self.paginator.page_query_param, None) is None:
|
||||
return None
|
||||
return super().paginate_queryset(queryset)
|
|
@ -12,7 +12,7 @@ let statusCodes = {
|
|||
icon: "far fa-hourglass fa-fw"
|
||||
},
|
||||
[RUNNING]: {
|
||||
descr: _("Running"),
|
||||
descr: _("Processing"),
|
||||
icon: "fa fa-cog fa-spin fa-fw"
|
||||
},
|
||||
[FAILED]: {
|
||||
|
|
|
@ -17,7 +17,7 @@ class FormDialog extends React.Component {
|
|||
|
||||
static propTypes = {
|
||||
getFormData: PropTypes.func.isRequired,
|
||||
reset: PropTypes.func.isRequired,
|
||||
reset: PropTypes.func,
|
||||
saveAction: PropTypes.func.isRequired,
|
||||
onShow: PropTypes.func,
|
||||
onHide: PropTypes.func,
|
||||
|
@ -88,7 +88,7 @@ class FormDialog extends React.Component {
|
|||
}
|
||||
|
||||
show(){
|
||||
this.props.reset();
|
||||
if (this.props.reset) this.props.reset();
|
||||
this.setState({showModal: true, saving: false, error: ""});
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
import React from 'react';
|
||||
import FormDialog from './FormDialog';
|
||||
import PropTypes from 'prop-types';
|
||||
import { _ } from '../classes/gettext';
|
||||
import $ from 'jquery';
|
||||
|
||||
class MoveTaskDialog extends React.Component {
|
||||
static defaultProps = {
|
||||
title: _("Move Task"),
|
||||
saveLabel: _("Save Changes"),
|
||||
savingLabel: _("Moving..."),
|
||||
saveIcon: "far fa-edit",
|
||||
show: true
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
task: PropTypes.object.isRequired,
|
||||
saveAction: PropTypes.func.isRequired,
|
||||
title: PropTypes.string,
|
||||
saveLabel: PropTypes.string,
|
||||
savingLabel: PropTypes.string,
|
||||
saveIcon: PropTypes.string,
|
||||
show: PropTypes.bool
|
||||
};
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
projectId: props.task.project.id,
|
||||
projects: [],
|
||||
loading: true
|
||||
};
|
||||
|
||||
this.getFormData = this.getFormData.bind(this);
|
||||
this.onShow = this.onShow.bind(this);
|
||||
}
|
||||
|
||||
getFormData(){
|
||||
return this.state;
|
||||
}
|
||||
|
||||
onShow(){
|
||||
this.setState({loading: true, projects: []});
|
||||
|
||||
// Load projects from API
|
||||
this.serverRequest = $.getJSON(`/api/projects/?ordering=-created_at`, json => {
|
||||
this.setState({
|
||||
projects: json.filter(p => p.permissions.indexOf("add") !== -1)
|
||||
});
|
||||
})
|
||||
.fail((jqXHR, textStatus, errorThrown) => {
|
||||
this.dialog.setState({
|
||||
error: interpolate(_("Could not load projects list: %(error)s"), {error: textStatus})
|
||||
});
|
||||
})
|
||||
.always(() => {
|
||||
this.setState({loading: false});
|
||||
this.serverRequest = null;
|
||||
});
|
||||
}
|
||||
|
||||
show(){
|
||||
this.dialog.show();
|
||||
}
|
||||
|
||||
hide(){
|
||||
this.dialog.hide();
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
if (this.serverRquest) this.serverRquest.abort();
|
||||
}
|
||||
|
||||
handleProjectChange = e => {
|
||||
this.setState({projectId: e.target.value});
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<FormDialog {...this.props}
|
||||
getFormData={this.getFormData}
|
||||
onShow={this.onShow}
|
||||
ref={(domNode) => { this.dialog = domNode; }}>
|
||||
{!this.state.loading ?
|
||||
<div className="form-group">
|
||||
<label className="col-sm-2 control-label">{_("Project")}</label>
|
||||
<div className="col-sm-10">
|
||||
<select className="form-control"
|
||||
value={this.state.projectId}
|
||||
onChange={this.handleProjectChange}>
|
||||
{this.state.projects.map(p =>
|
||||
<option value={p.id} key={p.id}>{p.name}</option>
|
||||
)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
: <i className="fa fa-circle-notch fa-spin fa-fw name-loading"></i>}
|
||||
</FormDialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default MoveTaskDialog;
|
|
@ -47,6 +47,7 @@ class ProjectListItem extends React.Component {
|
|||
this.handleEditProject = this.handleEditProject.bind(this);
|
||||
this.updateProject = this.updateProject.bind(this);
|
||||
this.taskDeleted = this.taskDeleted.bind(this);
|
||||
this.taskMoved = this.taskMoved.bind(this);
|
||||
this.hasPermission = this.hasPermission.bind(this);
|
||||
}
|
||||
|
||||
|
@ -304,6 +305,10 @@ class ProjectListItem extends React.Component {
|
|||
this.refresh();
|
||||
}
|
||||
|
||||
taskMoved(){
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
handleDelete(){
|
||||
return $.ajax({
|
||||
url: `/api/projects/${this.state.data.id}/`,
|
||||
|
@ -570,6 +575,8 @@ class ProjectListItem extends React.Component {
|
|||
ref={this.setRef("taskList")}
|
||||
source={`/api/projects/${data.id}/tasks/?ordering=-created_at`}
|
||||
onDelete={this.taskDeleted}
|
||||
onMove={this.taskMoved}
|
||||
hasPermission={this.hasPermission}
|
||||
history={this.props.history}
|
||||
/> : ""}
|
||||
|
||||
|
|
|
@ -9,7 +9,9 @@ class TaskList extends React.Component {
|
|||
static propTypes = {
|
||||
history: PropTypes.object.isRequired,
|
||||
source: PropTypes.string.isRequired, // URL where to load task list
|
||||
onDelete: PropTypes.func
|
||||
onDelete: PropTypes.func,
|
||||
onMove: PropTypes.func,
|
||||
hasPermission: PropTypes.func.isRequired
|
||||
}
|
||||
|
||||
constructor(props){
|
||||
|
@ -69,6 +71,13 @@ class TaskList extends React.Component {
|
|||
if (this.props.onDelete) this.props.onDelete(id);
|
||||
}
|
||||
|
||||
moveTask(id){
|
||||
this.setState({
|
||||
tasks: this.state.tasks.filter(t => t.id !== id)
|
||||
});
|
||||
if (this.props.onMove) this.props.onMove(id);
|
||||
}
|
||||
|
||||
render() {
|
||||
let message = "";
|
||||
if (this.state.loading){
|
||||
|
@ -88,7 +97,9 @@ class TaskList extends React.Component {
|
|||
data={task}
|
||||
key={task.id}
|
||||
refreshInterval={3000}
|
||||
onDelete={this.deleteTask}
|
||||
onDelete={this.deleteTask}
|
||||
onMove={this.moveTask}
|
||||
hasPermission={this.props.hasPermission}
|
||||
history={this.props.history} />
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -9,6 +9,7 @@ import AssetDownloadButtons from './AssetDownloadButtons';
|
|||
import HistoryNav from '../classes/HistoryNav';
|
||||
import PropTypes from 'prop-types';
|
||||
import TaskPluginActionButtons from './TaskPluginActionButtons';
|
||||
import MoveTaskDialog from './MoveTaskDialog';
|
||||
import PipelineSteps from '../classes/PipelineSteps';
|
||||
import Css from '../classes/Css';
|
||||
import Trans from './Trans';
|
||||
|
@ -19,7 +20,9 @@ class TaskListItem extends React.Component {
|
|||
history: PropTypes.object.isRequired,
|
||||
data: PropTypes.object.isRequired, // task json
|
||||
refreshInterval: PropTypes.number, // how often to refresh info
|
||||
onDelete: PropTypes.func
|
||||
onDelete: PropTypes.func,
|
||||
onMove: PropTypes.func,
|
||||
hasPermission: PropTypes.func
|
||||
}
|
||||
|
||||
constructor(props){
|
||||
|
@ -37,7 +40,8 @@ class TaskListItem extends React.Component {
|
|||
memoryError: false,
|
||||
friendlyTaskError: "",
|
||||
pluginActionButtons: [],
|
||||
view: "basic"
|
||||
view: "basic",
|
||||
showMoveDialog: false
|
||||
}
|
||||
|
||||
for (let k in props.data){
|
||||
|
@ -271,6 +275,10 @@ class TaskListItem extends React.Component {
|
|||
this.setAutoRefresh();
|
||||
}
|
||||
|
||||
handleMoveTask = () => {
|
||||
this.setState({showMoveDialog: true});
|
||||
}
|
||||
|
||||
getRestartSubmenuItems(){
|
||||
const { task } = this.state;
|
||||
|
||||
|
@ -406,21 +414,6 @@ class TaskListItem extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
// Ability to change options
|
||||
if ([statusCodes.FAILED, statusCodes.COMPLETED, statusCodes.CANCELED].indexOf(task.status) !== -1 ||
|
||||
(!task.processing_node)){
|
||||
addActionButton(_("Edit"), "btn-primary pull-right edit-button", "glyphicon glyphicon-pencil", () => {
|
||||
this.startEditing();
|
||||
}, {
|
||||
className: "inline"
|
||||
});
|
||||
}
|
||||
|
||||
if ([statusCodes.QUEUED, statusCodes.RUNNING, null].indexOf(task.status) !== -1 &&
|
||||
(task.processing_node || imported)){
|
||||
addActionButton(_("Cancel"), "btn-primary", "glyphicon glyphicon-remove-circle", this.genActionApiCall("cancel", {defaultError: _("Cannot cancel task.")}));
|
||||
}
|
||||
|
||||
if ([statusCodes.FAILED, statusCodes.COMPLETED, statusCodes.CANCELED].indexOf(task.status) !== -1 &&
|
||||
task.processing_node &&
|
||||
!imported){
|
||||
|
@ -435,11 +428,6 @@ class TaskListItem extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
addActionButton(_("Delete"), "btn-danger", "glyphicon glyphicon-trash", this.genActionApiCall("remove", {
|
||||
confirm: _("All information related to this task, including images, maps and models will be deleted. Continue?"),
|
||||
defaultError: _("Cannot delete task.")
|
||||
}));
|
||||
|
||||
const disabled = this.state.actionButtonsDisabled ||
|
||||
([pendingActions.CANCEL,
|
||||
pendingActions.REMOVE,
|
||||
|
@ -580,6 +568,8 @@ class TaskListItem extends React.Component {
|
|||
</div>;
|
||||
}
|
||||
}
|
||||
|
||||
let statusIcon = statusCodes.icon(task.status);
|
||||
|
||||
// @param type {String} one of: ['neutral', 'done', 'error']
|
||||
const getStatusLabel = (text, type = 'neutral', progress = 100) => {
|
||||
|
@ -589,11 +579,10 @@ class TaskListItem extends React.Component {
|
|||
return (<div
|
||||
className={"status-label theme-border-primary " + type}
|
||||
style={{background: `linear-gradient(90deg, ${color} ${progress}%, rgba(255, 255, 255, 0) ${progress}%)`}}
|
||||
title={text}>{text}</div>);
|
||||
title={text}><i className={statusIcon}></i> {text}</div>);
|
||||
}
|
||||
|
||||
let statusLabel = "";
|
||||
let statusIcon = statusCodes.icon(task.status);
|
||||
let showEditLink = false;
|
||||
|
||||
if (task.last_error){
|
||||
|
@ -624,8 +613,57 @@ class TaskListItem extends React.Component {
|
|||
statusLabel = getStatusLabel(status, type, progress);
|
||||
}
|
||||
|
||||
const taskActions = [];
|
||||
const addTaskAction = (label, icon, onClick) => {
|
||||
taskActions.push(
|
||||
<li key={label}><a href="javascript:void(0)" onClick={onClick}><i className={icon}></i>{label}</a></li>
|
||||
);
|
||||
};
|
||||
|
||||
if ([statusCodes.QUEUED, statusCodes.RUNNING, null].indexOf(task.status) !== -1 &&
|
||||
(task.processing_node || imported) && this.props.hasPermission("change")){
|
||||
addTaskAction(_("Cancel"), "glyphicon glyphicon-remove-circle", this.genActionApiCall("cancel", {defaultError: _("Cannot cancel task.")}));
|
||||
}
|
||||
|
||||
// Ability to change options
|
||||
const canAddDelPerms = this.props.hasPermission("add") && this.props.hasPermission("delete");
|
||||
const editable = [statusCodes.FAILED, statusCodes.COMPLETED, statusCodes.CANCELED].indexOf(task.status) !== -1;
|
||||
|
||||
if (canAddDelPerms){
|
||||
if (editable || (!task.processing_node)){
|
||||
taskActions.push(<li key="edit"><a href="javascript:void(0)" onClick={this.startEditing}><i className="glyphicon glyphicon-pencil"></i>{_("Edit")}</a></li>);
|
||||
}
|
||||
|
||||
if (editable){
|
||||
taskActions.push(
|
||||
<li key="move"><a href="javascript:void(0)" onClick={this.handleMoveTask}><i className="fa fa-arrows-alt"></i>{_("Move")}</a></li>,
|
||||
<li key="duplicate"><a href="javascript:void(0)"><i className="fa fa-copy"></i>{_("Duplicate")}</a></li>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (this.props.hasPermission("delete")){
|
||||
taskActions.push(
|
||||
<li key="sep" role="separator" className="divider"></li>,
|
||||
);
|
||||
|
||||
addTaskAction(_("Delete"), "glyphicon glyphicon-trash", this.genActionApiCall("remove", {
|
||||
confirm: _("All information related to this task, including images, maps and models will be deleted. Continue?"),
|
||||
defaultError: _("Cannot delete task.")
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="task-list-item">
|
||||
{this.state.showMoveDialog ?
|
||||
<MoveTaskDialog
|
||||
task={task}
|
||||
ref={(domNode) => { this.moveTaskDialog = domNode; }}
|
||||
onHide={() => this.setState({showMoveDialog: false})}
|
||||
saveAction={() => {}}
|
||||
/>
|
||||
: ""}
|
||||
<div className="row">
|
||||
<div className="col-sm-5 name">
|
||||
<i onClick={this.toggleExpanded} className={"clickable far " + (this.state.expanded ? "fa-minus-square" : " fa-plus-square")}></i> <a href="javascript:void(0);" onClick={this.toggleExpanded}>{name}</a>
|
||||
|
@ -642,9 +680,16 @@ class TaskListItem extends React.Component {
|
|||
: statusLabel}
|
||||
</div>
|
||||
<div className="col-sm-1 text-right">
|
||||
<div className="status-icon">
|
||||
<i className={statusIcon}></i>
|
||||
</div>
|
||||
{taskActions.length > 0 ?
|
||||
<div className="btn-group">
|
||||
<button disabled={this.state.actionButtonsDisabled} className="btn task-actions btn-secondary btn-xs dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<i className="fa fa-ellipsis-h"></i>
|
||||
</button>
|
||||
<ul className="dropdown-menu dropdown-menu-right">
|
||||
{taskActions}
|
||||
</ul>
|
||||
</div>
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
{expanded}
|
||||
|
|
|
@ -31,11 +31,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
.status-icon{
|
||||
margin-top: 3px;
|
||||
.fa-cog{
|
||||
width: auto;
|
||||
}
|
||||
.fa-cog{
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.status-label{
|
||||
|
|
Ładowanie…
Reference in New Issue