OpenDroneMap-WebODM/app/static/app/js/components/ProjectListItem.jsx

791 wiersze
27 KiB
React
Czysty Zwykły widok Historia

import '../css/ProjectListItem.scss';
import React from 'react';
import update from 'immutability-helper';
2016-10-25 14:47:49 +00:00
import TaskList from './TaskList';
import NewTaskPanel from './NewTaskPanel';
2019-02-20 21:42:20 +00:00
import ImportTaskPanel from './ImportTaskPanel';
import UploadProgressBar from './UploadProgressBar';
import ErrorMessage from './ErrorMessage';
import EditProjectDialog from './EditProjectDialog';
2023-02-22 16:56:38 +00:00
import SortPanel from './SortPanel';
import Dropzone from '../vendor/dropzone';
import csrf from '../django/csrf';
import HistoryNav from '../classes/HistoryNav';
2017-09-06 15:47:04 +00:00
import PropTypes from 'prop-types';
2019-06-27 15:29:46 +00:00
import ResizeModes from '../classes/ResizeModes';
2023-03-07 18:24:26 +00:00
import Tags from '../classes/Tags';
2020-12-22 16:21:10 +00:00
import exifr from '../vendor/exifr';
import { _, interpolate } from '../classes/gettext';
import $ from 'jquery';
class ProjectListItem extends React.Component {
2017-09-06 15:47:04 +00:00
static propTypes = {
history: PropTypes.object.isRequired,
data: PropTypes.object.isRequired, // project json
2021-08-04 20:20:51 +00:00
onDelete: PropTypes.func,
onTaskMoved: PropTypes.func,
2021-08-06 15:33:43 +00:00
onProjectDuplicated: PropTypes.func
2017-09-06 15:47:04 +00:00
}
2016-10-11 20:37:00 +00:00
constructor(props){
super(props);
this.historyNav = new HistoryNav(props.history);
this.state = {
showTaskList: this.historyNav.isValueInQSList("project_task_open", props.data.id),
upload: this.getDefaultUploadState(),
error: "",
2016-11-15 16:51:19 +00:00
data: props.data,
2019-02-20 21:42:20 +00:00
refreshing: false,
2019-08-29 02:16:39 +00:00
importing: false,
2023-02-22 16:56:38 +00:00
buttons: [],
2023-03-13 19:15:02 +00:00
sortKey: "-created_at",
2023-03-14 15:10:38 +00:00
filterTags: [],
selectedTags: [],
filterText: ""
};
2023-02-22 16:56:38 +00:00
this.sortItems = [{
key: "created_at",
2023-03-08 19:40:35 +00:00
label: _("Created on")
2023-02-22 16:56:38 +00:00
},{
key: "name",
label: _("Name")
},{
key: "tags",
label: _("Tags")
2023-02-22 16:56:38 +00:00
}];
2016-10-25 14:47:49 +00:00
this.toggleTaskList = this.toggleTaskList.bind(this);
2016-10-18 15:25:14 +00:00
this.closeUploadError = this.closeUploadError.bind(this);
this.cancelUpload = this.cancelUpload.bind(this);
2023-11-06 15:35:02 +00:00
this.handleCancel = this.handleCancel.bind(this);
2016-10-21 14:42:46 +00:00
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);
this.taskMoved = this.taskMoved.bind(this);
this.hasPermission = this.hasPermission.bind(this);
}
2016-11-15 16:51:19 +00:00
refresh(){
// Update project information based on server
this.setState({refreshing: true});
this.refreshRequest =
$.getJSON(`/api/projects/${this.state.data.id}/`)
.done((json) => {
this.setState({data: json});
})
.fail((_, __, e) => {
this.setState({error: e.message});
})
.always(() => {
this.setState({refreshing: false});
});
}
componentWillUnmount(){
if (this.deleteProjectRequest) this.deleteProjectRequest.abort();
2016-11-15 16:51:19 +00:00
if (this.refreshRequest) this.refreshRequest.abort();
}
2023-03-14 15:10:38 +00:00
componentDidUpdate(prevProps, prevState){
if (prevState.filterText !== this.state.filterText ||
prevState.selectedTags.length !== this.state.selectedTags.length){
if (this.taskList) this.taskList.applyFilter(this.state.filterText, this.state.selectedTags);
}
}
getDefaultUploadState(){
return {
uploading: false,
editing: false,
2016-10-18 15:25:14 +00:00
error: "",
progress: 0,
files: [],
totalCount: 0,
uploadedCount: 0,
totalBytes: 0,
totalBytesSent: 0,
lastUpdated: 0
};
}
resetUploadState(){
this.setUploadState(this.getDefaultUploadState());
}
setUploadState(props){
this.setState(update(this.state, {
upload: {
$merge: props
}
}));
}
hasPermission(perm){
return this.state.data.permissions.indexOf(perm) !== -1;
}
componentDidMount(){
Dropzone.autoDiscover = false;
if (this.hasPermission("add")){
this.dz = new Dropzone(this.dropzone, {
paramName: "images",
url : 'TO_BE_CHANGED',
2019-06-27 15:29:46 +00:00
parallelUploads: 6,
uploadMultiple: false,
2023-01-24 16:10:14 +00:00
acceptedFiles: "image/*,text/*,.las,.laz,video/*,.srt",
autoProcessQueue: false,
createImageThumbnails: false,
clickable: this.uploadButton,
2023-06-19 06:24:14 +00:00
maxFilesize: 131072, // 128G
chunkSize: 2147483647,
timeout: 2147483647,
headers: {
[csrf.header]: csrf.token
}
});
2016-10-21 14:42:46 +00:00
this.dz.on("addedfiles", files => {
let totalBytes = 0;
for (let i = 0; i < files.length; i++){
totalBytes += files[i].size;
files[i].deltaBytesSent = 0;
files[i].trackedBytesSent = 0;
files[i].retries = 0;
}
this.setUploadState({
editing: true,
totalCount: this.state.upload.totalCount + files.length,
files,
totalBytes: this.state.upload.totalBytes + totalBytes
});
})
.on("uploadprogress", (file, progress, bytesSent) => {
const now = new Date().getTime();
if (bytesSent > file.size) bytesSent = file.size;
if (progress === 100 || now - this.state.upload.lastUpdated > 500){
const deltaBytesSent = bytesSent - file.deltaBytesSent;
file.trackedBytesSent += deltaBytesSent;
const totalBytesSent = this.state.upload.totalBytesSent + deltaBytesSent;
const progress = totalBytesSent / this.state.upload.totalBytes * 100;
this.setUploadState({
progress,
totalBytesSent,
lastUpdated: now
});
file.deltaBytesSent = bytesSent;
}
})
.on("complete", (file) => {
// Retry
const retry = () => {
const MAX_RETRIES = 10;
if (file.retries < MAX_RETRIES){
// Update progress
const totalBytesSent = this.state.upload.totalBytesSent - file.trackedBytesSent;
const progress = totalBytesSent / this.state.upload.totalBytes * 100;
this.setUploadState({
progress,
totalBytesSent,
});
file.status = Dropzone.QUEUED;
file.deltaBytesSent = 0;
file.trackedBytesSent = 0;
file.retries++;
2023-12-21 14:00:12 +00:00
setTimeout(() => {
this.dz.processQueue();
}, 5000 * file.retries);
}else{
throw new Error(interpolate(_('Cannot upload %(filename)s, exceeded max retries (%(max_retries)s)'), {filename: file.name, max_retries: MAX_RETRIES}));
}
};
try{
if (file.status === "error"){
2023-06-19 06:24:14 +00:00
if ((file.size / 1024) > this.dz.options.maxFilesize) {
// Delete from upload queue
this.setUploadState({
totalCount: this.state.upload.totalCount - 1,
totalBytes: this.state.upload.totalBytes - file.size
});
throw new Error(interpolate(_('Cannot upload %(filename)s, file is too large! Default MaxFileSize is %(maxFileSize)s MB!'), { filename: file.name, maxFileSize: this.dz.options.maxFilesize }));
2023-06-19 06:26:18 +00:00
}
retry();
}else{
// Check response
let response = JSON.parse(file.xhr.response);
if (response.success){
// Update progress by removing the tracked progress and
// use the file size as the true number of bytes
let totalBytesSent = this.state.upload.totalBytesSent + file.size;
if (file.trackedBytesSent) totalBytesSent -= file.trackedBytesSent;
const progress = totalBytesSent / this.state.upload.totalBytes * 100;
this.setUploadState({
progress,
totalBytesSent,
uploadedCount: this.state.upload.uploadedCount + 1
});
this.dz.processQueue();
}else{
retry();
}
}
}catch(e){
2023-11-06 15:35:02 +00:00
if (this.manuallyCanceled){
// Manually canceled, ignore error
this.setUploadState({uploading: false});
}else{
this.setUploadState({error: `${e.message}`, uploading: false});
}
if (this.dz.files.length) this.dz.cancelUpload();
}
})
.on("queuecomplete", () => {
const remainingFilesCount = this.state.upload.totalCount - this.state.upload.uploadedCount;
2023-11-06 15:35:02 +00:00
if (remainingFilesCount === 0 && this.state.upload.uploadedCount > 0){
// All files have uploaded!
this.setUploadState({uploading: false});
$.ajax({
url: `/api/projects/${this.state.data.id}/tasks/${this.dz._taskInfo.id}/commit/`,
contentType: 'application/json',
dataType: 'json',
type: 'POST'
}).done((task) => {
if (task && task.id){
this.newTaskAdded();
}else{
this.setUploadState({error: interpolate(_('Cannot create new task. Invalid response from server: %(error)s'), { error: JSON.stringify(task) }) });
}
}).fail(() => {
this.setUploadState({error: _("Cannot create new task. Please try again later.")});
});
}else if (this.dz.getQueuedFiles() === 0){
// Done but didn't upload all?
this.setUploadState({
totalCount: this.state.upload.totalCount - remainingFilesCount,
uploading: false,
error: interpolate(_('%(count)s files cannot be uploaded. As a reminder, only images (.jpg, .tif, .png) and GCP files (.txt) can be uploaded. Try again.'), { count: remainingFilesCount })
});
}
})
.on("reset", () => {
this.resetUploadState();
})
.on("dragenter", () => {
if (!this.state.upload.editing){
this.resetUploadState();
}
});
}
2019-08-29 02:16:39 +00:00
PluginsAPI.Dashboard.triggerAddNewTaskButton({projectId: this.state.data.id, onNewTaskAdded: this.newTaskAdded}, (button) => {
if (!button) return;
2019-09-07 18:35:30 +00:00
2019-08-29 02:16:39 +00:00
this.setState(update(this.state, {
buttons: {$push: [button]}
}));
});
}
2019-02-21 19:16:48 +00:00
newTaskAdded = () => {
this.setState({importing: false});
if (this.state.showTaskList){
this.taskList.refresh();
}else{
this.setState({showTaskList: true});
}
this.resetUploadState();
this.refresh();
}
setRef(prop){
return (domNode) => {
if (domNode != null) this[prop] = domNode;
}
}
2016-10-25 14:47:49 +00:00
toggleTaskList(){
const showTaskList = !this.state.showTaskList;
this.historyNav.toggleQSListItem("project_task_open", this.state.data.id, showTaskList);
2016-10-11 20:37:00 +00:00
this.setState({
showTaskList: showTaskList
2016-10-11 20:37:00 +00:00
});
}
2016-10-18 15:25:14 +00:00
closeUploadError(){
this.setUploadState({error: ""});
}
2023-11-06 15:35:02 +00:00
cancelUpload(){
2016-10-18 15:25:14 +00:00
this.dz.removeAllFiles(true);
}
2023-11-06 15:35:02 +00:00
handleCancel(){
this.manuallyCanceled = true;
this.cancelUpload();
if (this.dz._taskInfo && this.dz._taskInfo.id !== undefined){
$.ajax({
url: `/api/projects/${this.state.data.id}/tasks/${this.dz._taskInfo.id}/remove/`,
contentType: 'application/json',
dataType: 'json',
type: 'POST'
});
}
setTimeout(() => {
this.manuallyCanceled = false;
}, 500);
}
taskDeleted(){
2016-11-15 16:51:19 +00:00
this.refresh();
}
2021-08-04 20:20:51 +00:00
taskMoved(task){
this.refresh();
if (this.props.onTaskMoved) this.props.onTaskMoved(task);
}
handleDelete(){
2016-11-15 16:51:19 +00:00
return $.ajax({
url: `/api/projects/${this.state.data.id}/`,
type: 'DELETE'
}).done(() => {
if (this.props.onDelete) this.props.onDelete(this.state.data.id);
});
}
2016-10-21 14:42:46 +00:00
handleTaskSaved(taskInfo){
this.dz._taskInfo = taskInfo; // Allow us to access the task info from dz
2019-06-27 15:19:52 +00:00
this.setUploadState({uploading: true, editing: false});
// Create task
const formData = {
name: taskInfo.name,
options: taskInfo.options,
processing_node: taskInfo.selectedNode.id,
auto_processing_node: taskInfo.selectedNode.key == "auto",
partial: true
};
2019-06-27 15:29:46 +00:00
if (taskInfo.resizeMode === ResizeModes.YES){
formData.resize_to = taskInfo.resizeSize;
}
$.ajax({
url: `/api/projects/${this.state.data.id}/tasks/`,
contentType: 'application/json',
data: JSON.stringify(formData),
dataType: 'json',
type: 'POST'
}).done((task) => {
if (task && task.id){
this.dz._taskInfo.id = task.id;
this.dz.options.url = `/api/projects/${this.state.data.id}/tasks/${task.id}/upload/`;
this.dz.processQueue();
}else{
this.setState({error: interpolate(_('Cannot create new task. Invalid response from server: %(error)s'), { error: JSON.stringify(task) }) });
this.handleTaskCanceled();
}
}).fail(() => {
this.setState({error: _("Cannot create new task. Please try again later.")});
this.handleTaskCanceled();
});
}
handleTaskCanceled = () => {
this.dz.removeAllFiles(true);
this.resetUploadState();
}
handleUpload = () => {
// Not a second click for adding more files?
if (!this.state.upload.editing){
this.handleTaskCanceled();
}
2016-10-21 14:42:46 +00:00
}
handleEditProject(){
this.editProjectDialog.show();
}
handleHideProject = (deleteWarning, deleteAction) => {
return () => {
if (window.confirm(deleteWarning)){
this.setState({error: "", refreshing: true});
deleteAction()
.fail(e => {
this.setState({error: e.message || (e.responseJSON || {}).detail || e.responseText || _("Could not delete item")});
}).always(() => {
this.setState({refreshing: false});
});
}
}
}
updateProject(project){
2016-11-15 16:51:19 +00:00
return $.ajax({
2021-10-19 16:40:23 +00:00
url: `/api/projects/${this.state.data.id}/edit/`,
2016-11-15 16:51:19 +00:00
contentType: 'application/json',
data: JSON.stringify({
name: project.name,
description: project.descr,
2023-03-07 18:24:26 +00:00
tags: project.tags,
2021-10-19 16:40:23 +00:00
permissions: project.permissions
2016-11-15 16:51:19 +00:00
}),
dataType: 'json',
2021-10-19 16:40:23 +00:00
type: 'POST'
2016-11-15 16:51:19 +00:00
}).done(() => {
this.refresh();
});
}
viewMap(){
2016-11-15 16:51:19 +00:00
location.href = `/map/project/${this.state.data.id}/`;
}
2019-02-20 21:42:20 +00:00
handleImportTask = () => {
this.setState({importing: true});
}
handleCancelImportTask = () => {
this.setState({importing: false});
}
handleTaskTitleHint = () => {
return new Promise((resolve, reject) => {
if (this.state.upload.files.length > 0){
// Find first image in list
let f = null;
for (let i = 0; i < this.state.upload.files.length; i++){
if (this.state.upload.files[i].type.indexOf("image") === 0){
f = this.state.upload.files[i];
break;
}
}
if (!f){
reject();
return;
}
// Parse EXIF
const options = {
ifd0: false,
exif: [0x9003],
gps: [0x0001, 0x0002, 0x0003, 0x0004],
interop: false,
ifd1: false // thumbnail
};
exifr.parse(f, options).then(gps => {
if (!gps.latitude || !gps.longitude){
reject();
return;
}
let dateTime = gps["36867"];
// Try to parse the date from EXIF to JS
const parts = dateTime.split(" ");
if (parts.length == 2){
let [ d, t ] = parts;
2021-01-23 22:11:46 +00:00
d = d.replace(/:/g, "-");
const tm = Date.parse(`${d} ${t}`);
if (!isNaN(tm)){
dateTime = new Date(tm).toLocaleDateString();
}
}
// Fallback to file modified date if
// no exif info is available
if (!dateTime) dateTime = f.lastModifiedDate.toLocaleDateString();
// Query nominatim OSM
$.ajax({
url: `https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${gps.latitude}&lon=${gps.longitude}`,
contentType: 'application/json',
type: 'GET'
}).done(json => {
if (json.name) resolve(`${json.name} - ${dateTime}`);
2020-12-22 16:21:10 +00:00
else if (json.address && json.address.road) resolve(`${json.address.road} - ${dateTime}`);
else reject(new Error("Invalid json"));
}).fail(reject);
}).catch(reject);
}
});
}
2023-02-22 16:56:38 +00:00
sortChanged = key => {
if (this.taskList){
this.setState({sortKey: key});
setTimeout(() => {
this.taskList.refresh();
}, 0);
}
}
2023-03-13 16:28:18 +00:00
handleTagClick = tag => {
return e => {
const evt = new CustomEvent("onProjectListTagClicked", { detail: tag });
document.dispatchEvent(evt);
}
}
2023-03-13 19:15:02 +00:00
tagsChanged = (filterTags) => {
2023-03-14 15:10:38 +00:00
this.setState({filterTags, selectedTags: []});
}
handleFilterTextChange = e => {
this.setState({filterText: e.target.value});
}
toggleTag = t => {
return () => {
if (this.state.selectedTags.indexOf(t) === -1){
this.setState(update(this.state, { selectedTags: {$push: [t]} }));
}else{
this.setState({selectedTags: this.state.selectedTags.filter(tag => tag !== t)});
}
}
}
selectTag = t => {
if (this.state.selectedTags.indexOf(t) === -1){
this.setState(update(this.state, { selectedTags: {$push: [t]} }));
}
}
clearFilter = () => {
this.setState({
filterText: "",
selectedTags: []
});
}
onOpenFilter = () => {
if (this.state.filterTags.length === 0){
setTimeout(() => {
this.filterTextInput.focus();
}, 0);
}
2023-03-13 19:15:02 +00:00
}
2016-10-21 14:42:46 +00:00
render() {
2023-03-13 19:15:02 +00:00
const { refreshing, data, filterTags } = this.state;
2016-11-15 16:51:19 +00:00
const numTasks = data.tasks.length;
const canEdit = this.hasPermission("change");
2023-03-07 18:24:26 +00:00
const userTags = Tags.userTags(data.tags);
2023-03-22 15:35:43 +00:00
let deleteWarning = _("All tasks, images and models associated with this project will be permanently deleted. Are you sure you want to continue?");
if (!data.owned) deleteWarning = _("This project was shared with you. It will not be deleted, but simply hidden from your dashboard. Continue?")
2016-11-15 16:51:19 +00:00
return (
2016-11-15 16:51:19 +00:00
<li className={"project-list-item list-group-item " + (refreshing ? "refreshing" : "")}
2016-11-11 18:22:29 +00:00
href="javascript:void(0);"
ref={this.setRef("dropzone")}
>
{canEdit ?
<EditProjectDialog
ref={(domNode) => { this.editProjectDialog = domNode; }}
title={_("Edit Project")}
saveLabel={_("Save Changes")}
savingLabel={_("Saving changes...")}
saveIcon="far fa-edit"
showDuplicate={true}
onDuplicated={this.props.onProjectDuplicated}
projectName={data.name}
projectDescr={data.description}
projectId={data.id}
2023-03-07 18:24:26 +00:00
projectTags={data.tags}
2023-03-22 15:35:43 +00:00
deleteWarning={deleteWarning}
saveAction={this.updateProject}
showPermissions={this.hasPermission("change")}
deleteAction={this.hasPermission("delete") ? this.handleDelete : undefined}
/>
: ""}
2016-10-18 15:25:14 +00:00
<div className="row no-margin">
<ErrorMessage bind={[this, 'error']} />
2022-02-02 18:46:53 +00:00
<div className="btn-group project-buttons">
{this.hasPermission("add") ?
2019-02-20 21:42:20 +00:00
<div className={"asset-download-buttons btn-group " + (this.state.upload.uploading ? "hide" : "")}>
<button type="button"
className="btn btn-primary btn-sm"
onClick={this.handleUpload}
ref={this.setRef("uploadButton")}>
2019-05-15 19:50:36 +00:00
<i className="glyphicon glyphicon-upload"></i>
{_("Select Images and GCP")}
2019-05-15 19:50:36 +00:00
</button>
<button type="button"
className="btn btn-default btn-sm"
onClick={this.handleImportTask}>
<i className="glyphicon glyphicon-import"></i> {_("Import")}
2019-05-15 19:50:36 +00:00
</button>
2019-09-07 18:35:30 +00:00
{this.state.buttons.map((button, i) => <React.Fragment key={i}>{button}</React.Fragment>)}
2019-05-15 19:50:36 +00:00
</div>
: ""}
2016-10-18 15:25:14 +00:00
<button disabled={this.state.upload.error !== ""}
type="button"
className={"btn btn-danger btn-sm " + (!this.state.upload.uploading ? "hide" : "")}
2023-11-06 15:35:02 +00:00
onClick={this.handleCancel}>
2016-10-18 15:25:14 +00:00
<i className="glyphicon glyphicon-remove-circle"></i>
Cancel Upload
</button>
</div>
2016-10-11 20:37:00 +00:00
2022-02-02 18:46:53 +00:00
<div className="project-name">
2016-11-15 16:51:19 +00:00
{data.name}
2023-03-07 18:24:26 +00:00
{userTags.length > 0 ?
2023-03-13 16:28:18 +00:00
userTags.map((t, i) => <div key={i} className="tag-badge small-badge" onClick={this.handleTagClick(t)}>{t}</div>)
2023-03-07 18:24:26 +00:00
: ""}
2022-02-02 18:46:53 +00:00
</div>
2016-10-25 14:47:49 +00:00
<div className="project-description">
2016-11-15 16:51:19 +00:00
{data.description}
2016-10-25 14:47:49 +00:00
</div>
<div className="row project-links">
2016-11-15 16:51:19 +00:00
{numTasks > 0 ?
<span>
2022-01-28 12:01:51 +00:00
<i className='fa fa-tasks'></i>
2023-02-21 17:06:47 +00:00
<a href="javascript:void(0);" onClick={this.toggleTaskList}>
{interpolate(_("%(count)s Tasks"), { count: numTasks})} <i className={'fa fa-caret-' + (this.state.showTaskList ? 'down' : 'right')}></i>
</a>
</span>
: ""}
2023-02-21 17:06:47 +00:00
{this.state.showTaskList && numTasks > 1 ?
<div className="task-filters">
2023-03-14 15:10:38 +00:00
<div className="btn-group">
{this.state.selectedTags.length || this.state.filterText !== "" ?
<a className="quick-clear-filter" href="javascript:void(0)" onClick={this.clearFilter}>×</a>
: ""}
<i className='fa fa-filter'></i>
<a href="javascript:void(0);" onClick={this.onOpenFilter} className="dropdown-toggle" data-toggle-outside data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
{_("Filter")}
</a>
<ul className="dropdown-menu dropdown-menu-right filter-dropdown">
<li className="filter-text-container">
<input type="text" className="form-control filter-text theme-border-secondary-07"
value={this.state.filterText}
ref={domNode => {this.filterTextInput = domNode}}
placeholder=""
spellCheck="false"
autoComplete="false"
onChange={this.handleFilterTextChange} />
</li>
{filterTags.map(t => <li key={t} className="tag-selection">
<input type="checkbox"
className="filter-checkbox"
id={"filter-tag-" + data.id + "-" + t}
checked={this.state.selectedTags.indexOf(t) !== -1}
onChange={this.toggleTag(t)} /> <label className="filter-checkbox-label" htmlFor={"filter-tag-" + data.id + "-" + t}>{t}</label>
</li>)}
<li className="clear-container"><input type="button" onClick={this.clearFilter} className="btn btn-default btn-xs" value={_("Clear")}/></li>
</ul>
</div>
2023-02-21 17:06:47 +00:00
<div className="btn-group">
<i className='fa fa-sort-alpha-down'></i>
<a href="javascript:void(0);" className="dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
2023-02-22 16:56:38 +00:00
{_("Sort")}
2023-02-21 17:06:47 +00:00
</a>
2023-03-08 19:40:35 +00:00
<SortPanel selected="-created_at" items={this.sortItems} onChange={this.sortChanged} />
2023-02-21 17:06:47 +00:00
</div>
</div> : ""}
2023-03-08 17:21:23 +00:00
{numTasks > 0 ?
[<i key="edit-icon" className='fa fa-globe'></i>
,<a key="edit-text" href="javascript:void(0);" onClick={this.viewMap}>
{_("View Map")}
</a>]
: ""}
{canEdit ?
2022-01-28 12:01:51 +00:00
[<i key="edit-icon" className='far fa-edit'></i>
,<a key="edit-text" href="javascript:void(0);" onClick={this.handleEditProject}> {_("Edit")}
</a>]
: ""}
2023-03-08 17:21:23 +00:00
{!canEdit && !data.owned ?
[<i key="edit-icon" className='far fa-eye-slash'></i>
,<a key="edit-text" href="javascript:void(0);" onClick={this.handleHideProject(deleteWarning, this.handleDelete)}> {_("Delete")}
</a>]
: ""}
2016-10-25 14:47:49 +00:00
</div>
2016-10-18 15:25:14 +00:00
</div>
2016-11-11 18:22:29 +00:00
<i className="drag-drop-icon fa fa-inbox"></i>
2016-10-18 15:25:14 +00:00
<div className="row">
{this.state.upload.uploading ? <UploadProgressBar {...this.state.upload}/> : ""}
2019-06-27 15:19:52 +00:00
2016-10-18 15:25:14 +00:00
{this.state.upload.error !== "" ?
<div className="alert alert-warning alert-dismissible">
2020-12-16 19:37:35 +00:00
<button type="button" className="close" title={_("Close")} onClick={this.closeUploadError}><span aria-hidden="true">&times;</span></button>
2016-10-18 15:25:14 +00:00
{this.state.upload.error}
</div>
2016-10-18 15:25:14 +00:00
: ""}
{this.state.upload.editing ?
<NewTaskPanel
onSave={this.handleTaskSaved}
onCancel={this.handleTaskCanceled}
suggestedTaskName={this.handleTaskTitleHint}
filesCount={this.state.upload.totalCount}
showResize={true}
getFiles={() => this.state.upload.files }
2016-10-21 14:42:46 +00:00
/>
: ""}
2019-02-20 21:42:20 +00:00
{this.state.importing ?
<ImportTaskPanel
2019-02-21 19:16:48 +00:00
onImported={this.newTaskAdded}
2019-02-20 21:42:20 +00:00
onCancel={this.handleCancelImportTask}
projectId={this.state.data.id}
/>
: ""}
{this.state.showTaskList ?
<TaskList
ref={this.setRef("taskList")}
2023-02-22 16:56:38 +00:00
source={`/api/projects/${data.id}/tasks/?ordering=${this.state.sortKey}`}
onDelete={this.taskDeleted}
2021-08-04 20:20:51 +00:00
onTaskMoved={this.taskMoved}
hasPermission={this.hasPermission}
2023-03-13 19:15:02 +00:00
onTagsChanged={this.tagsChanged}
2023-03-14 15:10:38 +00:00
onTagClicked={this.selectTag}
history={this.props.history}
/> : ""}
2016-10-18 15:25:14 +00:00
</div>
2016-10-11 20:37:00 +00:00
</li>
);
}
}
export default ProjectListItem;