Presets interface working, still needs more testing

pull/246/head
Piero Toffanin 2017-07-25 12:37:42 -04:00
rodzic 81871d1149
commit 4acb13829a
5 zmienionych plików z 265 dodań i 53 usunięć

Wyświetl plik

@ -1,6 +1,5 @@
import '../css/EditPresetDialog.scss';
import React from 'react';
import ErrorMessage from './ErrorMessage';
import FormDialog from './FormDialog';
import ProcessingNodeOption from './ProcessingNodeOption';
import PresetUtils from '../classes/PresetUtils';
@ -13,6 +12,8 @@ class EditPresetDialog extends React.Component {
static propTypes = {
preset: React.PropTypes.object.isRequired,
availableOptions: React.PropTypes.array.isRequired,
saveAction: React.PropTypes.func.isRequired,
deleteAction: React.PropTypes.func.isRequired,
onHide: React.PropTypes.func
};
@ -30,7 +31,7 @@ class EditPresetDialog extends React.Component {
this.onShow = this.onShow.bind(this);
this.setOptionRef = this.setOptionRef.bind(this);
this.getOptions = this.getOptions.bind(this);
this.handleSave = this.handleSave.bind(this);
this.isCustomPreset = this.isCustomPreset.bind(this);
}
setOptionRef(optionName){
@ -51,11 +52,19 @@ class EditPresetDialog extends React.Component {
}
getFormData(){
return this.state; // TODO: necessary?
return {
id: this.props.preset.id,
name: this.state.name,
options: this.getOptions()
};
}
isCustomPreset(){
return this.props.preset.id === -1;
}
onShow(){
this.nameInput.focus();
if (!this.isCustomPreset()) this.nameInput.focus();
}
handleChange(field){
@ -66,10 +75,6 @@ class EditPresetDialog extends React.Component {
}
}
handleSave(){
}
render(){
let options = PresetUtils.getAvailableOptions(this.props.preset.options, this.props.availableOptions);
@ -82,13 +87,17 @@ class EditPresetDialog extends React.Component {
onShow={this.onShow}
saveIcon="fa fa-edit"
title="Edit Options"
saveAction={this.handleSave}>
<div className="row preset-name">
<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; }} value={this.state.name} onChange={this.handleChange('name')} />
saveAction={this.props.saveAction}
deleteWarning={false}
deleteAction={(this.props.preset.id !== -1 && !this.props.preset.system) ? this.props.deleteAction : undefined}>
{!this.isCustomPreset() ?
<div className="row preset-name">
<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; }} value={this.state.name} onChange={this.handleChange('name')} />
</div>
</div>
</div>
: ""}
<div className="row">
<label className="col-sm-2 control-label">Options</label>
<div className="col-sm-10">

Wyświetl plik

