Place specialized upload button in the tool form.

This commit is contained in:
John Chilton
2020-08-07 08:59:48 -04:00
parent 540d690462
commit 90c03fcc26
18 changed files with 240 additions and 35 deletions
@@ -23,6 +23,10 @@
<div class="fa fa-caret-left mr-1" />
Back
</b-btn>
<b-btn size="sm" class="float-left mr-1" @click="newUpload">
<div class="fa fa-upload ml-1" />
Upload
</b-btn>
<b-btn
v-if="multiple"
size="sm"
@@ -44,6 +48,7 @@ import SelectionDialogMixin from "components/SelectionDialog/SelectionDialogMixi
import { UrlTracker } from "./utilities";
import { Model } from "./model";
import { Services } from "./services";
import { mountUploadModal } from "components/Upload";
Vue.use(BootstrapVue);
@@ -125,6 +130,16 @@ export default {
this.load(record.url);
}
},
newUpload: function () {
const propsData = {
multiple: this.multiple,
format: this.format,
callback: this.callback,
modalShow: true,
};
mountUploadModal(propsData);
this.modalShow = false;
},
/** Called when selection is complete, values are formatted and parsed to external callback **/
finalize: function () {
const results = this.model.finalize();
@@ -20,7 +20,9 @@ describe("Datatypes/index.js", () => {
axiosMock.onGet(`/api/datatypes/types_and_mapping`).reply(200, typesAndMappingResponse);
await getDatatypesMapper().then((mapper) => {
expect(mapper.isSubType("txt", "data")).to.equals(true);
expect(mapper.isSubType("txt", "txt")).to.equals(true);
expect(mapper.isSubType("data", "txt")).to.equals(false);
expect(mapper.isSubTypeOfAny("data", ["txt", "data"])).to.equals(true);
});
});
});
@@ -10,4 +10,8 @@ export class DatatypesMapperModel {
parent = mapping.ext_to_class_name[parent];
return mapping.class_to_classes[child] && parent in mapping.class_to_classes[child];
}
isSubTypeOfAny(child, parents) {
return parents.some((parent) => this.isSubType(child, parent));
}
}
@@ -42,7 +42,7 @@
</select2>
</template>
<template v-slot:buttons>
<b-button ref="btnClose" class="ui-button-default" id="btn-close" @click="app.hide()">
<b-button ref="btnClose" class="ui-button-default" id="btn-close" @click="app.dismiss()">
{{ btnCloseTitle }}
</b-button>
<b-button
@@ -139,6 +139,7 @@ export default {
listExtensions: [],
listGenomes: [],
running: false,
multiple: true, // needed for uploadbox stuff - always allow multiple uploads for collections
counterAnnounce: 0,
counterSuccess: 0,
counterError: 0,
@@ -158,7 +159,7 @@ export default {
btnBuildTitle: _l("Build"),
btnStopTitle: _l("Pause"),
btnResetTitle: _l("Reset"),
btnCloseTitle: _l("Close"),
btnCloseTitle: this.app.callback ? _l("Cancel") : _l("Close"),
};
},
created() {
@@ -34,7 +34,7 @@
</select2>
</template>
<template v-slot:buttons>
<b-button ref="btnClose" class="ui-button-default" @click="app.hide()">
<b-button ref="btnClose" class="ui-button-default" @click="app.dismiss()">
{{ btnCloseTitle }}
</b-button>
<b-button
@@ -74,7 +74,7 @@ export default {
showHelper: true,
btnResetTitle: _l("Reset"),
btnStartTitle: _l("Start"),
btnCloseTitle: _l("Close"),
btnCloseTitle: this.app.callback ? _l("Cancel") : _l("Close"),
readyStart: false,
};
},
@@ -34,7 +34,7 @@
</select2>
</template>
<template v-slot:buttons>
<b-button ref="btnClose" class="ui-button-default" id="btn-close" @click="app.hide()">
<b-button ref="btnClose" class="ui-button-default" id="btn-close" @click="app.dismiss()">
{{ btnCloseTitle }}
</b-button>
<b-button
@@ -55,6 +55,16 @@
>
{{ btnStopTitle }}
</b-button>
<b-button
ref="btnBuild"
class="ui-button-default"
id="btn-build"
@click="_eventSelect"
:disabled="!enableBuild"
:variant="enableBuild ? 'primary' : ''"
>
{{ btnSelectTitle }}
</b-button>
<b-button
ref="btnStart"
class="ui-button-default"
@@ -111,6 +121,12 @@ Vue.use(BootstrapVue);
export default {
mixins: [UploadBoxMixin],
props: {
multiple: {
type: Boolean,
default: true,
},
},
data() {
return {
topInfo: "",
@@ -118,6 +134,7 @@ export default {
showHelper: true,
extension: this.app.defaultExtension,
genome: this.app.defaultGenome,
callback: this.app.callback,
listExtensions: [],
listGenomes: [],
running: false,
@@ -129,6 +146,7 @@ export default {
uploadSize: 0,
uploadCompleted: 0,
enableReset: false,
enableBuild: false,
enableStart: false,
enableSources: false,
btnLocalTitle: _l("Choose local files"),
@@ -136,7 +154,8 @@ export default {
btnStartTitle: _l("Start"),
btnStopTitle: _l("Pause"),
btnResetTitle: _l("Reset"),
btnCloseTitle: _l("Close"),
btnCloseTitle: this.app.callback ? _l("Cancel") : _l("Close"),
btnSelectTitle: _l("Select"),
};
},
created() {
@@ -149,6 +168,7 @@ export default {
// file upload
this.initUploadbox({
url: this.app.uploadPath,
multiple: this.multiple,
announce: (index, file) => {
this._eventAnnounce(index, file);
},
@@ -200,6 +220,17 @@ export default {
},
},
methods: {
_eventSelect: function () {
const models = this.getUploadedModels();
const asDict = models.map((model) => {
return {
id: model.attributes.id, // model.id has datatype prefix
src: model.src,
};
});
this.callback(asDict);
this.app.cancel();
},
_newUploadModelProps: function (index, file) {
return {
id: index,
@@ -213,8 +244,9 @@ export default {
/** Success */
_eventSuccess: function (index, message) {
var hids = _.pluck(message["outputs"], "hid");
var it = this.collection.get(index);
it.set({ percentage: 100, status: "success" });
it.set({ percentage: 100, status: "success", hids: hids });
this._updateStateForSuccess(it);
},
@@ -46,7 +46,7 @@
></textarea>
</span>
<template v-slot:buttons>
<b-button ref="btnClose" class="ui-button-default" id="btn-close" @click="app.hide()">
<b-button ref="btnClose" class="ui-button-default" id="btn-close" @click="app.dismiss()">
{{ btnCloseTitle }}
</b-button>
<b-button
@@ -103,7 +103,7 @@ export default {
selectionType: "paste",
btnBuildTitle: _l("Build"),
btnResetTitle: _l("Reset"),
btnCloseTitle: _l("Close"),
btnCloseTitle: this.app.callback ? _l("Cancel") : _l("Close"),
};
},
created() {
@@ -102,15 +102,15 @@ export default {
},
_updateStateForCounters: function () {
this.setTopInfoBasedOnCounters();
this.enableReset =
this.counterRunning == 0 && this.counterAnnounce + this.counterSuccess + this.counterError > 0;
const counterNonRunning = this.counterAnnounce + this.counterSuccess + this.counterError;
this.enableReset = this.counterRunning == 0 && counterNonRunning > 0;
this.enableStart = this.counterRunning == 0 && this.counterAnnounce > 0;
this.enableBuild =
this.counterRunning == 0 &&
this.counterAnnounce == 0 &&
this.counterSuccess > 0 &&
this.counterError == 0;
this.enableSources = this.counterRunning == 0;
this.enableSources = this.counterRunning == 0 && (this.multiple || counterNonRunning == 0);
var show_table = this.counterAnnounce + this.counterSuccess + this.counterError > 0;
this.showHelper = !show_table;
},
@@ -259,7 +259,7 @@ export default {
return $(this.$refs.uploadTable);
},
extensionDetails(extension) {
return findExtension(this.listExtensions, extension);
return findExtension(this.effectiveExtensions, extension);
},
initExtensionInfo() {
$(this.$refs.footerExtensionInfo)
@@ -283,7 +283,7 @@ export default {
this.collection = new UploadModel.Collection();
},
initAppProperties() {
this.listExtensions = this.app.listExtensions;
this.listExtensions = this.app.effectiveExtensions;
this.listGenomes = this.app.listGenomes;
this.ftpUploadSite = this.app.currentFtp();
this.fileSourcesConfigured = this.app.fileSourcesConfigured;
@@ -6,23 +6,25 @@
modal-class="ui-modal"
dialog-class="upload-dialog"
body-class="upload-dialog-body"
ref="modal"
no-enforce-focus
hide-footer
:id="id"
>
<template v-slot:modal-header>
<h4 class="title" tabindex="0">{{ title }}</h4>
</template>
<b-tabs v-if="ready">
<b-tab title="Regular" id="regular" button-id="tab-title-link-regular">
<default :app="this" :lazy-load-max="50" />
<b-tab title="Regular" id="regular" button-id="tab-title-link-regular" v-if="showRegular">
<default :app="this" :lazy-load-max="50" :multiple="multiple" />
</b-tab>
<b-tab title="Composite" id="composite" button-id="tab-title-link-composite">
<b-tab title="Composite" id="composite" button-id="tab-title-link-composite" v-if="showComposite">
<composite :app="this" />
</b-tab>
<b-tab title="Collection" id="collection" button-id="tab-title-link-collection">
<b-tab title="Collection" id="collection" button-id="tab-title-link-collection" v-if="showCollection">
<collection :app="this" />
</b-tab>
<b-tab title="Rule-based" id="rule-based" button-id="tab-title-link-rule-based">
<b-tab title="Rule-based" id="rule-based" button-id="tab-title-link-rule-based" v-if="showRules">
<rules-input :app="this" />
</b-tab>
</b-tabs>
@@ -40,6 +42,7 @@ import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import { getGalaxyInstance } from "app";
import UploadUtils from "mvc/upload/upload-utils";
import { getDatatypesMapper } from "components/Datatypes";
import Composite from "./Composite";
import Collection from "./Collection";
import Default from "./Default";
@@ -93,6 +96,20 @@ export default {
type: Boolean,
default: false,
},
formats: {
type: Array,
default: null,
},
multiple: {
// Restrict the forms to a single dataset upload if false
type: Boolean,
default: true,
},
callback: {
// Return uploads when done if supplied.
type: Function,
default: null,
},
auto: {
type: Object,
default: function () {
@@ -102,6 +119,7 @@ export default {
},
data: function () {
return {
id: "",
title: _l("Download from web or upload from disk"),
historyAvailable: false,
currentUser: null,
@@ -109,6 +127,8 @@ export default {
listExtensions: [],
genomesSet: false,
extensionsSet: false,
datatypesMapper: null,
datatypesMapperReady: true,
};
},
created: function () {
@@ -138,11 +158,67 @@ export default {
}, this.defaultGenome);
this.initStateWhenHistoryReady();
if (this.formats !== null) {
this.datatypesMapperReady = false;
getDatatypesMapper().then((datatypesMapper) => {
this.datatypesMapper = datatypesMapper;
this.datatypesMapperReady = true;
});
} else {
this.datatypesMapperReady = true;
}
},
computed: {
ready() {
return this.genomesSet && this.extensionsSet && this.historyAvailable;
return this.genomesSet && this.extensionsSet && this.historyAvailable && this.datatypesMapperReady;
},
unrestricted() {
return this.formats === null && this.multiple;
},
effectiveExtensions() {
if (this.formats === null || !this.datatypesMapperReady) {
return this.listExtensions;
}
const effectiveExtensions = [];
this.listExtensions.forEach((extension) => {
if (extension && extension.id == "auto") {
effectiveExtensions.push(extension);
} else if (this.datatypesMapper.isSubTypeOfAny(extension.id, this.formats)) {
effectiveExtensions.push(extension);
}
});
return effectiveExtensions;
},
formatRestricted() {
return this.formats !== null;
},
showComposite() {
if (!this.formatRestricted) {
return true;
}
return this.effectiveExtensions.some((extension) => !!extension.composite_files);
},
showRegular() {
if (!this.formatRestricted) {
return true;
}
return this.effectiveExtensions.some((extension) => !extension.composite_files);
},
showCollection() {
if (this.unrestricted) {
return true;
}
return false;
},
showRules() {
if (this.unrestricted) {
return true;
}
return this.multiple;
},
},
mounted() {
this.id = String(this._uid);
},
methods: {
show() {
@@ -152,6 +228,21 @@ export default {
hide() {
this.modalShow = false;
},
cancel() {
this.hide();
this.$nextTick(() => {
this.$bvModal.hide(this.id, "cancel");
this.$destroy();
});
},
dismiss() {
// hide or cancel based on whether this is a singleton
if (this.callback) {
this.cancel();
} else {
this.hide();
}
},
initStateWhenHistoryReady() {
const Galaxy = getGalaxyInstance();
if (!Galaxy.currHistoryPanel || !Galaxy.currHistoryPanel.model) {
@@ -245,12 +336,4 @@ export default {
};
</script>
<style>
.upload-dialog {
width: 900px;
}
.upload-dialog-body {
height: 500px;
overflow: hidden;
}
</style>
<style></style>
@@ -4,3 +4,5 @@
export { default as UploadModal } from "./UploadModal";
export { initializeUploadDefaults } from "./config";
export { mount as mountUploadModal } from "./mount";
@@ -0,0 +1,13 @@
import Vue from "vue";
import UploadModal from "./UploadModal";
import { initializeUploadDefaults } from "./config";
export function mount(propsData = {}) {
propsData = initializeUploadDefaults(propsData);
const instance = Vue.extend(UploadModal);
const vm = document.createElement("div");
document.getElementsByTagName("body")[0].appendChild(vm);
new instance({
propsData: propsData,
}).$mount(vm);
}
@@ -10,7 +10,7 @@ export function mountWithApp(component, options = {}, propsData_ = {}) {
return "ftp://localhost";
},
model: new Backbone.Model(),
listExtensions: [
effectiveExtensions: [
{ id: "ab1", text: "ab1", description: "A binary sequence file in 'ab1' format with a '.ab1'" },
{
id: "affybatch",
@@ -272,8 +272,10 @@ const View = Backbone.View.extend({
});
if (cnf.src == "hda") {
this.button_dialog.show();
this.upload_dialog.show();
} else {
this.button_dialog.hide();
this.upload_dialog.hide();
}
this.button_type.value(i);
} else {
@@ -306,7 +308,8 @@ const View = Backbone.View.extend({
// prepare extension component of error message
const data = self.model.get("data");
const extensions = Utils.textify(this.model.get("extensions"));
const formats = this.model.get("extensions");
const extensions = Utils.textify(formats);
const src_labels = this.model.get("src_labels");
// build radio button for data selectors
@@ -362,14 +365,38 @@ const View = Backbone.View.extend({
},
});
// build data dialog button
this.upload_dialog = new Ui.Button({
icon: "fa-upload",
tooltip: "Upload Dataset",
onclick: () => {
const current = this.model.get("current");
const cnf = this.config[current];
// model doesn't have format yet unfortunately, need to get through to here.
galaxy.data.dialog(
(response) => {
this._handleDropValues(response, false);
},
{
multiple: cnf.multiple,
formats: formats,
new: true,
}
);
},
});
// append views
const $fields = $("<div/>").addClass("overflow-auto w-100 py-2 py-lg-0 pr-lg-2 ");
this.$el
.empty()
.addClass("d-flex flex-row flex-wrap flex-lg-nowrap")
.append($("<div/>").append(this.button_type.$el))
.append($fields)
.append($("<div/>").append(this.button_dialog.$el));
.append($fields);
if (galaxy.config.enable_upload_from_form_button) {
this.$el.append($("<div style='margin-right: 5px;' />").append(this.upload_dialog.$el));
}
this.$el.append($("<div/>").append(this.button_dialog.$el));
_.each(this.fields, (field) => {
$fields.append(field.$el);
});
+7 -1
View File
@@ -5,6 +5,7 @@ import DataDialog from "components/DataDialog/DataDialog.vue";
import { FilesDialog } from "components/FilesDialog";
import WorkflowDialog from "components/SelectionDialog/WorkflowDialog.vue";
import DatasetCollectionDialog from "components/SelectionDialog/DatasetCollectionDialog.vue";
import { mountUploadModal } from "components/Upload";
import { getGalaxyInstance } from "app";
import { getAppRoot } from "onload/loadConfig";
@@ -44,7 +45,12 @@ export function dialog(callback, options = {}) {
root: galaxy.root,
host: host,
});
_mountSelectionDialog(DataDialog, options);
if (options.new) {
options.modalShow = true;
mountUploadModal(options);
} else {
_mountSelectionDialog(DataDialog, options);
}
});
}
+2 -1
View File
@@ -300,6 +300,7 @@ import { getAppRoot } from "onload/loadConfig";
alert(m);
},
complete: () => {},
multiple: true,
},
options
);
@@ -320,7 +321,7 @@ import { getAppRoot } from "onload/loadConfig";
// element
var uploadinput = $(this).uploadinput({
multiple: true,
multiple: opts.multiple,
onchange: (files) => {
_.each(files, (file) => {
file.chunk_mode = true;
+9
View File
@@ -316,6 +316,15 @@ body {
}
}
.upload-dialog {
width: 900px;
}
.upload-dialog-body {
height: 500px;
overflow: hidden;
}
// ==== Masthead ====
#masthead {
@extend .p-0;
+1
View File
@@ -105,6 +105,7 @@ class ConfigSerializer(base.ModelSerializer):
'python' : _defaults_to((sys.version_info.major, sys.version_info.minor)),
'select_type_workflow_threshold' : _use_config,
'file_sources_configured' : lambda config, key, **context: self.app.file_sources.custom_sources_configured,
'enable_upload_from_form_button' : _use_config,
}
@@ -2158,6 +2158,15 @@ mapping:
When using LDAP for authentication, allow administrators to pre-populate users
using an additional form on 'Create new user'
enable_upload_from_form_button:
type: bool
default: false
required: false
desc: |
If enabled, add another button to tool form data inputs that allow uploading
data from the tool form in fewer clicks (at the expense of making the form more
complicated).
allow_user_dataset_purge:
type: bool
default: true