mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-19 10:51:34 +08:00
Merge pull request #7553 from guerler/add_data_dialog
Add data dialog option to tool form data selector
This commit is contained in:
@@ -1,232 +0,0 @@
|
||||
<template>
|
||||
<b-modal class="data-dialog-modal" v-model="modalShow" :ok-only="true" ok-title="Close">
|
||||
<template slot="modal-header">
|
||||
<b-input-group v-if="optionsShow">
|
||||
<b-input v-model="filter" placeholder="Type to Search" />
|
||||
<b-input-group-append>
|
||||
<b-btn :disabled="!filter" @click="filter = ''">Clear</b-btn>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</template>
|
||||
<b-alert v-if="errorMessage" variant="danger" :show="errorShow"> {{ errorMessage }} </b-alert>
|
||||
<div v-else>
|
||||
<div v-if="optionsShow">
|
||||
<b-table
|
||||
small
|
||||
hover
|
||||
:items="formatedItems"
|
||||
:fields="fields"
|
||||
:filter="filter"
|
||||
@row-clicked="clicked"
|
||||
@filtered="filtered"
|
||||
>
|
||||
<template slot="name" slot-scope="data">
|
||||
<i v-if="isDataset(data.item)" class="fa fa-file-o" /> <i v-else class="fa fa-copy" />
|
||||
{{ data.item.hid }}: {{ data.value }}
|
||||
</template>
|
||||
<template slot="extension" slot-scope="data">
|
||||
{{ data.value ? data.value : "-" }}
|
||||
</template>
|
||||
<template slot="update_time" slot-scope="data">
|
||||
{{ data.value ? data.value.substring(0, 16).replace("T", " ") : "-" }}
|
||||
</template>
|
||||
<template slot="arrow" slot-scope="data">
|
||||
<b-button
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="py-0"
|
||||
v-if="!isDataset(data.item)"
|
||||
@click.stop="load(data.item.url)"
|
||||
>
|
||||
View
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table>
|
||||
<div v-if="nItems == 0">
|
||||
<div v-if="filter">
|
||||
No search results found for: <b>{{ this.filter }}</b
|
||||
>.
|
||||
</div>
|
||||
<div v-else>No entries.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else><span class="fa fa-spinner fa-spin" /> <span>Please wait...</span></div>
|
||||
</div>
|
||||
<div slot="modal-footer" class="w-100">
|
||||
<b-btn size="sm" class="float-left" v-if="undoShow" @click="load()">
|
||||
<div class="fa fa-caret-left mr-1" />
|
||||
Back
|
||||
</b-btn>
|
||||
<b-btn size="sm" class="float-right ml-1" variant="primary" @click="done" :disabled="values.length === 0">
|
||||
Ok
|
||||
</b-btn>
|
||||
<b-btn size="sm" class="float-right" @click="modalShow = false"> Cancel </b-btn>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from "axios";
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { getGalaxyInstance } from "app";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
props: {
|
||||
callback: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: "url"
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
errorMessage: null,
|
||||
errorShow: false,
|
||||
fields: {
|
||||
name: {
|
||||
sortable: true
|
||||
},
|
||||
extension: {
|
||||
sortable: true
|
||||
},
|
||||
update_time: {
|
||||
sortable: true
|
||||
},
|
||||
arrow: {
|
||||
label: "",
|
||||
sortable: false,
|
||||
class: "text-right"
|
||||
}
|
||||
},
|
||||
filter: null,
|
||||
historyId: null,
|
||||
items: [],
|
||||
modalShow: true,
|
||||
nItems: 0,
|
||||
optionsShow: false,
|
||||
undoShow: false,
|
||||
url: null,
|
||||
values: {}
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
formatedItems() {
|
||||
for (let item of this.items) {
|
||||
if (this.isDataset(item)) {
|
||||
let key = item.id;
|
||||
item._rowVariant = this.values[key] ? "success" : "default";
|
||||
} else {
|
||||
item._rowVariant = "active";
|
||||
}
|
||||
}
|
||||
return this.items;
|
||||
}
|
||||
},
|
||||
created: function() {
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
isDataset: function(item) {
|
||||
return item.history_content_type == "dataset";
|
||||
},
|
||||
filtered: function(items) {
|
||||
this.nItems = items.length;
|
||||
},
|
||||
clicked: function(record) {
|
||||
if (this.isDataset(record)) {
|
||||
if (!this.multiple) {
|
||||
this.values = {};
|
||||
}
|
||||
let key = record.id;
|
||||
if (!this.values[key]) {
|
||||
this.values[key] = record;
|
||||
} else {
|
||||
delete this.values[key];
|
||||
}
|
||||
this.values = Object.assign({}, this.values);
|
||||
if (!this.multiple) {
|
||||
this.done();
|
||||
}
|
||||
}
|
||||
},
|
||||
done: function() {
|
||||
let results = [];
|
||||
Object.values(this.values).forEach(v => {
|
||||
let value = v.id;
|
||||
if (this.format == "url") {
|
||||
let host = `${window.location.protocol}//${window.location.hostname}:${window.location.port}`;
|
||||
value = `${host}/api/histories/${v.history_id}/contents/${value}/display`;
|
||||
}
|
||||
results.push(value);
|
||||
});
|
||||
if (results.length > 0 && !this.multiple) {
|
||||
results = results[0];
|
||||
}
|
||||
this.modalShow = false;
|
||||
this.callback(results);
|
||||
},
|
||||
load: function(url) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
this.optionsShow = false;
|
||||
this.undoShow = false;
|
||||
let hasUrl = !!url;
|
||||
if (!hasUrl) {
|
||||
let historyId = Galaxy.currHistoryPanel && Galaxy.currHistoryPanel.model.id;
|
||||
if (historyId) {
|
||||
url = `${Galaxy.root}api/histories/${historyId}/contents`;
|
||||
} else {
|
||||
this.errorMessage = "History not accessible.";
|
||||
return;
|
||||
}
|
||||
}
|
||||
axios
|
||||
.get(url)
|
||||
.then(response => {
|
||||
this.items = [];
|
||||
this.stack = [response.data];
|
||||
while (this.stack.length > 0) {
|
||||
let root = this.stack.pop();
|
||||
if (Array.isArray(root)) {
|
||||
root.forEach(element => {
|
||||
this.stack.push(element);
|
||||
});
|
||||
} else if (root.elements) {
|
||||
this.stack.push(root.elements);
|
||||
} else if (root.object) {
|
||||
this.stack.push(root.object);
|
||||
} else if (root.hid) {
|
||||
this.items.push(root);
|
||||
}
|
||||
}
|
||||
this.optionsShow = true;
|
||||
this.undoShow = hasUrl;
|
||||
})
|
||||
.catch(e => {
|
||||
if (e.response) {
|
||||
this.errorMessage =
|
||||
e.response.data.err_msg || `${e.response.statusText} (${e.response.status})`;
|
||||
} else {
|
||||
this.errorMessage = "Server unavailable.";
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.data-dialog-modal .modal-body {
|
||||
max-height: 50vh;
|
||||
height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,180 @@
|
||||
import sinon from "sinon";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import DataDialog from "./DataDialog.vue";
|
||||
import { __RewireAPI__ as rewire } from "./DataDialog";
|
||||
import { Model } from "./model.js";
|
||||
import { UrlTracker } from "./utilities.js";
|
||||
import { Services } from "./services";
|
||||
import Vue from "vue";
|
||||
|
||||
const mockOptions = {
|
||||
callback: () => {},
|
||||
host: "host",
|
||||
root: "root",
|
||||
history: "history"
|
||||
};
|
||||
|
||||
describe("model.js", () => {
|
||||
let result = null;
|
||||
it("Model operations for single, no format", () => {
|
||||
let model = new Model();
|
||||
try {
|
||||
model.add({ idx: 1 });
|
||||
throw "Accepted invalid record.";
|
||||
} catch (error) {
|
||||
expect(error).to.equals("Invalid record with no <id>.");
|
||||
}
|
||||
model.add({ id: 1 });
|
||||
expect(model.count()).to.equals(1);
|
||||
expect(model.exists(1)).to.equals(true);
|
||||
model.add({ id: 2, tag: "tag" });
|
||||
expect(model.count()).to.equals(1);
|
||||
expect(model.exists(1)).to.equals(false);
|
||||
expect(model.exists(2)).to.equals(true);
|
||||
result = model.finalize();
|
||||
expect(result.id).to.equals(2);
|
||||
expect(result.tag).to.equals("tag");
|
||||
});
|
||||
it("Model operations for multiple, with format", () => {
|
||||
let model = new Model({ multiple: true, format: "tag" });
|
||||
model.add({ id: 1, tag: "tag_1" });
|
||||
expect(model.count()).to.equals(1);
|
||||
model.add({ id: 2, tag: "tag_2" });
|
||||
expect(model.count()).to.equals(2);
|
||||
result = model.finalize();
|
||||
expect(result.length).to.equals(2);
|
||||
expect(result[0]).to.equals("tag_1");
|
||||
expect(result[1]).to.equals("tag_2");
|
||||
model.add({ id: 1 });
|
||||
expect(model.count()).to.equals(1);
|
||||
result = model.finalize();
|
||||
expect(result[0]).to.equals("tag_2");
|
||||
});
|
||||
});
|
||||
|
||||
describe("utilities.js/UrlTracker", () => {
|
||||
it("Test url tracker", () => {
|
||||
let urlTracker = new UrlTracker("url_initial");
|
||||
let url = urlTracker.getUrl();
|
||||
expect(url).to.equals("url_initial");
|
||||
expect(urlTracker.atRoot()).to.equals(true);
|
||||
url = urlTracker.getUrl("url_1");
|
||||
expect(url).to.equals("url_1");
|
||||
expect(urlTracker.atRoot()).to.equals(false);
|
||||
url = urlTracker.getUrl("url_2");
|
||||
expect(url).to.equals("url_2");
|
||||
expect(urlTracker.atRoot()).to.equals(false);
|
||||
url = urlTracker.getUrl();
|
||||
expect(url).to.equals("url_1");
|
||||
expect(urlTracker.atRoot()).to.equals(false);
|
||||
url = urlTracker.getUrl();
|
||||
expect(url).to.equals("url_initial");
|
||||
expect(urlTracker.atRoot()).to.equals(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("services/Services:isDataset", () => {
|
||||
it("Test dataset identifier", () => {
|
||||
let services = new Services(mockOptions);
|
||||
expect(services.isDataset({})).to.equals(false);
|
||||
expect(services.isDataset({ history_content_type: "dataset" })).to.equals(true);
|
||||
expect(services.isDataset({ history_content_type: "xyz" })).to.equals(false);
|
||||
expect(services.isDataset({ type: "file" })).to.equals(true);
|
||||
expect(services.getRecord({ hid: 1, history_content_type: "dataset" }).isDataset).to.equals(true);
|
||||
expect(services.getRecord({ hid: 2, history_content_type: "xyz" }).isDataset).to.equals(false);
|
||||
expect(services.getRecord({ type: "file" }).isDataset).to.equals(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("services.js/Services", () => {
|
||||
it("Test data population from raw data", () => {
|
||||
let rawData = {
|
||||
hid: 1,
|
||||
id: 1,
|
||||
history_id: 0,
|
||||
name: "name_1"
|
||||
};
|
||||
let services = new Services(mockOptions);
|
||||
let items = services.getItems(rawData);
|
||||
expect(items.length).to.equals(1);
|
||||
let first = items[0];
|
||||
expect(first.name).to.equals("1: name_1");
|
||||
expect(first.download).to.equals("host/api/histories/0/contents/1/display");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DataDialog.vue", () => {
|
||||
let stub;
|
||||
let wrapper;
|
||||
let emitted;
|
||||
|
||||
let rawData = [
|
||||
{
|
||||
id: 1,
|
||||
hid: 1,
|
||||
name: "dataset_1",
|
||||
history_content_type: "dataset"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "dataset_2",
|
||||
type: "file"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
hid: 3,
|
||||
name: "collection_1",
|
||||
history_content_type: "dataset_collection"
|
||||
}
|
||||
];
|
||||
|
||||
let mockServices = class {
|
||||
get(url) {
|
||||
let services = new Services(mockOptions);
|
||||
let items = services.getItems(rawData);
|
||||
return new Promise((resolve, reject) => {
|
||||
resolve(items);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
rewire.__Rewire__("Services", mockServices);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (stub) stub.restore();
|
||||
});
|
||||
|
||||
it("loads correctly, shows alert", () => {
|
||||
wrapper = mount(DataDialog, {
|
||||
propsData: mockOptions
|
||||
});
|
||||
emitted = wrapper.emitted();
|
||||
expect(wrapper.classes()).contain("data-dialog-modal");
|
||||
expect(wrapper.find(".fa-spinner").text()).to.equals("");
|
||||
expect(wrapper.find(".btn-secondary").text()).to.equals("Clear");
|
||||
expect(wrapper.find(".btn-primary").text()).to.equals("Ok");
|
||||
expect(wrapper.contains(".fa-spinner")).to.equals(true);
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(wrapper.findAll(".fa-copy").length).to.equals(2);
|
||||
expect(wrapper.findAll(".fa-file-o").length).to.equals(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("loads correctly, shows datasets and folders", () => {
|
||||
wrapper = mount(DataDialog, {
|
||||
propsData: mockOptions
|
||||
});
|
||||
emitted = wrapper.emitted();
|
||||
expect(wrapper.classes()).contain("data-dialog-modal");
|
||||
expect(wrapper.find(".fa-spinner").text()).to.equals("");
|
||||
expect(wrapper.find(".btn-secondary").text()).to.equals("Clear");
|
||||
expect(wrapper.find(".btn-primary").text()).to.equals("Ok");
|
||||
expect(wrapper.contains(".fa-spinner")).to.equals(true);
|
||||
return Vue.nextTick().then(() => {
|
||||
expect(wrapper.findAll(".fa-copy").length).to.equals(2);
|
||||
expect(wrapper.findAll(".fa-file-o").length).to.equals(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
<template>
|
||||
<b-modal class="data-dialog-modal" v-if="modalShow" visible ok-only ok-title="Close">
|
||||
<template slot="modal-header">
|
||||
<data-dialog-search v-model="filter" />
|
||||
</template>
|
||||
<b-alert v-if="errorMessage" variant="danger" show v-html="errorMessage" />
|
||||
<div v-else>
|
||||
<data-dialog-table
|
||||
v-if="optionsShow"
|
||||
:items="items"
|
||||
:multiple="multiple"
|
||||
:filter="filter"
|
||||
@clicked="clicked"
|
||||
@load="load"
|
||||
/>
|
||||
<div v-else><span class="fa fa-spinner fa-spin" /> <span>Please wait...</span></div>
|
||||
</div>
|
||||
<div v-if="!errorMessage" slot="modal-footer" class="w-100">
|
||||
<b-btn size="sm" class="float-left" v-if="undoShow" @click="load()">
|
||||
<div class="fa fa-caret-left mr-1" />
|
||||
Back
|
||||
</b-btn>
|
||||
<b-btn size="sm" class="float-right ml-1" variant="primary" @click="finalize" :disabled="!hasValue">
|
||||
Ok
|
||||
</b-btn>
|
||||
<b-btn size="sm" class="float-right" @click="modalShow = false"> Cancel </b-btn>
|
||||
</div>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import DataDialogSearch from "./DataDialogSearch.vue";
|
||||
import DataDialogTable from "./DataDialogTable.vue";
|
||||
import { UrlTracker } from "./utilities.js";
|
||||
import { Model } from "./model.js";
|
||||
import { Services } from "./services.js";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
"data-dialog-search": DataDialogSearch,
|
||||
"data-dialog-table": DataDialogTable
|
||||
},
|
||||
props: {
|
||||
callback: {
|
||||
type: Function,
|
||||
required: true
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
format: {
|
||||
type: String,
|
||||
default: "download"
|
||||
},
|
||||
library: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
},
|
||||
root: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
host: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
history: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
errorMessage: null,
|
||||
filter: null,
|
||||
items: [],
|
||||
modalShow: true,
|
||||
optionsShow: false,
|
||||
undoShow: false,
|
||||
hasValue: false,
|
||||
oldnewItems: false
|
||||
};
|
||||
},
|
||||
created: function() {
|
||||
this.services = new Services({ root: this.root, host: this.host });
|
||||
this.urlTracker = new UrlTracker(this.getHistoryUrl());
|
||||
this.model = new Model({ multiple: this.multiple, format: this.format });
|
||||
this.load();
|
||||
},
|
||||
methods: {
|
||||
/** Returns the default url i.e. the url of the current history **/
|
||||
getHistoryUrl: function() {
|
||||
return `${this.root}api/histories/${this.history}/contents?deleted=false`;
|
||||
},
|
||||
/** Add highlighting for record variations, i.e. datasets vs. libraries/collections **/
|
||||
formatRows() {
|
||||
for (let item of this.items) {
|
||||
let _rowVariant = "active";
|
||||
if (item.isDataset) {
|
||||
_rowVariant = this.model.exists(item.id) ? "success" : "default";
|
||||
}
|
||||
Vue.set(item, "_rowVariant", _rowVariant);
|
||||
}
|
||||
},
|
||||
/** Collects selected datasets in value array **/
|
||||
clicked: function(record) {
|
||||
if (record.isDataset) {
|
||||
this.model.add(record);
|
||||
this.hasValue = this.model.count() > 0;
|
||||
if (this.multiple) {
|
||||
this.formatRows();
|
||||
} else {
|
||||
this.finalize();
|
||||
}
|
||||
}
|
||||
},
|
||||
/** Called when selection is complete, values are formatted and parsed to external callback **/
|
||||
finalize: function() {
|
||||
let results = this.model.finalize();
|
||||
this.modalShow = false;
|
||||
this.callback(results);
|
||||
},
|
||||
/** Performs server request to retrieve data records **/
|
||||
load: function(url) {
|
||||
url = this.urlTracker.getUrl(url);
|
||||
this.filter = null;
|
||||
this.optionsShow = false;
|
||||
this.undoShow = !this.urlTracker.atRoot();
|
||||
this.services
|
||||
.get(url)
|
||||
.then(items => {
|
||||
if (this.library && this.urlTracker.atRoot()) {
|
||||
items.unshift({
|
||||
name: "Data Libraries",
|
||||
url: `${this.root}api/libraries`
|
||||
});
|
||||
}
|
||||
this.items = items;
|
||||
this.formatRows();
|
||||
this.optionsShow = true;
|
||||
})
|
||||
.catch(errorMessage => {
|
||||
this.errorMessage = errorMessage;
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.data-dialog-modal .modal-body {
|
||||
max-height: 50vh;
|
||||
height: 50vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,29 @@
|
||||
<template>
|
||||
<b-input-group>
|
||||
<b-input v-model="filter" placeholder="Type to Search" />
|
||||
<b-input-group-append>
|
||||
<b-btn :disabled="!filter" @click="filter = ''">Clear</b-btn>
|
||||
</b-input-group-append>
|
||||
</b-input-group>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
props: ["value"],
|
||||
computed: {
|
||||
filter: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit('input', val);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-table
|
||||
small
|
||||
hover
|
||||
:items="items"
|
||||
:fields="fields"
|
||||
:filter="filter"
|
||||
:per-page="perPage"
|
||||
:current-page="currentPage"
|
||||
@row-clicked="clicked"
|
||||
@filtered="filtered"
|
||||
>
|
||||
<template slot="name" slot-scope="data">
|
||||
<i v-if="data.item.isDataset" class="fa fa-file-o" /> <i v-else class="fa fa-copy" />
|
||||
{{ data.value ? data.value : "-" }}
|
||||
</template>
|
||||
<template slot="details" slot-scope="data">
|
||||
{{ data.value ? data.value : "-" }}
|
||||
</template>
|
||||
<template slot="time" slot-scope="data">
|
||||
{{ data.value ? data.value : "-" }}
|
||||
</template>
|
||||
<template slot="arrow" slot-scope="data">
|
||||
<b-button
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="py-0"
|
||||
v-if="!data.item.isDataset"
|
||||
@click.stop="load(data.item.url)"
|
||||
>
|
||||
View
|
||||
</b-button>
|
||||
</template>
|
||||
</b-table>
|
||||
<div v-if="nItems === 0">
|
||||
<div v-if="filter">
|
||||
No search results found for: <b>{{ this.filter }}</b
|
||||
>.
|
||||
</div>
|
||||
<div v-else>No entries.</div>
|
||||
</div>
|
||||
<b-pagination v-if="nItems > perPage" v-model="currentPage" :per-page="perPage" :total-rows="nItems" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
props: {
|
||||
items: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
filter: {
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentPage: 1,
|
||||
fields: {
|
||||
name: {
|
||||
sortable: true
|
||||
},
|
||||
details: {
|
||||
sortable: true
|
||||
},
|
||||
time: {
|
||||
sortable: true
|
||||
},
|
||||
arrow: {
|
||||
label: "",
|
||||
sortable: false,
|
||||
class: "text-right"
|
||||
}
|
||||
},
|
||||
nItems: 0,
|
||||
perPage: 100
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
items: {
|
||||
immediate: true,
|
||||
handler(items) {
|
||||
this.filtered(items);
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
/** Resets pagination when a filter/search word is entered **/
|
||||
filtered: function(items) {
|
||||
this.nItems = items.length;
|
||||
this.currentPage = 1;
|
||||
},
|
||||
/** Collects selected datasets in value array **/
|
||||
clicked: function(record) {
|
||||
this.$emit("clicked", record);
|
||||
},
|
||||
/** Performs server request to retrieve data records **/
|
||||
load: function(url) {
|
||||
this.$emit("load", url);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,52 @@
|
||||
export class Model {
|
||||
constructor(options = {}) {
|
||||
this.values = {};
|
||||
this.multiple = options.multiple || false;
|
||||
this.format = options.format || null;
|
||||
}
|
||||
|
||||
/** Adds a new record to the value stack **/
|
||||
add(record) {
|
||||
if (!this.multiple) {
|
||||
this.values = {};
|
||||
}
|
||||
let key = record && record.id;
|
||||
if (key) {
|
||||
if (!this.values[key]) {
|
||||
this.values[key] = record;
|
||||
} else {
|
||||
delete this.values[key];
|
||||
}
|
||||
} else {
|
||||
throw "Invalid record with no <id>.";
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the number of added records **/
|
||||
count() {
|
||||
return Object.keys(this.values).length;
|
||||
}
|
||||
|
||||
/** Returns true if a record is available for a given key **/
|
||||
exists(key) {
|
||||
return !!this.values[key];
|
||||
}
|
||||
|
||||
/** Finalizes the results from added records **/
|
||||
finalize() {
|
||||
let results = [];
|
||||
Object.values(this.values).forEach(v => {
|
||||
let value = null;
|
||||
if (this.format) {
|
||||
value = v[this.format];
|
||||
} else {
|
||||
value = v;
|
||||
}
|
||||
results.push(value);
|
||||
});
|
||||
if (results.length > 0 && !this.multiple) {
|
||||
results = results[0];
|
||||
}
|
||||
return results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import axios from "axios";
|
||||
|
||||
/** Data populator traverses raw server responses **/
|
||||
export class Services {
|
||||
constructor(options = {}) {
|
||||
this.root = options.root;
|
||||
this.host = options.host;
|
||||
}
|
||||
|
||||
get(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(url)
|
||||
.then(response => {
|
||||
let items = this.getItems(response.data);
|
||||
resolve(items);
|
||||
})
|
||||
.catch(e => {
|
||||
let errorMessage = "Request failed.";
|
||||
if (e.response) {
|
||||
errorMessage = e.response.data.err_msg || `${e.response.statusText} (${e.response.status})`;
|
||||
}
|
||||
reject(errorMessage);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Returns the formatted results **/
|
||||
getItems(data) {
|
||||
let items = [];
|
||||
let stack = [data];
|
||||
while (stack.length > 0) {
|
||||
let root = stack.pop();
|
||||
if (Array.isArray(root)) {
|
||||
root.forEach(element => {
|
||||
stack.push(element);
|
||||
});
|
||||
} else if (root.elements) {
|
||||
stack.push(root.elements);
|
||||
} else if (root.object) {
|
||||
stack.push(root.object);
|
||||
} else {
|
||||
let record = this.getRecord(root);
|
||||
if (record) {
|
||||
items.push(record);
|
||||
}
|
||||
}
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/** Populate record data from raw record source **/
|
||||
getRecord(record) {
|
||||
record.details = record.extension || record.description;
|
||||
record.time = record.update_time || record.create_time;
|
||||
record.isDataset = this.isDataset(record);
|
||||
if (record.time) {
|
||||
record.time = record.time.substring(0, 16).replace("T", " ");
|
||||
}
|
||||
if (record.model_class == "Library") {
|
||||
record.url = `${this.root}api/libraries/${record.id}/contents`;
|
||||
return record;
|
||||
} else if (record.hid) {
|
||||
record.name = `${record.hid}: ${record.name}`;
|
||||
record.download = `${this.host}/api/histories/${record.history_id}/contents/${record.id}/display`;
|
||||
return record;
|
||||
} else if (record.type == "file") {
|
||||
if (record.name && record.name[0] === "/") {
|
||||
record.name = record.name.substring(1);
|
||||
}
|
||||
record.download = `${this.host}${this.root}api/libraries/datasets/download/uncompressed?ld_ids=${
|
||||
record.id
|
||||
}`;
|
||||
return record;
|
||||
}
|
||||
}
|
||||
|
||||
/** Checks if record is a dataset or drillable **/
|
||||
isDataset(record) {
|
||||
return record.history_content_type == "dataset" || record.type == "file";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/** This helps track urls for data drilling **/
|
||||
export class UrlTracker {
|
||||
constructor(root) {
|
||||
this.root = root;
|
||||
this.navigation = [];
|
||||
}
|
||||
|
||||
/** Returns urls for data drilling **/
|
||||
getUrl(url) {
|
||||
if (url) {
|
||||
this.navigation.push(url);
|
||||
} else {
|
||||
this.navigation.pop();
|
||||
let navigationLength = this.navigation.length;
|
||||
if (navigationLength > 0) {
|
||||
url = this.navigation[navigationLength - 1];
|
||||
} else {
|
||||
url = this.root;
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
/** Returns true if the last data is at navigation root **/
|
||||
atRoot() {
|
||||
return this.navigation.length == 0;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import $ from "jquery";
|
||||
import DataDialog from "components/DataDialog.vue";
|
||||
import DataDialog from "components/DataDialog/DataDialog.vue";
|
||||
import Vue from "vue";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
@@ -10,7 +10,14 @@ export default class Data {
|
||||
* @param {function} callback - Result function called with selection
|
||||
*/
|
||||
dialog(callback, options = {}) {
|
||||
options.callback = callback;
|
||||
let galaxy = getGalaxyInstance();
|
||||
let host = `${window.location.protocol}//${window.location.hostname}:${window.location.port}`;
|
||||
Object.assign(options, {
|
||||
callback: callback,
|
||||
history: galaxy.currHistoryPanel && galaxy.currHistoryPanel.model.id,
|
||||
root: galaxy.root,
|
||||
host: host
|
||||
});
|
||||
var instance = Vue.extend(DataDialog);
|
||||
var vm = document.createElement("div");
|
||||
$("body").append(vm);
|
||||
|
||||
@@ -12,6 +12,7 @@ var Base = Backbone.View.extend({
|
||||
(options && options.model) ||
|
||||
new Backbone.Model({
|
||||
visible: true,
|
||||
cls: null,
|
||||
data: [],
|
||||
id: Utils.uid(),
|
||||
error_text: "No options available.",
|
||||
@@ -36,6 +37,7 @@ var Base = Backbone.View.extend({
|
||||
.empty()
|
||||
.removeClass()
|
||||
.addClass("ui-options")
|
||||
.addClass(this.model.get("cls"))
|
||||
.append((this.$message = $("<div/>").addClass("mt-2")))
|
||||
.append((this.$menu = $("<div/>").addClass("ui-options-menu")))
|
||||
.append((this.$options = $(this._template())));
|
||||
@@ -154,6 +156,16 @@ var Base = Backbone.View.extend({
|
||||
return this.$(".ui-option").length;
|
||||
},
|
||||
|
||||
/** Shows the options */
|
||||
show: function() {
|
||||
this.model.set("visible", true);
|
||||
},
|
||||
|
||||
/** Hides the options */
|
||||
hide: function() {
|
||||
this.model.set("visible", false);
|
||||
},
|
||||
|
||||
/** Set value to dom */
|
||||
_setValue: function(new_value) {
|
||||
var self = this;
|
||||
@@ -260,7 +272,7 @@ RadioButton.View = Base.extend({
|
||||
|
||||
/** Template for a single option */
|
||||
_templateOption: function(pair) {
|
||||
var $el = $("<label/>").addClass("btn btn-secondary");
|
||||
var $el = $("<label/>").addClass("btn btn-secondary m-0");
|
||||
if (pair.icon) {
|
||||
$el.append(
|
||||
$("<i/>")
|
||||
@@ -286,7 +298,7 @@ RadioButton.View = Base.extend({
|
||||
|
||||
/** Main template function */
|
||||
_template: function() {
|
||||
return $("<div/>").addClass("btn-group ui-radiobutton");
|
||||
return $("<div/>").addClass("btn-group ui-radiobutton d-flex");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -149,28 +149,11 @@ var View = Backbone.View.extend({
|
||||
}
|
||||
]
|
||||
});
|
||||
var $batch_div = $("<div/>")
|
||||
.addClass("form-text text-muted")
|
||||
.append($("<i/>").addClass("fa fa-sitemap"))
|
||||
.append(
|
||||
$("<span/>").html(
|
||||
"This is a batch mode input field. Separate jobs will be triggered for each dataset selection."
|
||||
)
|
||||
);
|
||||
this.$batch = {
|
||||
linked: $batch_div.clone(),
|
||||
enabled: $batch_div
|
||||
linked: $(this._templateBatch()).clone(),
|
||||
enabled: $(this._templateBatch())
|
||||
.clone()
|
||||
.append(
|
||||
$("<div/>")
|
||||
.append(
|
||||
$("<div/>")
|
||||
.addClass("ui-form-title")
|
||||
.html("Batch options:")
|
||||
)
|
||||
.append(this.button_product.$el)
|
||||
)
|
||||
.append($("<div/>").css("clear", "both"))
|
||||
.append(this.button_product.$el)
|
||||
};
|
||||
|
||||
// add drag-drop event handlers
|
||||
@@ -186,7 +169,13 @@ var View = Backbone.View.extend({
|
||||
this.lastenter === e.target && self.$el.removeClass("ui-dragover");
|
||||
});
|
||||
element.addEventListener("drop", e => {
|
||||
self._handleDrop(e);
|
||||
e.preventDefault();
|
||||
try {
|
||||
let drop_data = JSON.parse(e.dataTransfer.getData("text"))[0];
|
||||
this._handleDropValues(drop_data);
|
||||
} catch (e) {
|
||||
this._handleDropStatus("danger");
|
||||
}
|
||||
});
|
||||
|
||||
// track current history elements
|
||||
@@ -234,7 +223,7 @@ var View = Backbone.View.extend({
|
||||
|
||||
/** Return the currently selected dataset values */
|
||||
value: function(new_value) {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
let galaxy = getGalaxyInstance();
|
||||
new_value !== undefined && this.model.set("value", new_value);
|
||||
var current = this.model.get("current");
|
||||
if (this.config[current]) {
|
||||
@@ -248,7 +237,7 @@ var View = Backbone.View.extend({
|
||||
if (details) {
|
||||
result.values.push(details);
|
||||
} else {
|
||||
Galaxy.emit.debug(
|
||||
galaxy.emit.debug(
|
||||
"ui-select-content::value()",
|
||||
`Requested details not found for '${id_list[i]}'.`
|
||||
);
|
||||
@@ -260,7 +249,7 @@ var View = Backbone.View.extend({
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Galaxy.emit.debug("ui-select-content::value()", `Invalid value/source '${new_value}'.`);
|
||||
galaxy.emit.debug("ui-select-content::value()", `Invalid value/source '${new_value}'.`);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -269,22 +258,37 @@ var View = Backbone.View.extend({
|
||||
_changeCurrent: function() {
|
||||
var self = this;
|
||||
_.each(this.fields, (field, i) => {
|
||||
let cnf = self.config[i];
|
||||
if (self.model.get("current") == i) {
|
||||
field.$el.show();
|
||||
_.each(self.$batch, ($batchfield, batchmode) => {
|
||||
$batchfield[self.config[i].batch == batchmode ? "show" : "hide"]();
|
||||
if (cnf.batch == batchmode) {
|
||||
$batchfield.show();
|
||||
} else {
|
||||
$batchfield.hide();
|
||||
}
|
||||
});
|
||||
if (cnf.showdialog) {
|
||||
self.button_dialog.show();
|
||||
} else {
|
||||
self.button_dialog.hide();
|
||||
}
|
||||
self.button_type.value(i);
|
||||
} else {
|
||||
field.$el.hide();
|
||||
}
|
||||
});
|
||||
if (this.fields.length > 1) {
|
||||
this.button_type.show();
|
||||
} else {
|
||||
this.button_type.hide();
|
||||
}
|
||||
},
|
||||
|
||||
/** Change of type */
|
||||
_changeType: function() {
|
||||
let Galaxy = getGalaxyInstance();
|
||||
var self = this;
|
||||
let self = this;
|
||||
let galaxy = getGalaxyInstance();
|
||||
|
||||
// identify selector type identifier i.e. [ flavor ]_[ type ]_[ multiple ]
|
||||
var config_id =
|
||||
@@ -295,7 +299,7 @@ var View = Backbone.View.extend({
|
||||
this.config = Configurations[config_id];
|
||||
} else {
|
||||
this.config = Configurations["data"];
|
||||
Galaxy.emit.debug("ui-select-content::_changeType()", `Invalid configuration/type id '${config_id}'.`);
|
||||
galaxy.emit.debug("ui-select-content::_changeType()", `Invalid configuration/type id '${config_id}'.`);
|
||||
}
|
||||
|
||||
// prepare extension component of error message
|
||||
@@ -303,7 +307,7 @@ var View = Backbone.View.extend({
|
||||
var extensions = Utils.textify(this.model.get("extensions"));
|
||||
var src_labels = this.model.get("src_labels");
|
||||
|
||||
// build views
|
||||
// build radio button for data selectors
|
||||
this.fields = [];
|
||||
this.button_data = [];
|
||||
_.each(this.config, (c, i) => {
|
||||
@@ -329,24 +333,47 @@ var View = Backbone.View.extend({
|
||||
this.button_type = new Ui.RadioButton.View({
|
||||
value: this.model.get("current"),
|
||||
data: this.button_data,
|
||||
cls: "mr-2",
|
||||
onchange: function(value) {
|
||||
self.model.set("current", value);
|
||||
self.trigger("change");
|
||||
}
|
||||
});
|
||||
|
||||
// build data dialog button
|
||||
this.button_dialog = new Ui.Button({
|
||||
icon: "fa-folder-open-o",
|
||||
tooltip: "Browse Datasets",
|
||||
cls: "ml-2",
|
||||
onclick: () => {
|
||||
let current = this.model.get("current");
|
||||
let cnf = this.config[current];
|
||||
galaxy.data.dialog(
|
||||
response => {
|
||||
this._handleDropValues(response, false);
|
||||
},
|
||||
{
|
||||
multiple: cnf.multiple,
|
||||
format: null,
|
||||
library: false
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// append views
|
||||
this.$el.empty();
|
||||
var button_width = 0;
|
||||
if (this.fields.length > 1) {
|
||||
this.$el.append(this.button_type.$el);
|
||||
button_width = `${Math.max(0, this.fields.length * 40)}px`;
|
||||
}
|
||||
let $fields = $("<div/>").addClass("w-100");
|
||||
this.$el
|
||||
.empty()
|
||||
.addClass("d-flex flex-row")
|
||||
.append($("<div/>").append(this.button_type.$el))
|
||||
.append($fields)
|
||||
.append($("<div/>").append(this.button_dialog.$el));
|
||||
_.each(this.fields, field => {
|
||||
self.$el.append(field.$el.css({ "margin-left": button_width }));
|
||||
$fields.append(field.$el);
|
||||
});
|
||||
_.each(this.$batch, ($batchfield, batchmode) => {
|
||||
self.$el.append($batchfield.css({ "margin-left": button_width }));
|
||||
$fields.append($batchfield);
|
||||
});
|
||||
this.model.set("current", 0);
|
||||
this._changeCurrent();
|
||||
@@ -412,52 +439,62 @@ var View = Backbone.View.extend({
|
||||
}
|
||||
},
|
||||
|
||||
/** Handles drop events e.g. from history panel */
|
||||
_handleDrop: function(ev) {
|
||||
try {
|
||||
var data = this.model.get("data");
|
||||
var current = this.model.get("current");
|
||||
var config = this.config[current];
|
||||
var field = this.fields[current];
|
||||
var drop_data = JSON.parse(ev.dataTransfer.getData("text"))[0];
|
||||
var new_id = drop_data.id;
|
||||
var new_src = drop_data.history_content_type == "dataset_collection" ? "hdca" : "hda";
|
||||
var new_value = { id: new_id, src: new_src };
|
||||
if (data && drop_data.history_id) {
|
||||
if (!_.findWhere(data[new_src], new_value)) {
|
||||
data[new_src].push({
|
||||
id: new_id,
|
||||
src: new_src,
|
||||
hid: drop_data.hid || "Dropped",
|
||||
name: drop_data.hid ? drop_data.name : new_id,
|
||||
keep: true,
|
||||
tags: []
|
||||
});
|
||||
/** Source helper matches history_content_types to source types */
|
||||
_getSource: function(v) {
|
||||
return v.history_content_type == "dataset_collection" ? "hdca" : "hda";
|
||||
},
|
||||
|
||||
/** Add values from drag/drop */
|
||||
_handleDropValues: function(drop_data, drop_partial = true) {
|
||||
let data = this.model.get("data");
|
||||
let current = this.model.get("current");
|
||||
let config = this.config[current];
|
||||
let field = this.fields[current];
|
||||
if (data) {
|
||||
let values = $.isArray(drop_data) ? drop_data : [drop_data];
|
||||
if (values.length > 0) {
|
||||
let data_changed = false;
|
||||
_.each(values, v => {
|
||||
let new_id = v.id;
|
||||
let new_src = (v.src = this._getSource(v));
|
||||
let new_value = { id: new_id, src: new_src };
|
||||
if (!_.findWhere(data[new_src], new_value)) {
|
||||
data_changed = true;
|
||||
data[new_src].push({
|
||||
id: new_id,
|
||||
src: new_src,
|
||||
hid: v.hid || "Selected",
|
||||
name: v.hid ? v.name : new_id,
|
||||
keep: true,
|
||||
tags: []
|
||||
});
|
||||
}
|
||||
});
|
||||
if (data_changed) {
|
||||
this._changeData();
|
||||
}
|
||||
if (config.src == new_src) {
|
||||
let first_id = values[0].id;
|
||||
let first_src = values[0].src;
|
||||
if (config.src == first_src && drop_partial) {
|
||||
var current_value = field.value();
|
||||
if (current_value && config.multiple) {
|
||||
if (current_value.indexOf(new_id) == -1) {
|
||||
current_value.push(new_id);
|
||||
}
|
||||
_.each(values, v => {
|
||||
if (current_value.indexOf(v.id) == -1) {
|
||||
current_value.push(v.id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
current_value = new_id;
|
||||
current_value = first_id;
|
||||
}
|
||||
field.value(current_value);
|
||||
} else {
|
||||
this.model.set("value", { values: [new_value] });
|
||||
this.model.set("value", { values: values });
|
||||
this.model.trigger("change:value");
|
||||
}
|
||||
this.trigger("change");
|
||||
this._handleDropStatus("success");
|
||||
} else {
|
||||
this._handleDropStatus("danger");
|
||||
}
|
||||
} catch (e) {
|
||||
this._handleDropStatus("danger");
|
||||
}
|
||||
ev.preventDefault();
|
||||
this._handleDropStatus("success");
|
||||
},
|
||||
|
||||
/** Highlight drag result */
|
||||
@@ -487,6 +524,16 @@ var View = Backbone.View.extend({
|
||||
}
|
||||
}
|
||||
return result;
|
||||
},
|
||||
|
||||
/** Template for batch mode execution options */
|
||||
_templateBatch: function() {
|
||||
return `<div class="form-text text-muted" style="clear: both;">
|
||||
<i class="fa fa-sitemap"/>
|
||||
<span>
|
||||
This is a batch mode input field. Separate jobs will be triggered for each dataset selection.
|
||||
</span>
|
||||
</div>`;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -726,15 +726,18 @@ QUnit.test("select-content", function(assert) {
|
||||
"Contains " + options.totalmultiple + " multiselect fields"
|
||||
);
|
||||
assert.ok(
|
||||
select.$el.children(".ui-options").find(".ui-option").length ===
|
||||
(options.selectfields > 1 ? options.selectfields : 0),
|
||||
"Radio button count"
|
||||
select.$el.find(".ui-options:first .ui-option").length === options.selectfields,
|
||||
"Radio button count, expected " + options.selectfields
|
||||
);
|
||||
assert.ok(select.$(".ui-select:first").css("display") == "block", "Check select visibility");
|
||||
assert.ok(
|
||||
select.$(".ui-select:last").css("display") == (options.selectfields == 1 ? "block" : "none"),
|
||||
"Last select visibility"
|
||||
);
|
||||
/*assert.ok(
|
||||
(select.button_dialog.$el.css("display") != "none") === options.showdialog,
|
||||
"Data dialog button visible"
|
||||
);*/
|
||||
_testSelect("first", options);
|
||||
_testSelect("last", options);
|
||||
};
|
||||
@@ -759,7 +762,8 @@ QUnit.test("select-content", function(assert) {
|
||||
lastvalue: "id2",
|
||||
lastlabel: "hid2: name2",
|
||||
lastlength: 3,
|
||||
lastmultiple: false
|
||||
lastmultiple: false,
|
||||
showdialog: true
|
||||
};
|
||||
_test(initial);
|
||||
|
||||
@@ -775,7 +779,8 @@ QUnit.test("select-content", function(assert) {
|
||||
lastvalue: "id2",
|
||||
lastlabel: "hid2: name2",
|
||||
lastlength: 3,
|
||||
lastmultiple: true
|
||||
lastmultiple: true,
|
||||
showdialog: true
|
||||
});
|
||||
|
||||
select.model.set("multiple", false);
|
||||
@@ -790,7 +795,8 @@ QUnit.test("select-content", function(assert) {
|
||||
lastvalue: "id2",
|
||||
lastlabel: "hid2: name2",
|
||||
lastlength: 3,
|
||||
lastmultiple: false
|
||||
lastmultiple: false,
|
||||
showdialog: false
|
||||
});
|
||||
|
||||
select.model.set("type", "module_data_collection");
|
||||
@@ -804,7 +810,8 @@ QUnit.test("select-content", function(assert) {
|
||||
lastvalue: "id2",
|
||||
lastlabel: "hid2: name2",
|
||||
lastlength: 3,
|
||||
lastmultiple: true
|
||||
lastmultiple: true,
|
||||
showdialog: false
|
||||
});
|
||||
|
||||
select.model.set("type", "module_data");
|
||||
@@ -818,7 +825,8 @@ QUnit.test("select-content", function(assert) {
|
||||
lastvalue: "id0",
|
||||
lastlabel: "hid0: name0",
|
||||
lastlength: 2,
|
||||
lastmultiple: true
|
||||
lastmultiple: true,
|
||||
showdialog: true
|
||||
});
|
||||
|
||||
select.model.set("type", "data");
|
||||
|
||||
@@ -421,6 +421,9 @@ $ui-margin-horizontal-large: $margin-v * 2;
|
||||
-webkit-appearance: none;
|
||||
-moz-border-radius: $border-radius-base;
|
||||
line-height: 1.5rem;
|
||||
.select2-chosen {
|
||||
white-space: normal;
|
||||
}
|
||||
.select2-arrow {
|
||||
display: none;
|
||||
}
|
||||
@@ -449,12 +452,6 @@ $ui-margin-horizontal-large: $margin-v * 2;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-select-content {
|
||||
.ui-options {
|
||||
@extend .float-left;
|
||||
}
|
||||
}
|
||||
|
||||
.ui-dragover {
|
||||
border-radius: 3px;
|
||||
border: 2px solid $table-border-color;
|
||||
|
||||
Reference in New Issue
Block a user