CSS improvements, edit project modal dialog, better project display UI, create new projects

pull/50/head
Piero Toffanin 2016-11-14 16:32:05 -05:00
rodzic 9cf6a7222a
commit 7c0fc4ffd5
18 zmienionych plików z 320 dodań i 46 usunięć

Wyświetl plik

@ -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/&lt;projectId&gt;/tasks : list all tasks belonging to a project<br/>
- /api/projects/&lt;projectId&gt;/tasks/&lt;taskId&gt; : 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.

Wyświetl plik

@ -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;

Wyświetl plik

@ -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>
);
}
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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();

Wyświetl plik

@ -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>&times;</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;

Wyświetl plik

@ -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);

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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(){

Wyświetl plik

@ -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>

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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}>

Wyświetl plik

@ -13,4 +13,8 @@
line-height: 18px;
}
}
.add-button{
margin-bottom: 8px;
}
}

Wyświetl plik

@ -0,0 +1,5 @@
.edit-project-dialog{
.fa-spin{
margin-right: 4px;
}
}

Wyświetl plik

@ -44,7 +44,7 @@
margin-right: 4px;
}
a{
margin-right: 8px;
margin-right: 12px;
}
}

Wyświetl plik

@ -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(){

Wyświetl plik

@ -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)