@ -3,6 +3,8 @@ import React from 'react';
import values from 'object.values';
import Utils from '../classes/Utils';
import EditPresetDialog from './EditPresetDialog';
import ErrorMessage from './ErrorMessage';
import $ from 'jquery';
if (!Object.values) {
values.shim();
@ -30,6 +32,9 @@ class EditTaskForm extends React.Component {
this.state = {
error: "",
presetError: "",
presetActionPerforming: false,
name: props.task !== null ? (props.task.name || "") : "",
loadedProcessingNodes: false,
loadedPresets: false,
@ -54,6 +59,11 @@ class EditTaskForm extends React.Component {
this.selectPresetById = this.selectPresetById.bind(this);
this.handleEditPreset = this.handleEditPreset.bind(this);
this.handleCancelEditPreset = this.handleCancelEditPreset.bind(this);
this.handlePresetSave = this.handlePresetSave.bind(this);
this.handleDuplicateSavePreset = this.handleDuplicateSavePreset.bind(this);
this.handleDeletePreset = this.handleDeletePreset.bind(this);
this.findFirstPresetMatching = this.findFirstPresetMatching.bind(this);
this.getAvailableOptionsOnly = this.getAvailableOptionsOnly.bind(this);
}
notifyFormLoaded(){
@ -165,6 +175,33 @@ class EditTaskForm extends React.Component {
this.loadPresets();
}
findFirstPresetMatching(presets, options){
for (let i = 0; i < presets.length; i++){
const preset = presets[i];
if (options.length === preset.options.length){
let dict = {};
options.forEach(opt => {
dict[opt.name] = opt.value;
});
let matchingOptions = 0;
for (let j = 0; j < preset.options.length; j++){
if (dict[preset.options[j].name] !== preset.options[j].value){
break;
}else{
matchingOptions++;
}
}
// If we terminated the loop above, all options match
if (matchingOptions === options.length) return preset;
}
}
return null;
}
loadPresets(){
function failed(){
// Try again
@ -172,19 +209,33 @@ class EditTaskForm extends React.Component {
}
this.presetsRequest =
$.getJSON("/api/presets/", presets => {
$.getJSON("/api/presets/?ordering=-created_at", presets => {
if (Array.isArray(presets)){
// In case somebody decides to remove all presets...
if (presets.length === 0){
this.setState({error: "There are no presets. Please create a system preset from the Administration -- Presets page, then try again."});
return;
}
// Add custom preset
const customPreset = {
id: -1,
name: "(Custom)",
options: [],
system: true
};
presets.push(customPreset);
// Choose preset
let selectedPreset = presets[0],
defaultPreset = presets.find(p => p.name === "Default"); // Do not translate Default
if (defaultPreset) selectedPreset = defaultPreset;
// TODO: look at task options
// If task's options are set attempt
// to find a preset that matches the current task options
if (this.props.task && Array.isArray(this.props.task.options) && this.props.task.options.length > 0){
const taskPreset = this.findFirstPresetMatching(presets, this.props.task.options);
if (taskPreset !== null){
selectedPreset = taskPreset;
}else{
customPreset.options = Utils.clone(this.props.task.options);
selectedPreset = customPreset;
}
}
this.setState({
loadedPresets: true,
@ -237,23 +288,148 @@ class EditTaskForm extends React.Component {
this.selectNodeByKey(e.target.value);
}
// Filter a list of options based on the ones that
// are available (usually options are from a preset and availableOptions
// from a processing node)
getAvailableOptionsOnly(options, availableOptions){
const optionNames = {};
availableOptions.forEach(opt => optionNames[opt.name] = true);
return options.filter(opt => optionNames[opt.name]);
}
getTaskInfo(){
const { name, selectedNode, selectedPreset } = this.state;
return {
name: this.state.name !== "" ? this.state.name : this.namePlaceholder,
selectedNode: this.state.selectedNode,
options: this.state.selectedPreset.options
name: name !== "" ? name : this.namePlaceholder,
selectedNode: selectedNode,
options: this.getAvailableOptionsOnly(selectedPreset.options, selectedNode.options)
};
}
handleEditPreset(){
// If the user tries to edit a system preset
// set the "Custom..." options to it
const { selectedPreset, presets } = this.state;
if (selectedPreset.system){
let customPreset = presets.find(p => p.id === -1);
// Might have been deleted
if (!customPreset){
customPreset = {
id: -1,
name: "(Custom)",
options: [],
system: true
};
presets.push(customPreset);
this.setState({presets});
}
customPreset.options = Utils.clone(selectedPreset.options);
this.setState({selectedPreset: customPreset});
}
this.setState({editingPreset: true});
// this.editPresetDialog.show();
}
handleCancelEditPreset(){
this.setState({editingPreset: false});
}
handlePresetSave(preset){
const done = () => {
// Update presets and selected preset
let p = this.state.presets.find(p => p.id === preset.id);
p.name = preset.name;
p.options = preset.options;
this.setState({selectedPreset: p});
};
// If it's a custom preset do not update server-side
if (preset.id === -1){
done();
return $.Deferred().resolve();
}else{
return $.ajax({
url: `/api/presets/${preset.id}/`,
contentType: 'application/json',
data: JSON.stringify({
name: preset.name,
options: preset.options
}),
dataType: 'json',
type: 'PATCH'
}).done(done);
}
}
handleDuplicateSavePreset(){
// Create a new preset with the same settings as the
// currently selected preset
const { selectedPreset, presets } = this.state;
this.setState({presetActionPerforming: true});
const isCustom = selectedPreset.id === -1,
name = isCustom ? "My Preset" : "Copy of " + selectedPreset.name;
$.ajax({
url: `/api/presets/`,
contentType: 'application/json',
data: JSON.stringify({
name: name,
options: selectedPreset.options
}),
dataType: 'json',
type: 'POST'
}).done(preset => {
// If the original preset was a custom one,
// we remove it from the list (since we just saved it)
if (isCustom){
presets.splice(presets.indexOf(selectedPreset), 1);
}
// Add new preset to list, select it, then edit
presets.push(preset);
this.setState({presets, selectedPreset: preset});
this.handleEditPreset();
}).fail(() => {
this.setState({presetError: "Could not duplicate the preset. Please try to refresh the page."});
}).always(() => {
this.setState({presetActionPerforming: false});
});
}
handleDeletePreset(){
const { selectedPreset, presets } = this.state;
if (selectedPreset.system){
this.setState({presetError: "System presets can only be removed by a staff member from the Administration panel."});
return;
}
if (window.confirm(`Are you sure you want to delete "${selectedPreset.name}"?`)){
this.setState({presetActionPerforming: true});
return $.ajax({
url: `/api/presets/${selectedPreset.id}/`,
contentType: 'application/json',
type: 'DELETE'
}).done(() => {
presets.splice(presets.indexOf(selectedPreset), 1);
// Select first by default
this.setState({presets, selectedPreset: presets[0], editingPreset: false});
}).fail(() => {
this.setState({presetError: "Could not delete the preset. Please try to refresh the page."});
}).always(() => {
this.setState({presetActionPerforming: false});
});
}else{
return $.Deferred().resolve();
}
}
render() {
if (this.state.error){
return (<div className="edit-task-panel">
@ -291,38 +467,48 @@ class EditTaskForm extends React.Component {
{this.state.presets.map(preset =>
<option value={preset.id} key={preset.id}>{preset.name}</option>
)}
</select>
<div className="btn-group presets-dropdown">
<button type="button" className="btn btn-default" onClick={this.handleEditPreset}>
<i className="fa fa-sliders"></i>
</button>
<button type="button" className="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span className="caret"></span>
</button>
<ul className="dropdown-menu">
<li>
<a href="javascript:void(0);" onClick={this.handleEditPreset}><i className="fa fa-sliders"></i> Edit</a>
</li>
<li className="divider"></li>
<li>
<a href="javascript:void(0);" onClick={this.handleDuplicatePreset}><i className="fa fa-copy"></i> Duplicate</a>
</li>
<li>
<a href="javascript:void(0);" onClick={this.handleDeletePreset}><i className="fa fa-trash-o"></i> Delete</a>
</li>
</ul>
</div>
</select>
{!this.state.presetActionPerforming ?
<div className="btn-group presets-dropdown">
<button type="button" className="btn btn-default" onClick={this.handleEditPreset}>
<i className="fa fa-sliders"></i>
</button>
<button type="button" className="btn btn-default dropdown-toggle" data-toggle="dropdown">
<span className="caret"></span>
</button>
<ul className="dropdown-menu">
<li>
<a href="javascript:void(0);" onClick={this.handleEditPreset}><i className="fa fa-sliders"></i> Edit</a>
</li>
<li className="divider"></li>
{this.state.selectedPreset.id !== -1 ?
<li>
<a href="javascript:void(0);" onClick={this.handleDuplicateSavePreset}><i className="fa fa-copy"></i> Duplicate</a>
</li>
:
<li>
<a href="javascript:void(0);" onClick={this.handleDuplicateSavePreset}><i className="fa fa-save"></i> Save</a>
</li>
}
<li className={this.state.selectedPreset.system ? "disabled" : ""}>
<a href="javascript:void(0);" onClick={this.handleDeletePreset}><i className="fa fa-trash-o"></i> Delete</a>
</li>
</ul>
</div>
: <i className="preset-performing-action-icon fa fa-cog fa-spin fa-fw"></i>}
<ErrorMessage className="preset-error" bind={[this, 'presetError']} />
</div>
</div>
PRESET:
{JSON.stringify(this.state.selectedPreset.options)}<br/>
{this.state.editingPreset ?
<EditPresetDialog
preset={this.state.selectedPreset}
availableOptions={this.state.selectedNode.options}
onHide={this.handleCancelEditPreset}
saveAction={this.handlePresetSave}
deleteAction={this.handleDeletePreset}
ref={(domNode) => { if (domNode) this.editPresetDialog = domNode; }}
/>
: ""}

Wyświetl plik

@ -23,7 +23,7 @@ class ErrorMessage extends React.Component {
if (parent.state[prop]){
return (
<div className="alert alert-warning alert-dismissible">
<div className={"alert alert-warning alert-dismissible " + (this.props.className ? this.props.className : "")}>
<button type="button" className="close" aria-label="Close" onClick={this.close}><span aria-hidden="true">&times;</span></button>
{parent.state[prop]}
</div>

Wyświetl plik

@ -24,7 +24,10 @@ class FormDialog extends React.Component {
saveLabel: React.PropTypes.string,
savingLabel: React.PropTypes.string,
saveIcon: React.PropTypes.string,
deleteWarning: React.PropTypes.string,
deleteWarning: React.PropTypes.oneOfType([
React.PropTypes.string,
React.PropTypes.bool
]),
show: React.PropTypes.bool
};
@ -50,6 +53,8 @@ class FormDialog extends React.Component {
}
componentDidMount(){
this._mounted = true;
$(this.modal)
// Ensure state is kept up to date when
// the user presses the escape key
@ -66,6 +71,8 @@ class FormDialog extends React.Component {
}
componentWillUnmount(){
this._mounted = false;
$(this.modal).off('hidden.bs.modal hidden.bs.modal')
.modal('hide');
}
@ -107,11 +114,13 @@ class FormDialog extends React.Component {
handleDelete(){
if (this.props.deleteAction){
if (window.confirm(this.props.deleteWarning)){
if (this.props.deleteWarning === false || window.confirm(this.props.deleteWarning)){
this.setState({deleting: true});
this.props.deleteAction()
.fail(e => {
this.setState({error: e.message || (e.responseJSON || {}).detail || e.responseText || "Could not delete item", deleting: false});
if (this._mounted) this.setState({error: e.message || (e.responseJSON || {}).detail || e.responseText || "Could not delete item"});
}).always(() => {
if (this._mounted) this.setState({deleting: false});
});
}
}

Wyświetl plik

@ -2,4 +2,12 @@
.presets-dropdown{
margin-left: 8px;
}
.preset-performing-action-icon{
margin-left: 12px;
}
.preset-error{
margin-top: 12px;
}
}