kopia lustrzana https://github.com/OpenDroneMap/WebODM
CSS improvements, edit project modal dialog, better project display UI, create new projects
rodzic
9cf6a7222a
commit
7c0fc4ffd5
|
|
@ -6,7 +6,11 @@ from .tasks import TaskIDsSerializer
|
|||
|
||||
|
||||
class ProjectSerializer(serializers.ModelSerializer):
|
||||
tasks = TaskIDsSerializer(many=True)
|
||||
tasks = TaskIDsSerializer(many=True, read_only=True)
|
||||
owner = serializers.HiddenField(
|
||||
default=serializers.CurrentUserDefault()
|
||||
)
|
||||
created_at = serializers.ReadOnlyField()
|
||||
|
||||
class Meta:
|
||||
model = models.Project
|
||||
|
|
@ -22,7 +26,7 @@ class ProjectViewSet(viewsets.ModelViewSet):
|
|||
- /api/projects/<projectId>/tasks : list all tasks belonging to a project<br/>
|
||||
- /api/projects/<projectId>/tasks/<taskId> : get task details
|
||||
"""
|
||||
filter_fields = ('id', 'owner', 'name')
|
||||
filter_fields = ('id', 'name', 'description', 'created_at')
|
||||
serializer_class = ProjectSerializer
|
||||
queryset = models.Project.objects.all()
|
||||
ordering_fields = '__all__'
|
||||
Plik binarny nie jest wyświetlany.
|
|
@ -1,3 +1,7 @@
|
|||
html{
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#navbar-top{
|
||||
height: 50px;
|
||||
min-height: 50px;
|
||||
|
|
@ -86,6 +90,10 @@ ul#side-menu.nav{
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.modal-dialog{
|
||||
z-index: 999999;
|
||||
}
|
||||
|
||||
table.table-first-col-bold{
|
||||
td:first-child{
|
||||
font-weight: bold;
|
||||
|
|
|
|||
|
|
@ -1,11 +1,55 @@
|
|||
import React from 'react';
|
||||
import './css/Dashboard.scss';
|
||||
import ProjectList from './components/ProjectList';
|
||||
import EditProjectDialog from './components/EditProjectDialog';
|
||||
import $ from 'jquery';
|
||||
|
||||
class Dashboard extends React.Component {
|
||||
constructor(){
|
||||
super();
|
||||
|
||||
this.handleAddProject = this.handleAddProject.bind(this);
|
||||
this.addNewProject = this.addNewProject.bind(this);
|
||||
}
|
||||
|
||||
handleAddProject(){
|
||||
this.projectDialog.show();
|
||||
}
|
||||
|
||||
addNewProject(project){
|
||||
if (!project.name) return (new $.Deferred()).reject("Name field is required");
|
||||
|
||||
return $.ajax({
|
||||
url: `/api/projects/`,
|
||||
type: 'POST',
|
||||
data: {
|
||||
name: project.name,
|
||||
description: project.descr
|
||||
}
|
||||
}).done(() => {
|
||||
this.projectList.refresh();
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<ProjectList source="/api/projects/?ordering=-created_at"/>
|
||||
<div>
|
||||
<div className="text-right add-button">
|
||||
<button type="button"
|
||||
className="btn btn-primary btn-sm"
|
||||
onClick={this.handleAddProject}>
|
||||
<i className="glyphicon glyphicon-plus"></i>
|
||||
Add Project
|
||||
</button>
|
||||
</div>
|
||||
<EditProjectDialog
|
||||
saveAction={this.addNewProject}
|
||||
ref={(domNode) => { this.projectDialog = domNode; }}
|
||||
/>
|
||||
<ProjectList
|
||||
source="/api/projects/?ordering=-created_at"
|
||||
ref={(domNode) => { this.projectList = domNode; }} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,17 +7,15 @@ class MapView extends React.Component {
|
|||
static defaultProps = {
|
||||
task: "",
|
||||
project: ""
|
||||
}
|
||||
};
|
||||
|
||||
static propTypes() {
|
||||
return {
|
||||
static propTypes = {
|
||||
// task id to display, if empty display all for a particular project
|
||||
task: React.PropTypes.string,
|
||||
|
||||
// project id to display, if empty display all
|
||||
project: React.PropTypes.string
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
|
|
|||
|
|
@ -5,15 +5,12 @@ class AssetDownloadButtons extends React.Component {
|
|||
static defaultProps = {
|
||||
disabled: false,
|
||||
task: null
|
||||
}
|
||||
|
||||
static propTypes() {
|
||||
return {
|
||||
disabled: React.PropTypes.boolean,
|
||||
task: React.PropTypes.object.isRequired
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
disabled: React.PropTypes.bool,
|
||||
task: React.PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
constructor(props){
|
||||
super();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
import React from 'react';
|
||||
import '../vendor/bootstrap.min';
|
||||
import ErrorMessage from './ErrorMessage';
|
||||
import '../css/EditProjectDialog.scss';
|
||||
import $ from 'jquery';
|
||||
|
||||
class EditProjectDialog extends React.Component {
|
||||
static defaultProps = {
|
||||
title: "New Project",
|
||||
saveLabel: "Create Project",
|
||||
savingLabel: "Creating project...",
|
||||
saveIcon: "glyphicon glyphicon-plus",
|
||||
projectName: "",
|
||||
projectDescr: "",
|
||||
show: false
|
||||
};
|
||||
|
||||
static propTypes = {
|
||||
saveAction: React.PropTypes.func.isRequired,
|
||||
deleteAction: React.PropTypes.func,
|
||||
title: React.PropTypes.string,
|
||||
saveLabel: React.PropTypes.string,
|
||||
savingLabel: React.PropTypes.string,
|
||||
saveIcon: React.PropTypes.string,
|
||||
projectName: React.PropTypes.string,
|
||||
projectDescr: React.PropTypes.string,
|
||||
show: React.PropTypes.bool
|
||||
};
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
showModal: props.show,
|
||||
saving: false,
|
||||
error: ""
|
||||
};
|
||||
|
||||
this.show = this.show.bind(this);
|
||||
this.hide = this.hide.bind(this);
|
||||
this.handleSave = this.handleSave.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
$(this.modal)
|
||||
// Ensure state is kept up to date when
|
||||
// the user presses the escape key
|
||||
.on('hidden.bs.modal', (e) => {
|
||||
this.setState({showModal: false});
|
||||
})
|
||||
|
||||
// Autofocus
|
||||
.on('shown.bs.modal', (e) => {
|
||||
this.nameInput.focus();
|
||||
});
|
||||
|
||||
this.componentDidUpdate();
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
$(this.modal).off('hidden.bs.modal hidden.bs.modal');
|
||||
}
|
||||
|
||||
componentDidUpdate(){
|
||||
if (this.state.showModal){
|
||||
|
||||
$(this.modal).modal('show');
|
||||
|
||||
// Reset to original values
|
||||
this.nameInput.value = this.props.projectName;
|
||||
this.descrInput.value = this.props.projectDescr;
|
||||
}else{
|
||||
$(this.modal).modal('hide');
|
||||
}
|
||||
}
|
||||
|
||||
show(){
|
||||
this.setState({showModal: true, saving: false, error: ""});
|
||||
}
|
||||
|
||||
hide(){
|
||||
this.setState({showModal: false});
|
||||
}
|
||||
|
||||
handleSave(e){
|
||||
e.preventDefault();
|
||||
|
||||
this.setState({saving: true});
|
||||
|
||||
this.props.saveAction({
|
||||
name: this.nameInput.value,
|
||||
descr: this.descrInput.value
|
||||
}).fail(e => {
|
||||
this.setState({error: e.message || e.responseText || e});
|
||||
}).done(() => {
|
||||
this.hide();
|
||||
}).always(() => {
|
||||
this.setState({saving: false});
|
||||
})
|
||||
}
|
||||
|
||||
render(){
|
||||
return (
|
||||
<div ref={(domNode) => { this.modal = domNode; }}
|
||||
className="modal fade edit-project-dialog" tabIndex="-1"
|
||||
data-backdrop="static"
|
||||
>
|
||||
<div className="modal-dialog">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<button type="button" className="close" onClick={this.hide}><span>×</span></button>
|
||||
<h4 className="modal-title">{this.props.title}</h4>
|
||||
</div>
|
||||
<div className="modal-body">
|
||||
<ErrorMessage bind={[this, "error"]} />
|
||||
<form className="form-horizontal" onSubmit={this.handleSave}>
|
||||
<div className="form-group">
|
||||
<label className="col-sm-2 control-label">Name</label>
|
||||
<div className="col-sm-10">
|
||||
<input type="text" className="form-control" ref={(domNode) => { this.nameInput = domNode; }} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="form-group">
|
||||
<label className="col-sm-2 control-label">Description (optional)</label>
|
||||
<div className="col-sm-10">
|
||||
<textarea className="form-control" rows="3" ref={(domNode) => { this.descrInput = domNode; }} />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
<div className="pull-right">
|
||||
<button type="button" className="btn btn-default" onClick={this.hide} disabled={this.state.saving}>Cancel</button>
|
||||
<button type="button" className="btn btn-primary save" onClick={this.handleSave} disabled={this.state.saving}>
|
||||
{this.state.saving ?
|
||||
<span>
|
||||
<i className="fa fa-circle-o-notch fa-spin"></i> {this.props.savingLabel}
|
||||
</span>
|
||||
: <span>
|
||||
<i className={this.props.saveIcon}></i> {this.props.saveLabel}
|
||||
</span>}
|
||||
</button>
|
||||
</div>
|
||||
{this.props.deleteAction ?
|
||||
<div className="text-left">
|
||||
<button className="btn btn-danger" onClick={this.props.deleteAction}><i className="glyphicon glyphicon-trash"></i> Delete</button>
|
||||
</div>
|
||||
: ""}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default EditProjectDialog;
|
||||
|
|
@ -1,14 +1,12 @@
|
|||
import React from 'react';
|
||||
|
||||
class ErrorMessage extends React.Component {
|
||||
static propTypes() {
|
||||
return {
|
||||
bind: React.PropTypes.array.isRequired // two element array,
|
||||
// with first element being the parent element
|
||||
// and the second the error property to display
|
||||
// ex. [this, 'error']
|
||||
};
|
||||
}
|
||||
static propTypes = {
|
||||
bind: React.PropTypes.array.isRequired // two element array,
|
||||
// with first element being the parent element
|
||||
// and the second the error property to display
|
||||
// ex. [this, 'error']
|
||||
};
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
|
|
|||
|
|
@ -20,17 +20,15 @@ class Map extends React.Component {
|
|||
error: ""
|
||||
}
|
||||
|
||||
static propTypes() {
|
||||
return {
|
||||
bounds: React.PropTypes.array,
|
||||
maxzoom: React.PropTypes.integer,
|
||||
minzoom: React.PropTypes.integer,
|
||||
scheme: React.PropTypes.string, // either 'tms' or 'xyz'
|
||||
showBackground: React.PropTypes.boolean,
|
||||
showControls: React.PropTypes.boolean,
|
||||
tileJSON: React.PropTypes.string,
|
||||
url: React.PropTypes.string
|
||||
};
|
||||
static propTypes = {
|
||||
bounds: React.PropTypes.array,
|
||||
maxzoom: React.PropTypes.integer,
|
||||
minzoom: React.PropTypes.integer,
|
||||
scheme: React.PropTypes.string, // either 'tms' or 'xyz'
|
||||
showBackground: React.PropTypes.bool,
|
||||
showControls: React.PropTypes.bool,
|
||||
tileJSON: React.PropTypes.string,
|
||||
url: React.PropTypes.string
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
|
|
|
|||
|
|
@ -15,6 +15,10 @@ class ProjectList extends React.Component {
|
|||
}
|
||||
|
||||
componentDidMount(){
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
refresh(){
|
||||
// Load projects from API
|
||||
this.serverRequest =
|
||||
$.getJSON(this.props.source, json => {
|
||||
|
|
@ -36,6 +40,7 @@ class ProjectList extends React.Component {
|
|||
loading: false
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import TaskList from './TaskList';
|
|||
import EditTaskPanel from './EditTaskPanel';
|
||||
import UploadProgressBar from './UploadProgressBar';
|
||||
import ErrorMessage from './ErrorMessage';
|
||||
import EditProjectDialog from './EditProjectDialog';
|
||||
import Dropzone from '../vendor/dropzone';
|
||||
import csrf from '../django/csrf';
|
||||
import $ from 'jquery';
|
||||
|
|
@ -17,7 +18,8 @@ class ProjectListItem extends React.Component {
|
|||
showTaskList: false,
|
||||
updatingTask: false,
|
||||
upload: this.getDefaultUploadState(),
|
||||
error: ""
|
||||
error: "",
|
||||
numTasks: props.data.tasks.length
|
||||
};
|
||||
|
||||
this.toggleTaskList = this.toggleTaskList.bind(this);
|
||||
|
|
@ -27,6 +29,9 @@ class ProjectListItem extends React.Component {
|
|||
this.handleTaskSaved = this.handleTaskSaved.bind(this);
|
||||
this.viewMap = this.viewMap.bind(this);
|
||||
this.handleDelete = this.handleDelete.bind(this);
|
||||
this.handleEditProject = this.handleEditProject.bind(this);
|
||||
this.updateProject = this.updateProject.bind(this);
|
||||
this.taskDeleted = this.taskDeleted.bind(this);
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
|
|
@ -158,6 +163,7 @@ class ProjectListItem extends React.Component {
|
|||
}else{
|
||||
this.setState({showTaskList: true});
|
||||
}
|
||||
this.setState({numTasks: this.state.numTasks + 1});
|
||||
}).fail(() => {
|
||||
this.setUploadState({error: "Could not update task information. Plese try again."});
|
||||
}).always(() => {
|
||||
|
|
@ -189,6 +195,13 @@ class ProjectListItem extends React.Component {
|
|||
this.resetUploadState();
|
||||
}
|
||||
|
||||
taskDeleted(){
|
||||
this.setState({numTasks: this.state.numTasks - 1});
|
||||
if (this.state.numTasks === 0){
|
||||
this.setState({showTaskList: false});
|
||||
}
|
||||
}
|
||||
|
||||
handleDelete(){
|
||||
this.setState({error: "HI!" + Math.random()});
|
||||
// if (window.confirm("All tasks, images and models associated with this project will be permanently deleted. Are you sure you want to continue?")){
|
||||
|
|
@ -216,6 +229,14 @@ class ProjectListItem extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleEditProject(){
|
||||
this.editProjectDialog.show();
|
||||
}
|
||||
|
||||
updateProject(project){
|
||||
console.log("OK", project);
|
||||
}
|
||||
|
||||
viewMap(){
|
||||
location.href = `/map/?project=${this.props.data.id}`;
|
||||
}
|
||||
|
|
@ -225,6 +246,19 @@ class ProjectListItem extends React.Component {
|
|||
<li className="project-list-item list-group-item"
|
||||
href="javascript:void(0);"
|
||||
ref={this.setRef("dropzone")}>
|
||||
|
||||
<EditProjectDialog
|
||||
ref={(domNode) => { this.editProjectDialog = domNode; }}
|
||||
title="Edit Project"
|
||||
saveLabel="Save Changes"
|
||||
savingLabel="Saving changes..."
|
||||
saveIcon="fa fa-edit"
|
||||
projectName={this.props.data.name}
|
||||
projectDescr={this.props.data.description}
|
||||
saveAction={this.updateProject}
|
||||
deleteAction={this.handleDelete}
|
||||
/>
|
||||
|
||||
<div className="row no-margin">
|
||||
<ErrorMessage bind={[this, 'error']} />
|
||||
<div className="btn-group pull-right">
|
||||
|
|
@ -252,8 +286,6 @@ class ProjectListItem extends React.Component {
|
|||
</button>
|
||||
<ul className="dropdown-menu">
|
||||
<li><a href="javascript:alert('TODO!');"><i className="fa fa-cube"></i> 3D View</a></li>
|
||||
<li className="divider"></li>
|
||||
<li><a href="javascript:void(0);" onClick={this.handleDelete}><i className="glyphicon glyphicon-trash"></i> Delete Project</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
|
|
@ -264,9 +296,17 @@ class ProjectListItem extends React.Component {
|
|||
{this.props.data.description}
|
||||
</div>
|
||||
<div className="row project-links">
|
||||
<i className='fa fa-tasks'>
|
||||
</i> <a href="javascript:void(0);" onClick={this.toggleTaskList}>
|
||||
{(this.state.showTaskList ? 'Hide' : 'Show')} Tasks
|
||||
{this.state.numTasks > 0 ?
|
||||
<span>
|
||||
<i className='fa fa-tasks'>
|
||||
</i> <a href="javascript:void(0);" onClick={this.toggleTaskList}>
|
||||
{this.state.numTasks} Tasks <i className={'fa fa-caret-' + (this.state.showTaskList ? 'down' : 'right')}></i>
|
||||
</a>
|
||||
</span>
|
||||
: ""}
|
||||
|
||||
<i className='fa fa-edit'>
|
||||
</i> <a href="javascript:void(0);" onClick={this.handleEditProject}> Edit
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -293,7 +333,12 @@ class ProjectListItem extends React.Component {
|
|||
<span>Updating task information... <i className="fa fa-refresh fa-spin fa-fw"></i></span>
|
||||
: ""}
|
||||
|
||||
{this.state.showTaskList ? <TaskList ref={this.setRef("taskList")} source={`/api/projects/${this.props.data.id}/tasks/?ordering=-created_at`}/> : ""}
|
||||
{this.state.showTaskList ?
|
||||
<TaskList
|
||||
ref={this.setRef("taskList")}
|
||||
source={`/api/projects/${this.props.data.id}/tasks/?ordering=-created_at`}
|
||||
onDelete={this.taskDeleted}
|
||||
/> : ""}
|
||||
|
||||
</div>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -57,6 +57,7 @@ class TaskList extends React.Component {
|
|||
this.setState({
|
||||
tasks: this.state.tasks.filter(t => t.id !== id)
|
||||
});
|
||||
if (this.props.onDelete) this.props.onDelete(id);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
|
|
|||
|
|
@ -222,7 +222,9 @@ class TaskListItem extends React.Component {
|
|||
const disabled = this.state.actionButtonsDisabled || !!task.pending_action;
|
||||
|
||||
actionButtons = (<div className="action-buttons">
|
||||
<AssetDownloadButtons task={this.state.task} disabled={disabled} />
|
||||
{task.status === statusCodes.COMPLETED ?
|
||||
<AssetDownloadButtons task={this.state.task} disabled={disabled} />
|
||||
: ""}
|
||||
{actionButtons.map(button => {
|
||||
return (
|
||||
<button key={button.label} type="button" className={"btn btn-sm " + button.className} onClick={button.onClick} disabled={disabled}>
|
||||
|
|
|
|||
|
|
@ -13,4 +13,8 @@
|
|||
line-height: 18px;
|
||||
}
|
||||
}
|
||||
|
||||
.add-button{
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
.edit-project-dialog{
|
||||
.fa-spin{
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
margin-right: 4px;
|
||||
}
|
||||
a{
|
||||
margin-right: 8px;
|
||||
margin-right: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -68,7 +68,6 @@
|
|||
{% block page-wrapper %}{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
<script src="{% static 'app/js/vendor/bootstrap.min.js' %}"></script>
|
||||
<script src="{% static 'app/js/vendor/metisMenu.min.js' %}"></script>
|
||||
<script>
|
||||
$(function(){
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ class TestApi(BootTestCase):
|
|||
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Can filter
|
||||
res = client.get('/api/projects/?owner=999')
|
||||
res = client.get('/api/projects/?name=999')
|
||||
self.assertEqual(res.status_code, status.HTTP_200_OK)
|
||||
self.assertTrue(len(res.data["results"]) == 0)
|
||||
|
||||
|
|
@ -71,6 +71,15 @@ class TestApi(BootTestCase):
|
|||
res = client.get('/api/projects/{}/'.format(other_project.id))
|
||||
self.assertEqual(res.status_code, status.HTTP_404_NOT_FOUND)
|
||||
|
||||
# Can create project, but owner cannot be set
|
||||
res = client.post('/api/projects/', {'name': 'test', 'description': 'test descr'})
|
||||
self.assertEqual(res.status_code, status.HTTP_201_CREATED)
|
||||
self.assertTrue(Project.objects.get(pk=res.data['id']).owner.id == user.id)
|
||||
|
||||
# Cannot leave name empty
|
||||
res = client.post('/api/projects/', {'description': 'test descr'})
|
||||
self.assertEqual(res.status_code, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
# Create some tasks
|
||||
task = Task.objects.create(project=project)
|
||||
|
|
|
|||
Ładowanie…
Reference in New Issue