diff --git a/client/src/components/History/CurrentHistory/HistoryPanel.vue b/client/src/components/History/CurrentHistory/HistoryPanel.vue index cb453905505..37e6acb280d 100644 --- a/client/src/components/History/CurrentHistory/HistoryPanel.vue +++ b/client/src/components/History/CurrentHistory/HistoryPanel.vue @@ -367,21 +367,28 @@ export default { }, onDrop(evt) { this.showDropZone = false; - const data = JSON.parse(evt.dataTransfer.getData("text"))[0]; - const dataSource = data.history_content_type === "dataset" ? "hda" : "hdca"; - if (data.history_id != this.historyId) { - copyDataset(data.id, this.historyId, data.history_content_type, dataSource) - .then(() => { - if (data.history_content_type === "dataset") { - Toast.info("Dataset copied to history"); - } else { - Toast.info("Collection copied to history"); - } - this.loadHistoryById(this.historyId); - }) - .catch((error) => { - this.onError(error); - }); + let data; + try { + data = JSON.parse(evt.dataTransfer.getData("text"))[0]; + } catch (error) { + // this was not a valid object for this dropzone, ignore + } + if (data) { + const dataSource = data.history_content_type === "dataset" ? "hda" : "hdca"; + if (data.history_id != this.historyId) { + copyDataset(data.id, this.historyId, data.history_content_type, dataSource) + .then(() => { + if (data.history_content_type === "dataset") { + Toast.info("Dataset copied to history"); + } else { + Toast.info("Collection copied to history"); + } + this.loadHistoryById(this.historyId); + }) + .catch((error) => { + this.onError(error); + }); + } } }, onError(error) { diff --git a/client/src/composables/fileDrop.ts b/client/src/composables/fileDrop.ts index 0d71b92425b..5c281137695 100644 --- a/client/src/composables/fileDrop.ts +++ b/client/src/composables/fileDrop.ts @@ -1,6 +1,5 @@ -import { ref, unref, type Ref } from "vue"; +import { computed, ref, unref, type Ref } from "vue"; import { useEventListener, type MaybeComputedRef } from "@vueuse/core"; -import { wait } from "@/utils/wait"; export type FileDropHandler = (event: DragEvent) => void; @@ -9,114 +8,108 @@ export type FileDropHandler = (event: DragEvent) => void; * @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 + * @param idleTime how long to wait until state resets */ export function useFileDrop( dropZone: MaybeComputedRef, onDrop: Ref | FileDropHandler, - solo: MaybeComputedRef + solo: MaybeComputedRef, + idleTime = 800 ) { - const isFileOverDocument = ref(false); - const isFileOverDropZone = ref(false); - - // blocks drag events in this composable, to avoid drag events in unwanted situations - let dragBlocked = false; - - // keeps track if the drag has exited, to avoid premature drag canceling - let hasExited = true; - - // Don't react to page-internal drag events - useEventListener( - document.body, - "dragstart", - () => { - dragBlocked = true; - }, - true - ); - - useEventListener( - document.body, - "dragover", - (event) => { - if (!dragBlocked) { - // prevent the browser from opening the file - event.preventDefault(); - hasExited = false; - } - }, - true - ); - - useEventListener( - document.body, - "drop", - (event) => { - if (!dragBlocked) { - // prevent the browser from opening the file - event.preventDefault(); - - if (isFileOverDropZone.value && isFileOverDocument.value) { - const dropHandler = unref(onDrop); - dropHandler(event as DragEvent); - } - } - isFileOverDocument.value = false; - dragBlocked = false; - hasExited = true; - }, - true - ); - - /** Reset all variables */ - const reset = () => { - isFileOverDocument.value = false; - isFileOverDropZone.value = false; - dragBlocked = false; - hasExited = true; - }; - - useEventListener(document.body, "dragend", reset, true); - - useEventListener(document.body, "dragleave", async () => { - hasExited = true; - - // This event may have been triggered by components - // which have not been properly childed to the body yet. - // Wait a bit, and check if hasExited is still true. - await wait(100); - - if (hasExited) { - reset(); - } - }); - - useEventListener( - document.body, - "dragenter", - (event) => { - // init values if drag is possible - if (!dragBlocked && !(unref(solo) && isAnyModalOpen())) { - isFileOverDocument.value = true; - isFileOverDropZone.value = false; - hasExited = false; - - event.preventDefault(); - } - }, - true - ); - /** returns if any bootstrap modal is open */ function isAnyModalOpen() { return document.querySelectorAll(".modal.show").length > 0; } + type State = "idle" | "blocked" | "fileDragging"; + type StateMachine = { + [state in State]: (event: MouseEvent) => State; + }; + + const currentState: Ref = ref("idle"); + + let idleTimer: ReturnType | null = null; + const resetTimer = () => { + if (idleTimer) { + clearTimeout(idleTimer); + } + }; + + const stateMachine = { + idle(event: MouseEvent): State { + switch (event.type) { + case "dragstart": + return "blocked"; + case "dragenter": + if (!(unref(solo) && isAnyModalOpen())) { + return "fileDragging"; + } + break; + } + + return "idle"; + }, + blocked(event: MouseEvent): State { + switch (event.type) { + case "drop": + return "idle"; + case "dragend": + return "idle"; + } + + return "blocked"; + }, + fileDragging(event: MouseEvent): State { + resetTimer(); + + switch (event.type) { + case "dragover": + event.preventDefault(); + idleTimer = setTimeout(() => (currentState.value = "idle"), idleTime); + break; + case "drop": + event.preventDefault(); + if (isFileOverDropZone.value) { + const dropHandler = unref(onDrop); + dropHandler(event as DragEvent); + } + return "idle"; + case "dragend": + return "idle"; + } + + return "fileDragging"; + }, + } as const satisfies StateMachine; + + const eventHandler = (event: MouseEvent) => (currentState.value = stateMachine[currentState.value](event)); + + useEventListener(document.body, "dragstart", eventHandler, true); + useEventListener(document.body, "dragover", eventHandler, true); + useEventListener(document.body, "drop", eventHandler, true); + useEventListener(document.body, "dragend", eventHandler, true); + useEventListener(document.body, "dragenter", eventHandler, true); + + const isFileOverDocument = computed({ + get() { + return currentState.value === "fileDragging"; + }, + set(value) { + if (value !== true) { + currentState.value = "idle"; + } else { + currentState.value = "fileDragging"; + } + }, + }); + + const isFileOverDropZone = ref(false); + useEventListener( dropZone, "dragenter", () => { isFileOverDropZone.value = true; - hasExited = false; }, true ); @@ -126,7 +119,6 @@ export function useFileDrop( "dragleave", () => { isFileOverDropZone.value = false; - hasExited = false; }, true ); diff --git a/lib/galaxy/datatypes/converters/uncompressed_to_gz.xml b/lib/galaxy/datatypes/converters/uncompressed_to_gz.xml index e3b2db365d1..7cbdb7abb6c 100644 --- a/lib/galaxy/datatypes/converters/uncompressed_to_gz.xml +++ b/lib/galaxy/datatypes/converters/uncompressed_to_gz.xml @@ -1,19 +1,27 @@