diff --git a/client/src/bundleEntries.js b/client/src/bundleEntries.js index fd41c060c12..a6a83d2a68c 100644 --- a/client/src/bundleEntries.js +++ b/client/src/bundleEntries.js @@ -20,7 +20,6 @@ export { SweepsterVisualization, SweepsterVisualizationView } from "viz/sweepste export { createTabularDatasetChunkedView } from "mvc/dataset/data"; export { default as LegacyGridView } from "legacy/grid/grid-view"; export { create_chart, create_histogram } from "reports/run_stats"; -export { openGlobalUploadModal } from "components/Upload"; export { runTour } from "components/Tour/runTour"; export { Toast } from "ui/toast"; // TODO: remove when external consumers are updated/gone (IES right now) diff --git a/client/src/components/DataDialog/DataDialog.vue b/client/src/components/DataDialog/DataDialog.vue index e2e73ec52a7..fcb6fe3654c 100644 --- a/client/src/components/DataDialog/DataDialog.vue +++ b/client/src/components/DataDialog/DataDialog.vue @@ -46,7 +46,7 @@ import { UrlTracker } from "./utilities"; import { Model } from "./model"; import { Services } from "./services"; import { getAppRoot } from "onload/loadConfig"; -import { mountUploadModal } from "components/Upload"; +import { useGlobalUploadModal } from "composables/globalUploadModal"; Vue.use(BootstrapVue); @@ -78,6 +78,10 @@ export default { default: true, }, }, + setup() { + const { openGlobalUploadModal } = useGlobalUploadModal(); + return { openGlobalUploadModal }; + }, data() { return { errorMessage: null, @@ -133,7 +137,7 @@ export default { modalShow: true, selectable: true, }; - mountUploadModal(propsData); + this.openGlobalUploadModal(propsData); this.modalShow = false; }, /** Called when selection is complete, values are formatted and parsed to external callback **/ diff --git a/client/src/components/History/CurrentHistory/HistoryEmpty.vue b/client/src/components/History/CurrentHistory/HistoryEmpty.vue index bfd16dc946a..dbd74b8ef7a 100644 --- a/client/src/components/History/CurrentHistory/HistoryEmpty.vue +++ b/client/src/components/History/CurrentHistory/HistoryEmpty.vue @@ -13,14 +13,17 @@ + + + + diff --git a/client/src/components/Upload/UploadBoxMixin.js b/client/src/components/Upload/UploadBoxMixin.js index 35c611c6d2e..400657fb72b 100644 --- a/client/src/components/Upload/UploadBoxMixin.js +++ b/client/src/components/Upload/UploadBoxMixin.js @@ -289,6 +289,9 @@ export default { _eventCreate: function () { this.uploadbox.add([{ name: defaultNewFileName, size: 0, mode: "new" }]); }, + addFiles: function (files) { + this.uploadbox.add(files); + }, /** Pause upload process */ _eventStop: function () { if (this.counterRunning > 0) { diff --git a/client/src/components/Upload/UploadButton.vue b/client/src/components/Upload/UploadButton.vue index 3cd86885061..cfa90e5ae88 100644 --- a/client/src/components/Upload/UploadButton.vue +++ b/client/src/components/Upload/UploadButton.vue @@ -28,7 +28,8 @@ import { VBTooltip } from "bootstrap-vue"; import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; import { library } from "@fortawesome/fontawesome-svg-core"; import { faUpload } from "@fortawesome/free-solid-svg-icons"; -import { openGlobalUploadModal } from "./mount"; +import { useGlobalUploadModal } from "composables/globalUploadModal"; + library.add(faUpload); export default { @@ -39,6 +40,10 @@ export default { props: { title: { type: String, default: "Download from URL or upload files from disk" }, }, + setup() { + const { openGlobalUploadModal } = useGlobalUploadModal(); + return { openGlobalUploadModal }; + }, data() { return { status: "", @@ -58,7 +63,7 @@ export default { }, methods: { showUploadDialog() { - openGlobalUploadModal(); + this.openGlobalUploadModal(); }, setStatus(val) { this.status = val; diff --git a/client/src/components/Upload/UploadModal.test.js b/client/src/components/Upload/UploadModal.test.js index b528d74fcc4..8767da5f6ea 100644 --- a/client/src/components/Upload/UploadModal.test.js +++ b/client/src/components/Upload/UploadModal.test.js @@ -2,12 +2,12 @@ import MockAdapter from "axios-mock-adapter"; import axios from "axios"; import UploadModal from "./UploadModal"; import UploadModalContent from "./UploadModalContent"; -import store from "../../store"; import { mount } from "@vue/test-utils"; -import { getLocalVue } from "tests/jest/helpers"; - -import MockCurrentUser from "../providers/MockCurrentUser"; -import MockCurrentHistory from "../providers/MockCurrentHistory"; +import { getLocalVue, mockModule } from "tests/jest/helpers"; +import { userStore } from "store/userStore"; +import { historyStore } from "store/historyStore"; +import { configStore } from "store/configStore"; +import Vuex from "vuex"; jest.mock("app"); @@ -17,6 +17,16 @@ const propsData = { fileSourcesConfigured: true, }; +const createStore = () => { + return new Vuex.Store({ + modules: { + user: mockModule(userStore, { currentUser: { id: "fakeuser" } }), + history: mockModule(historyStore, { currentHistoryId: "fakehistory", histories: { fakehistory: {} } }), + config: mockModule(configStore, { config: {} }), + }, + }); +}; + describe("UploadModal.vue", () => { let wrapper; let axiosMock; @@ -47,17 +57,14 @@ describe("UploadModal.vue", () => { axiosMock.onGet(`/api/datatypes?extension_only=False`).reply(200, datatypesResponse); const localVue = getLocalVue(); + const store = createStore(); wrapper = await mount(UploadModal, { store, + provide: { store }, propsData, localVue, stubs: { - // Need to stub all this horrible-ness because of the last 2 tests - // which need to dig into the first layer of the mount tree, will remove - // all of this shortly with a PR that completely replaces Upload - CurrentUser: MockCurrentUser({ id: "fakeuser" }), - UserHistories: MockCurrentHistory({ id: "fakehistory" }), BTabs: true, BTab: true, Collection: true, @@ -66,6 +73,8 @@ describe("UploadModal.vue", () => { RulesInput: true, }, }); + + await wrapper.vm.open(); }); afterEach(() => { @@ -74,8 +83,9 @@ describe("UploadModal.vue", () => { }); it("should load with correct defaults", async () => { - expect(wrapper.vm.auto.id).toBe("auto"); - expect(wrapper.vm.datatypesDisableAuto).toBe(false); + const contentWrapper = wrapper.findComponent(UploadModalContent); + expect(contentWrapper.vm.auto.id).toBe("auto"); + expect(contentWrapper.vm.datatypesDisableAuto).toBe(false); }); it("should fetch datatypes and parse them", async () => { diff --git a/client/src/components/Upload/UploadModal.vue b/client/src/components/Upload/UploadModal.vue index 24849669c20..15fb8dfc368 100644 --- a/client/src/components/Upload/UploadModal.vue +++ b/client/src/components/Upload/UploadModal.vue @@ -1,87 +1,118 @@ - + - - diff --git a/client/src/components/Upload/UploadModalContent.vue b/client/src/components/Upload/UploadModalContent.vue index 94a1c036376..23fb5bac880 100644 --- a/client/src/components/Upload/UploadModalContent.vue +++ b/client/src/components/Upload/UploadModalContent.vue @@ -2,6 +2,7 @@ UploadUtils.AUTO_EXTENSION, + }, }, data: function () { return { @@ -180,6 +202,10 @@ export default { toData: function (items, history_id, composite = false) { return uploadModelsToPayload(items, history_id, composite); }, + immediateUpload: function (files) { + this.$refs.regular?.addFiles(files); + this.$refs.regular?._eventStart(); + }, }, }; diff --git a/client/src/components/Upload/config.js b/client/src/components/Upload/config.js deleted file mode 100644 index 4d2f01ae353..00000000000 --- a/client/src/components/Upload/config.js +++ /dev/null @@ -1,18 +0,0 @@ -import { getGalaxyInstance } from "app"; -import { getAppRoot } from "onload"; - -export function initializeUploadDefaults(propsData = {}) { - const Galaxy = getGalaxyInstance(); - const appRoot = getAppRoot(); - const defaults = { - multiple: true, - uploadPath: Galaxy.config.nginx_upload_path || `${appRoot}api/tools`, - chunkUploadSize: Galaxy.config.chunk_upload_size, - fileSourcesConfigured: Galaxy.config.file_sources_configured, - ftpUploadSite: Galaxy.config.ftp_upload_site, - defaultDbKey: Galaxy.config.default_genome, - defaultExtension: Galaxy.config.default_extension, - selectable: false, - }; - return Object.assign({}, defaults, propsData); -} diff --git a/client/src/components/Upload/helpers.js b/client/src/components/Upload/helpers.js index 5c83b9419e0..b5646be0ec1 100644 --- a/client/src/components/Upload/helpers.js +++ b/client/src/components/Upload/helpers.js @@ -1,4 +1,3 @@ -import UploadUtils from "mvc/upload/upload-utils"; export const defaultNewFileName = "New File"; const URI_PREFIXES = ["http", "https", "ftp", "file", "gxfiles", "gximport", "gxuserimport", "gxftp"]; @@ -109,58 +108,3 @@ export function uploadModelsToPayload(items, history_id, composite = false) { files: files, }; } - -export const commonProps = { - uploadPath: { - type: String, - required: true, - }, - chunkUploadSize: { - type: Number, - default: 1024, - }, - fileSourcesConfigured: { - type: Boolean, - default: false, - }, - ftpUploadSite: { - type: String, - default: "", - }, - defaultDbKey: { - type: String, - default: UploadUtils.DEFAULT_DBKEY, - }, - defaultExtension: { - type: String, - default: UploadUtils.DEFAULT_EXTENSION, - }, - datatypesDisableAuto: { - type: Boolean, - default: false, - }, - formats: { - type: Array, - default: null, - }, - multiple: { - // Restrict the forms to a single dataset upload if false - type: Boolean, - default: true, - }, - hasCallback: { - // Return uploads when done if supplied. - type: Boolean, - default: false, - }, - selectable: { - type: Boolean, - default: false, - }, - auto: { - type: Object, - default: () => { - return UploadUtils.AUTO_EXTENSION; - }, - }, -}; diff --git a/client/src/components/Upload/index.js b/client/src/components/Upload/index.js index 79c93e197fa..b83caf02a74 100644 --- a/client/src/components/Upload/index.js +++ b/client/src/components/Upload/index.js @@ -2,6 +2,4 @@ * External entry point for Upload components (currently only the modal). */ export { default as UploadModal } from "./UploadModal"; -export { initializeUploadDefaults } from "./config"; -export { openGlobalUploadModal, mountUploadModal } from "./mount"; export { default as UploadButton } from "./UploadButton"; diff --git a/client/src/components/Upload/mount.js b/client/src/components/Upload/mount.js deleted file mode 100644 index c94860a1f8a..00000000000 --- a/client/src/components/Upload/mount.js +++ /dev/null @@ -1,37 +0,0 @@ -import UploadModal from "./UploadModal"; -import { initializeUploadDefaults } from "./config"; -import { mountVueComponent } from "utils/mountVueComponent"; - -export function mountUploadModal(options = {}) { - const props = initializeUploadDefaults(options); - - // should use events insted of passing in functions - const { callback, ...propsData } = props; - if (callback) { - // internal display characteristic - propsData.hasCallback = true; - } - - const mounter = mountVueComponent(UploadModal); - const container = document.createElement("div"); - document.body.appendChild(container); - const uploadVm = mounter(propsData, container); - - if (callback) { - uploadVm.$once("uploadResult", callback); - } - - return uploadVm; -} - -// Global upload dialog instance -let uploadVm = null; - -export function openGlobalUploadModal(options) { - if (!uploadVm) { - uploadVm = mountUploadModal(options); - } - // re-open - uploadVm.$emit("openUpload", uploadVm); - return uploadVm; -} diff --git a/client/src/composables/fileDrop.js b/client/src/composables/fileDrop.js new file mode 100644 index 00000000000..76e8ed1609f --- /dev/null +++ b/client/src/composables/fileDrop.js @@ -0,0 +1,107 @@ +import { ref, unref } from "vue"; +import { useEventListener } from "@vueuse/core"; + +/** + * Custom File-Drop composable + * @param dropZone Element which files should be dropped on + * @param onDrop callback function called when drop occurs + * @param solo when true, only reacts if no modal is open + */ +export function useFileDrop(dropZone, onDrop, solo) { + const isFileOverDocument = ref(false); + const isFileOverDropZone = ref(false); + + const dragBlocked = ref(false); + + // Don't react to page-internal drag events + useEventListener( + document.body, + "dragstart", + () => { + dragBlocked.value = true; + }, + true + ); + + useEventListener( + document.body, + "dragover", + (event) => { + if (!dragBlocked.value) { + // prevent the browser from opening the file + event.preventDefault(); + } + }, + true + ); + + useEventListener( + document.body, + "drop", + (event) => { + if (!dragBlocked.value) { + // prevent the browser from opening the file + event.preventDefault(); + + if (isFileOverDropZone.value && isFileOverDocument.value) { + unref(onDrop)(event); + } + } + isFileOverDocument.value = false; + dragBlocked.value = false; + }, + true + ); + + useEventListener( + document.body, + "dragend", + () => { + // reset on drag end + isFileOverDocument.value = false; + isFileOverDropZone.value = false; + dragBlocked.value = false; + }, + true + ); + + useEventListener( + document.body, + "dragenter", + (event) => { + // init values if drag is possible + if (!dragBlocked.value && !(unref(solo) && isAnyModalOpen())) { + isFileOverDocument.value = true; + isFileOverDropZone.value = false; + + event.preventDefault(); + } + }, + true + ); + + /** returns if any bootstrap modal is open */ + function isAnyModalOpen() { + return document.querySelectorAll(".modal.show").length > 0; + } + + useEventListener( + dropZone, + "dragenter", + () => { + isFileOverDropZone.value = true; + }, + true + ); + + useEventListener( + dropZone, + "dragleave", + () => { + isFileOverDropZone.value = false; + }, + true + ); + + return { isFileOverDocument, isFileOverDropZone }; +} diff --git a/client/src/composables/globalUploadModal.js b/client/src/composables/globalUploadModal.js new file mode 100644 index 00000000000..9020d7c6daf --- /dev/null +++ b/client/src/composables/globalUploadModal.js @@ -0,0 +1,16 @@ +import { ref } from "vue"; + +let modal = ref(null); + +export function setGlobalUploadModal(modalRef) { + // eslint-disable-next-line vue/no-ref-as-operand + modal = modalRef; +} + +export function useGlobalUploadModal() { + const openGlobalUploadModal = (options) => { + modal.value?.open(options); + }; + + return { openGlobalUploadModal }; +} diff --git a/client/src/composables/userHistories.js b/client/src/composables/userHistories.js new file mode 100644 index 00000000000..9a5edd90f17 --- /dev/null +++ b/client/src/composables/userHistories.js @@ -0,0 +1,19 @@ +import { computed, watch, unref, inject } from "vue"; + +export function useUserHistories(user) { + const store = inject("store"); + + watch( + () => unref(user), + (newUser, oldUser) => { + if (newUser?.id !== oldUser?.id) { + store.dispatch("history/loadHistories"); + } + }, + { immediate: true } + ); + + const currentHistoryId = computed(() => store.getters["history/currentHistoryId"]); + + return { currentHistoryId }; +} diff --git a/client/src/entry/analysis/App.vue b/client/src/entry/analysis/App.vue index d282079c426..9eab4abab84 100644 --- a/client/src/entry/analysis/App.vue +++ b/client/src/entry/analysis/App.vue @@ -36,6 +36,7 @@
+