kopia lustrzana https://github.com/OpenDroneMap/WebODM
Working on skeleton
rodzic
500420941d
commit
a9a474ad46
|
@ -0,0 +1,7 @@
|
|||
<h1 align="center">DroneDB</h1>
|
||||
|
||||
Welcome to DroneDB Plugin!
|
||||
|
||||
DroneDB is a WebODM add-on that enables you to import and export your files to DroneDB.
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
from .plugin import *
|
|
@ -0,0 +1,137 @@
|
|||
import importlib
|
||||
import requests
|
||||
import os
|
||||
from os import path
|
||||
|
||||
from app import models, pending_actions
|
||||
from app.plugins.views import TaskView
|
||||
from app.plugins.worker import run_function_async
|
||||
from app.plugins import get_current_plugin
|
||||
|
||||
from worker.celery import app
|
||||
from rest_framework.response import Response
|
||||
from rest_framework import status
|
||||
|
||||
#from .platform_helper import get_all_platforms, get_platform_by_name
|
||||
|
||||
class ImportFolderTaskView(TaskView):
|
||||
def post(self, request, project_pk=None, pk=None):
|
||||
task = self.get_and_check_task(request, pk)
|
||||
|
||||
# Read form data
|
||||
folder_url = request.data.get('selectedFolderUrl', None)
|
||||
platform_name = request.data.get('platform', None)
|
||||
|
||||
# Make sure both values are set
|
||||
if folder_url == None or platform_name == None:
|
||||
return Response({'error': 'Folder URL and platform name must be set.'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Fetch the platform by name
|
||||
platform = get_platform_by_name(platform_name)
|
||||
|
||||
# Make sure that the platform actually exists
|
||||
if platform == None:
|
||||
return Response({'error': 'Failed to find a platform with the name \'{}\''.format(platform_name)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Verify that the folder url is valid
|
||||
if platform.verify_folder_url(folder_url) == None:
|
||||
return Response({'error': 'Invalid URL'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Get the files from the folder
|
||||
files = platform.import_from_folder(folder_url)
|
||||
|
||||
# Update the task with the new information
|
||||
task.console_output += "Importing {} images...\n".format(len(files))
|
||||
task.images_count = len(files)
|
||||
task.pending_action = pending_actions.IMPORT
|
||||
task.save()
|
||||
|
||||
# Associate the folder url with the project and task
|
||||
combined_id = "{}_{}".format(project_pk, pk)
|
||||
get_current_plugin().get_global_data_store().set_string(combined_id, folder_url)
|
||||
|
||||
# Start importing the files in the background
|
||||
serialized = [file.serialize() for file in files]
|
||||
run_function_async(import_files, task.id, serialized)
|
||||
|
||||
return Response({}, status=status.HTTP_200_OK)
|
||||
|
||||
class CheckUrlTaskView(TaskView):
|
||||
def get(self, request, project_pk=None, pk=None):
|
||||
|
||||
# Assert that task exists
|
||||
self.get_and_check_task(request, pk)
|
||||
|
||||
# Check if there is an imported url associated with the project and task
|
||||
combined_id = "{}_{}".format(project_pk, pk)
|
||||
folder_url = get_current_plugin().get_global_data_store().get_string(combined_id, default = None)
|
||||
|
||||
if folder_url == None:
|
||||
return Response({}, status=status.HTTP_200_OK)
|
||||
else:
|
||||
return Response({'folder_url': folder_url}, status=status.HTTP_200_OK)
|
||||
|
||||
class PlatformsVerifyTaskView(TaskView):
|
||||
def get(self, request, platform_name):
|
||||
# Read the form data
|
||||
folder_url = request.GET.get('folderUrl', None)
|
||||
|
||||
# Fetch the platform by name
|
||||
platform = get_platform_by_name(platform_name)
|
||||
|
||||
# Make sure that the platform actually exists
|
||||
if platform == None:
|
||||
return Response({'error': 'Failed to find a platform with the name \'{}\''.format(platform_name)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Verify that the folder url is valid
|
||||
folder = platform.verify_folder_url(folder_url)
|
||||
if folder == None:
|
||||
return Response({'error': 'Invalid URL'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Return the folder
|
||||
return Response({'folder': folder.serialize()}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class PlatformsTaskView(TaskView):
|
||||
def get(self, request):
|
||||
# Fetch and return all platforms
|
||||
platforms = get_all_platforms()
|
||||
return Response({'platforms': [platform.serialize(user = request.user) for platform in platforms]}, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
def import_files(task_id, files):
|
||||
import requests
|
||||
from app import models
|
||||
from app.plugins import logger
|
||||
|
||||
def download_file(task, file):
|
||||
path = task.task_path(file['name'])
|
||||
download_stream = requests.get(file['url'], stream=True, timeout=60)
|
||||
|
||||
with open(path, 'wb') as fd:
|
||||
for chunk in download_stream.iter_content(4096):
|
||||
fd.write(chunk)
|
||||
|
||||
models.ImageUpload.objects.create(task=task, image=path)
|
||||
|
||||
logger.info("Will import {} files".format(len(files)))
|
||||
task = models.Task.objects.get(pk=task_id)
|
||||
task.create_task_directories()
|
||||
task.save()
|
||||
|
||||
try:
|
||||
downloaded_total = 0
|
||||
for file in files:
|
||||
download_file(task, file)
|
||||
task.check_if_canceled()
|
||||
models.Task.objects.filter(pk=task.id).update(upload_progress=(float(downloaded_total) / float(len(files))))
|
||||
downloaded_total += 1
|
||||
|
||||
except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as e:
|
||||
raise NodeServerError(e)
|
||||
|
||||
task.refresh_from_db()
|
||||
task.pending_action = None
|
||||
task.processing_time = 0
|
||||
task.partial = False
|
||||
task.save()
|
|
@ -0,0 +1,66 @@
|
|||
import requests
|
||||
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import render
|
||||
from django.contrib.auth.decorators import login_required
|
||||
|
||||
from app.plugins import logger
|
||||
|
||||
#from .platform_helper import get_all_extended_platforms
|
||||
|
||||
""" class DynamicForm(forms.Form):
|
||||
"""This dynamic form will go through all the extended platforms, and retrieve their fields"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
ds = kwargs.pop('data_store')
|
||||
super(DynamicForm, self).__init__(*args, **kwargs)
|
||||
extended_platforms = get_all_extended_platforms()
|
||||
|
||||
for platform in extended_platforms:
|
||||
for form_field in platform.get_form_fields():
|
||||
django_field = form_field.get_django_field(ds)
|
||||
django_field.group = platform.name
|
||||
self.fields[form_field.field_id] = django_field
|
||||
"""
|
||||
|
||||
def HomeView(plugin):
|
||||
@login_required
|
||||
def view(request):
|
||||
""" ds = plugin.get_user_data_store(request.user)
|
||||
|
||||
# if this is a POST request we need to process the form data
|
||||
if request.method == "POST":
|
||||
form = DynamicForm(request.POST, data_store = ds)
|
||||
if form.is_valid():
|
||||
extended_platforms = get_all_extended_platforms()
|
||||
for platform in extended_platforms:
|
||||
for form_field in platform.get_form_fields():
|
||||
form_field.save_value(ds, form)
|
||||
|
||||
messages.success(request, "Configuration updated successfuly!")
|
||||
else:
|
||||
form = DynamicForm(data_store = ds) """
|
||||
|
||||
return render(
|
||||
request,
|
||||
plugin.template_path("app.html"),
|
||||
{"title": "DroneDB"},
|
||||
)
|
||||
|
||||
return view
|
||||
|
||||
|
||||
def LoadButtonsView(plugin):
|
||||
def view(request):
|
||||
|
||||
return render(
|
||||
request,
|
||||
plugin.template_path("load_buttons.js"),
|
||||
{
|
||||
"api_url": "/api" + plugin.public_url("").rstrip("/"),
|
||||
},
|
||||
content_type="text/javascript",
|
||||
)
|
||||
|
||||
return view
|
|
@ -0,0 +1,40 @@
|
|||
from app.plugins import PluginBase, Menu, MountPoint, logger
|
||||
|
||||
from .api_views import PlatformsTaskView, PlatformsVerifyTaskView, ImportFolderTaskView, CheckUrlTaskView
|
||||
from .app_views import HomeView, LoadButtonsView
|
||||
#from .platform_helper import get_all_extended_platforms
|
||||
|
||||
|
||||
class Plugin(PluginBase):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def main_menu(self):
|
||||
return [Menu("DroneDB", self.public_url(""), "fa-cloud-download-alt fa fa-fw")]
|
||||
|
||||
def include_js_files(self):
|
||||
return ["load_buttons.js"]
|
||||
|
||||
def include_css_files(self):
|
||||
return ["build/ImportView.css", "build/TaskView.css"]
|
||||
|
||||
def build_jsx_components(self):
|
||||
return ["ImportView.jsx", "TaskView.jsx"]
|
||||
|
||||
""" def api_mount_points(self):
|
||||
#api_views = [api_view for platform in get_all_extended_platforms() for api_view in platform.get_api_views()]
|
||||
# mount_points = [MountPoint(path, view) for (path, view) in api_views]
|
||||
# Add mount points for each extended platform that might require us to do so
|
||||
|
||||
return mount_points + [
|
||||
MountPoint("projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/import", ImportFolderTaskView.as_view()),
|
||||
MountPoint("projects/(?P<project_pk>[^/.]+)/tasks/(?P<pk>[^/.]+)/checkforurl", CheckUrlTaskView.as_view()),
|
||||
MountPoint("platforms/(?P<platform_name>[^/.]+)/verify", PlatformsVerifyTaskView.as_view()),
|
||||
MountPoint("platforms", PlatformsTaskView.as_view()),
|
||||
] """
|
||||
|
||||
def app_mount_points(self):
|
||||
return [
|
||||
MountPoint("$", HomeView(self)),
|
||||
MountPoint("load_buttons.js$", LoadButtonsView(self)),
|
||||
]
|
|
@ -0,0 +1,128 @@
|
|||
import React, { Component, Fragment } from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import ResizeModes from 'webodm/classes/ResizeModes';
|
||||
|
||||
import PlatformSelectButton from "./components/PlatformSelectButton";
|
||||
import PlatformDialog from "./components/PlatformDialog";
|
||||
import LibraryDialog from "./components/LibraryDialog";
|
||||
import ErrorDialog from "./components/ErrorDialog";
|
||||
import ConfigureNewTaskDialog from "./components/ConfigureNewTaskDialog";
|
||||
|
||||
export default class TaskView extends Component {
|
||||
static propTypes = {
|
||||
projectId: PropTypes.number.isRequired,
|
||||
apiURL: PropTypes.string.isRequired,
|
||||
onNewTaskAdded: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
state = {
|
||||
error: "",
|
||||
currentPlatform: null,
|
||||
selectedFolder: null,
|
||||
platforms: [],
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
$.getJSON(`${this.props.apiURL}/platforms/`)
|
||||
.done(data => {
|
||||
this.setState({platforms: data.platforms});
|
||||
})
|
||||
.fail(() => {
|
||||
this.onErrorInDialog("Failed to find available platforms")
|
||||
})
|
||||
}
|
||||
|
||||
onSelectPlatform = platform => this.setState({ currentPlatform: platform });
|
||||
onSelectFolder = folder => this.setState({ selectedFolder: folder });
|
||||
onHideDialog = () => this.setState({ currentPlatform: null, selectedFolder: null, taskId: null });
|
||||
|
||||
onSaveTask = taskInfo => {
|
||||
// Create task
|
||||
const formData = {
|
||||
name: taskInfo.name,
|
||||
options: taskInfo.options,
|
||||
processing_node: taskInfo.selectedNode.id,
|
||||
auto_processing_node: taskInfo.selectedNode.key == "auto",
|
||||
partial: true
|
||||
};
|
||||
|
||||
if (taskInfo.resizeMode === ResizeModes.YES){
|
||||
formData.resize_to = taskInfo.resizeSize;
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: `/api/projects/${this.props.projectId}/tasks/`,
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(formData),
|
||||
dataType: 'json',
|
||||
type: 'POST'
|
||||
}).done((task) => {
|
||||
$.ajax({
|
||||
url: `${this.props.apiURL}/projects/${this.props.projectId}/tasks/${task.id}/import`,
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify({platform: this.state.currentPlatform.name, selectedFolderUrl: this.state.selectedFolder.url}),
|
||||
dataType: 'json',
|
||||
type: 'POST'
|
||||
}).done(() => {
|
||||
this.onHideDialog();
|
||||
this.props.onNewTaskAdded();
|
||||
}).fail(error => {
|
||||
this.onErrorInDialog("Failed to start importing.");
|
||||
});
|
||||
}).fail(() => {
|
||||
this.onErrorInDialog("Cannot create new task. Please try again later.");
|
||||
});
|
||||
}
|
||||
|
||||
onErrorInDialog = msg => {
|
||||
this.setState({ error: msg });
|
||||
this.onHideDialog();
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
currentPlatform,
|
||||
error,
|
||||
selectedFolder,
|
||||
platforms,
|
||||
} = this.state;
|
||||
return (
|
||||
<Fragment>
|
||||
{error ?
|
||||
<ErrorDialog errorMessage={error} />
|
||||
: ""}
|
||||
<PlatformSelectButton
|
||||
platforms={platforms}
|
||||
onSelect={this.onSelectPlatform}
|
||||
/>
|
||||
{selectedFolder === null ?
|
||||
<Fragment>
|
||||
<PlatformDialog
|
||||
show={selectedFolder === null}
|
||||
platform={currentPlatform}
|
||||
apiURL={this.props.apiURL}
|
||||
onHide={this.onHideDialog}
|
||||
onSubmit={this.onSelectFolder}
|
||||
/>
|
||||
<LibraryDialog
|
||||
show={selectedFolder === null}
|
||||
platform={currentPlatform}
|
||||
apiURL={this.props.apiURL}
|
||||
onHide={this.onHideDialog}
|
||||
onSubmit={this.onSelectFolder}
|
||||
/>
|
||||
</Fragment>
|
||||
:
|
||||
<ConfigureNewTaskDialog
|
||||
show={selectedFolder !== null}
|
||||
folder={selectedFolder}
|
||||
platform={currentPlatform}
|
||||
onHide={this.onHideDialog}
|
||||
onSaveTask={this.onSaveTask}
|
||||
/>
|
||||
}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import React, { Component, Fragment } from "react";
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import GoToFolderButton from "./components/GoToFolderButton";
|
||||
import $ from "jquery"; // Fixes a AMD module definition error due to webpack
|
||||
|
||||
export default class TaskView extends Component {
|
||||
static propTypes = {
|
||||
folderUrl: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
folderUrl,
|
||||
} = this.props;
|
||||
return (
|
||||
<Fragment>
|
||||
{folderUrl ?
|
||||
<GoToFolderButton
|
||||
folderUrl={folderUrl}
|
||||
/>
|
||||
: ""}
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { Component } from "react";
|
||||
import { Modal } from "react-bootstrap";
|
||||
import NewTaskPanel from "webodm/components/NewTaskPanel";
|
||||
|
||||
import "./ConfigureNewTaskDialog.scss";
|
||||
|
||||
export default class ConfigureNewTaskDialog extends Component {
|
||||
static defaultProps = {
|
||||
platform: null,
|
||||
};
|
||||
static propTypes = {
|
||||
onHide: PropTypes.func.isRequired,
|
||||
onSaveTask: PropTypes.func.isRequired,
|
||||
folder: PropTypes.object,
|
||||
platform: PropTypes.object,
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
onHide,
|
||||
onSaveTask,
|
||||
platform,
|
||||
folder,
|
||||
} = this.props;
|
||||
|
||||
const title = "Import from " + (platform !== null ? platform.name : "Platform");
|
||||
return (
|
||||
<Modal className={"new-task"} onHide={onHide} show={true}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
{title}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<NewTaskPanel
|
||||
onSave={onSaveTask}
|
||||
onCancel={onHide}
|
||||
filesCount={folder ? folder.images_count : 0}
|
||||
getFiles={() => []}
|
||||
showResize={false}
|
||||
suggestedTaskName={folder ? folder.name : null}
|
||||
/>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.modal-backdrop {
|
||||
z-index: 100000 !important;
|
||||
}
|
||||
|
||||
.new-task.modal button i {
|
||||
margin-right: 1em;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { Component } from "react";
|
||||
import { Modal } from "react-bootstrap";
|
||||
import "./ErrorDialog.scss";
|
||||
|
||||
export default class ErrorDialog extends Component {
|
||||
static propTypes = {
|
||||
errorMessage: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = { show: true };
|
||||
}
|
||||
|
||||
handleOnHide = () => this.setState({show: false});
|
||||
|
||||
render() {
|
||||
const { errorMessage } = this.props;
|
||||
|
||||
return (
|
||||
<Modal show={this.state.show}>
|
||||
<Modal.Header closeButton onHide={this.handleOnHide}>
|
||||
<Modal.Title>
|
||||
There was an error with Cloud Import :(
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
{ errorMessage }
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.modal-backdrop {
|
||||
z-index: 100000 !important;
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
import React, { Component, Fragment } from "react";
|
||||
|
||||
import { Button } from "react-bootstrap";
|
||||
|
||||
import "./GoToFolderButton.scss";
|
||||
|
||||
export default class GoToFolderButton extends Component {
|
||||
static defaultProps = {
|
||||
folderUrl: null,
|
||||
};
|
||||
|
||||
handleClick = () => window.open(this.props.folderUrl, '_blank');;
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Button
|
||||
bsStyle={"primary"}
|
||||
bsSize={"small"}
|
||||
onClick={this.handleClick}
|
||||
>
|
||||
<i className={"fa fa-folder icon"} />
|
||||
Go To Import Folder
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.icon{
|
||||
margin-right: 0.5em;
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { Component } from "react";
|
||||
import { Modal, Button } from "react-bootstrap";
|
||||
import Select from 'react-select';
|
||||
import "./LibraryDialog.scss";
|
||||
|
||||
export default class LibraryDialog extends Component {
|
||||
static defaultProps = {
|
||||
platform: null,
|
||||
};
|
||||
static propTypes = {
|
||||
onHide: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
platform: PropTypes.object,
|
||||
apiURL: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
availableFolders: [],
|
||||
selectedFolder: null,
|
||||
loadingFolders: true,
|
||||
error: "",
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(){
|
||||
if (this.props.platform !== null && this.props.platform.type == "library" && this.state.loadingFolders){
|
||||
$.get(`${this.props.apiURL}/cloudlibrary/${this.props.platform.name}/listfolders`)
|
||||
.done(result => {
|
||||
result.folders.forEach(album => {
|
||||
album.label = `${album.name} (${album.images_count} images)`;
|
||||
album.value = album.url;
|
||||
})
|
||||
this.setState({availableFolders: result.folders});
|
||||
})
|
||||
.fail((error) => {
|
||||
this.setState({loadingFolders: false, error: "Cannot load folders. Check your internet connection."});
|
||||
})
|
||||
.always(() => {
|
||||
this.setState({loadingFolders: false});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleSelectFolder = (e) => this.setState({selectedFolder: e});
|
||||
handleSubmit = e => this.props.onSubmit(this.state.selectedFolder);
|
||||
|
||||
render() {
|
||||
const {
|
||||
onHide,
|
||||
platform,
|
||||
} = this.props;
|
||||
|
||||
const title = "Import from " + (platform !== null ? platform.name : "Platform");
|
||||
const isVisible = platform !== null && platform.type === "library";
|
||||
return (
|
||||
<Modal className={"folder-select"} onHide={onHide} show={isVisible}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
{title}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body bsClass="my-modal">
|
||||
<p>Import the images from your <a target="_blank" href={platform === null ? "" : platform.server_url}>{platform === null ? "" : platform.name} library</a> into a new task.</p>
|
||||
<Select
|
||||
className="basic-single"
|
||||
classNamePrefix="select"
|
||||
isLoading={this.state.loadingFolders}
|
||||
isClearable={false}
|
||||
isSearchable={true}
|
||||
onChange={this.handleSelectFolder}
|
||||
options={this.state.availableFolders}
|
||||
placeholder={this.state.loadingFolders ? "Fetching Piwigo albums..." : "Please select a Piwigo album"}
|
||||
name="options"
|
||||
/>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={onHide}>Close</Button>
|
||||
<Button
|
||||
bsStyle="primary"
|
||||
disabled={this.state.selectedFolder === null}
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
<i className={"fa fa-upload"} />
|
||||
Import
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
.modal-backdrop {
|
||||
z-index: 100000 !important;
|
||||
}
|
||||
|
||||
.folder-select.modal button i {
|
||||
margin-right: 1em;
|
||||
}
|
||||
|
||||
.my-modal {
|
||||
max-height: calc(100vh - 220px);
|
||||
position: relative;
|
||||
padding: 20px;
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
import PropTypes from 'prop-types';
|
||||
import { Component } from "react";
|
||||
import { Modal, Button, FormGroup, ControlLabel, FormControl, HelpBlock } from "react-bootstrap";
|
||||
import "./PlatformDialog.scss";
|
||||
|
||||
export default class PlatformDialog extends Component {
|
||||
static defaultProps = {
|
||||
platform: null,
|
||||
};
|
||||
static propTypes = {
|
||||
onHide: PropTypes.func.isRequired,
|
||||
onSubmit: PropTypes.func.isRequired,
|
||||
platform: PropTypes.object,
|
||||
apiURL: PropTypes.string.isRequired,
|
||||
}
|
||||
|
||||
constructor(props){
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
folderUrl: "",
|
||||
fetchedFolder: null,
|
||||
fetching: false,
|
||||
};
|
||||
}
|
||||
|
||||
handleChange = e => {
|
||||
this.setState({folderUrl: e.target.value});
|
||||
this.verifyFolderUrl(e.target.value);
|
||||
}
|
||||
handleSubmit = e => this.props.onSubmit(this.state.fetchedFolder);
|
||||
|
||||
verifyFolderUrl = (folderUrl) => {
|
||||
if (this.props.platform == null) {
|
||||
this.setState({fetching: false, fetchedFolder: null});
|
||||
} else {
|
||||
this.setState({fetching: true});
|
||||
$.getJSON(`${this.props.apiURL}/platforms/${this.props.platform.name}/verify`, {folderUrl: folderUrl})
|
||||
.done(data => this.setState({fetching: false, fetchedFolder: data.folder}))
|
||||
.fail(() => this.setState({fetching: false, fetchedFolder: null}))
|
||||
}
|
||||
}
|
||||
|
||||
getValidationState = () => {
|
||||
if (this.state.folderUrl === "" || this.state.fetching === true) {
|
||||
return null;
|
||||
} else if (this.state.fetchedFolder == null){
|
||||
return "error";
|
||||
} else {
|
||||
return "success";
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
onHide,
|
||||
platform,
|
||||
} = this.props;
|
||||
|
||||
const title = "Import from " + (platform !== null ? platform.name : "Platform");
|
||||
const isVisible = platform !== null && platform.type === "platform";
|
||||
return (
|
||||
<Modal className={"folder-select"} onHide={onHide} show={isVisible}>
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>
|
||||
{title}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<form>
|
||||
<FormGroup controlId="folderUrl" validationState={this.getValidationState()}>
|
||||
<ControlLabel>Folder/Album URL</ControlLabel>
|
||||
<FormControl
|
||||
type="url"
|
||||
placeholder={platform !== null ? platform.folder_url_example : "Enter a folder url"}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
<FormControl.Feedback />
|
||||
<HelpBlock>Please enter a valid folder/album URL. We will import all images from that folder.</HelpBlock>
|
||||
</FormGroup>
|
||||
</form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button onClick={onHide}>Close</Button>
|
||||
<Button
|
||||
bsStyle="primary"
|
||||
disabled={this.state.fetchedFolder === null}
|
||||
onClick={this.handleSubmit}
|
||||
>
|
||||
<i className={"fa fa-upload"} />
|
||||
Import
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.modal-backdrop {
|
||||
z-index: 100000 !important;
|
||||
}
|
||||
|
||||
.folder-select.modal button i {
|
||||
margin-right: 1em;
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import React, { PureComponent, Fragment } from "react";
|
||||
|
||||
import { DropdownButton, MenuItem } from "react-bootstrap";
|
||||
|
||||
import "./PlatformSelectButton.scss";
|
||||
|
||||
export default class PlatformSelectButton extends PureComponent {
|
||||
static defaultProps = {
|
||||
platforms: [],
|
||||
onSelect: () => {}
|
||||
};
|
||||
|
||||
handleClick = platform => () => this.props.onSelect(platform);
|
||||
|
||||
render() {
|
||||
const {
|
||||
platforms,
|
||||
onSelect,
|
||||
} = this.props;
|
||||
|
||||
const menuItems = platforms
|
||||
.map(platform => (
|
||||
<MenuItem
|
||||
key={platform.name}
|
||||
tag={"a"}
|
||||
onClick={this.handleClick(platform)}
|
||||
>
|
||||
<Fragment>
|
||||
{" "}
|
||||
{platform.name}
|
||||
</Fragment>
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
const title = (
|
||||
<Fragment>
|
||||
<i className={"fa fa-cloud-download-alt fa-cloud-import"} />
|
||||
Cloud Import
|
||||
</Fragment>
|
||||
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownButton
|
||||
id={"platformsDropdown"}
|
||||
bsStyle={"default"}
|
||||
bsSize={"small"}
|
||||
className={"platform-btn"}
|
||||
title={title}
|
||||
>
|
||||
{menuItems}
|
||||
</DropdownButton>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
.platform-btn {
|
||||
.fa-cloud-import {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
.caret {
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.platforms-dropdowns {
|
||||
display: inline-block;
|
||||
|
||||
.dropdown {
|
||||
float: none;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"react-bootstrap": "^0.32.4",
|
||||
"react-select": "^3.0.4"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
{% extends "app/plugins/templates/base.html" %}
|
||||
{% load bootstrap_extras %}
|
||||
{% block content %}
|
||||
<h3>Welcome to Cloud Import!</h3>
|
||||
<h5><strong>Instructions</strong></h5>
|
||||
On this screen, you will be able to configure everything that is necessary for your different platforms.<BR/>
|
||||
You might need to set up a server URL, an authentication token or something else. If so, this is the place!
|
||||
<BR/>
|
||||
<BR/>
|
||||
<h3>Platforms</h3>
|
||||
<form action="" method="post" class="oam-form">
|
||||
{% csrf_token %}
|
||||
{% regroup form by field.group as field_groups %}
|
||||
{% for field_group in field_groups %}
|
||||
<h4><strong>{{field_group.grouper}}</strong></h4>
|
||||
{% for field in field_group.list %}
|
||||
<div class="form-group {% if field.errors %}has-error{% endif %}">
|
||||
<label for="{{ field.id_for_label }}" class="control-label">{{ field.label }}</label>
|
||||
{{ field|with_class:'form-control' }}
|
||||
{% if field.errors %}
|
||||
<span class='text-danger'>{{ field.errors|join:'<br />' }}</span>
|
||||
{% elif field.help_text %}
|
||||
<span class="help-block ">{{ field.help_text }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
|
||||
<button type="submit" class="btn btn-primary"><i class="fa fa-save fa-fw"></i> Save Configuration</button>
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -0,0 +1,30 @@
|
|||
PluginsAPI.Dashboard.addNewTaskButton(
|
||||
["cloudimport/build/ImportView.js"],
|
||||
function(args, ImportView) {
|
||||
return React.createElement(ImportView, {
|
||||
onNewTaskAdded: args.onNewTaskAdded,
|
||||
projectId: args.projectId,
|
||||
apiURL: "{{ api_url }}",
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
PluginsAPI.Dashboard.addTaskActionButton(
|
||||
["cloudimport/build/TaskView.js", "cloudimport/build/TaskView.css"],
|
||||
function(args, TaskView) {
|
||||
var reactElement;
|
||||
$.ajax({
|
||||
url: "{{ api_url }}/projects/" + args.task.project + "/tasks/" + args.task.id + "/checkforurl",
|
||||
dataType: 'json',
|
||||
async: false,
|
||||
success: function(data) {
|
||||
if (data.folder_url) {
|
||||
reactElement = React.createElement(TaskView, {
|
||||
folderUrl: data.folder_url,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return reactElement;
|
||||
}
|
||||
);
|
Ładowanie…
Reference in New Issue