mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-01 15:37:32 +08:00
Remove legacy upload components from client UI
This commit is contained in:
@@ -1,52 +0,0 @@
|
||||
import { getLocalVue } from "@tests/vitest/helpers";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import mountTarget from "./CompositeBox.vue";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
function getWrapper() {
|
||||
return mount(mountTarget, {
|
||||
propsData: {
|
||||
defaultDbKey: "?",
|
||||
effectiveExtensions: [
|
||||
{
|
||||
id: "affybatch",
|
||||
text: "affybatch",
|
||||
composite_files: [
|
||||
{
|
||||
name: "%s.pheno",
|
||||
optional: false,
|
||||
description: "Phenodata tab text file",
|
||||
},
|
||||
{
|
||||
name: "%s.affybatch",
|
||||
optional: false,
|
||||
description: "AffyBatch R object saved to file",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
fileSourcesConfigured: true,
|
||||
ftpUploadSite: null,
|
||||
historyId: "historyId",
|
||||
listDbKeys: [],
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
}
|
||||
|
||||
describe("Composite", () => {
|
||||
it("rendering", async () => {
|
||||
const wrapper = getWrapper();
|
||||
expect(wrapper.find("#btn-start").classes()).toEqual(expect.arrayContaining(["disabled"]));
|
||||
expect(wrapper.vm.showHelper).toBe(true);
|
||||
expect(wrapper.vm.enableStart).toBe(false);
|
||||
const extensions = wrapper.vm.listExtensions;
|
||||
expect(extensions.length).toBe(2);
|
||||
expect(extensions[0].id).toBe(null);
|
||||
expect(extensions[0].text).toBe("Select");
|
||||
expect(extensions[1].id).toBe("affybatch");
|
||||
});
|
||||
});
|
||||
@@ -1,227 +0,0 @@
|
||||
<script setup>
|
||||
import { BButton } from "bootstrap-vue";
|
||||
import Vue, { computed, ref } from "vue";
|
||||
|
||||
import { buildLegacyPayload, submitUpload } from "@/utils/upload";
|
||||
|
||||
import { defaultModel } from "./model";
|
||||
|
||||
import CompositeRow from "./CompositeRow.vue";
|
||||
import UploadSelect from "./UploadSelect.vue";
|
||||
|
||||
const props = defineProps({
|
||||
defaultDbKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
effectiveExtensions: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
fileSourcesConfigured: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
ftpUploadSite: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
hasCallback: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
historyId: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
listDbKeys: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const extension = ref(null);
|
||||
const dbKey = ref(props.defaultDbKey);
|
||||
const uploadItems = ref({});
|
||||
|
||||
const enableStart = computed(() => {
|
||||
const incomplete = uploadValues.value.find((v) => v.status === "init" && !v.optional && v.fileSize === 0);
|
||||
return !isRunning.value && uploadValues.value.length > 0 && !incomplete;
|
||||
});
|
||||
|
||||
const hasRemoteFiles = computed(() => props.fileSourcesConfigured || !!props.ftpUploadSite);
|
||||
|
||||
const isRunning = computed(() => {
|
||||
const model = uploadKeys.value[0];
|
||||
return model && model.status === "running";
|
||||
});
|
||||
|
||||
const listExtensions = computed(() => {
|
||||
const result = props.effectiveExtensions.filter((ext) => ext.composite_files);
|
||||
result.unshift({ id: null, text: "Select" });
|
||||
return result;
|
||||
});
|
||||
|
||||
const showHelper = computed(() => uploadKeys.value.length === 0);
|
||||
const uploadValues = computed(() => Object.values(uploadItems.value));
|
||||
const uploadKeys = computed(() => Object.keys(uploadItems.value));
|
||||
|
||||
/** Refresh error state */
|
||||
function eventError(message) {
|
||||
uploadValues.value.forEach((model) => {
|
||||
model.info = message;
|
||||
model.status = "error";
|
||||
});
|
||||
}
|
||||
|
||||
/** Update model */
|
||||
function eventInput(index, newData) {
|
||||
const it = uploadItems.value[index];
|
||||
Object.entries(newData).forEach(([key, value]) => {
|
||||
it[key] = value;
|
||||
});
|
||||
restoreStatus();
|
||||
}
|
||||
|
||||
/** Refresh progress state */
|
||||
function eventProgress(percentage) {
|
||||
uploadValues.value.forEach((model) => {
|
||||
model.percentage = percentage;
|
||||
});
|
||||
}
|
||||
|
||||
/** Remove all */
|
||||
function eventReset() {
|
||||
if (!uploadValues.value.find((v) => v.status === "running")) {
|
||||
uploadItems.value = {};
|
||||
dbKey.value = props.defaultDbKey;
|
||||
}
|
||||
}
|
||||
|
||||
/** Start upload process */
|
||||
function eventStart() {
|
||||
uploadValues.value.forEach((model) => {
|
||||
model.dbKey = dbKey.value;
|
||||
model.extension = extension.value;
|
||||
});
|
||||
try {
|
||||
submitUpload({
|
||||
data: buildLegacyPayload(uploadValues.value, props.historyId, true),
|
||||
error: eventError,
|
||||
progress: eventProgress,
|
||||
success: eventSuccess,
|
||||
isComposite: true,
|
||||
});
|
||||
} catch (e) {
|
||||
eventError(String(e));
|
||||
}
|
||||
}
|
||||
|
||||
/** Refresh success state */
|
||||
function eventSuccess() {
|
||||
uploadValues.value.forEach((model) => {
|
||||
model.percentage = 100;
|
||||
model.status = "success";
|
||||
});
|
||||
}
|
||||
|
||||
function inputDbkey(newDbkey) {
|
||||
dbKey.value = newDbkey;
|
||||
restoreStatus();
|
||||
}
|
||||
|
||||
function inputExtension(newExtension) {
|
||||
extension.value = newExtension;
|
||||
uploadItems.value = {};
|
||||
let uploadCount = 0;
|
||||
const extensionDetails = listExtensions.value.find((v) => v.id === newExtension);
|
||||
if (extensionDetails && extensionDetails.composite_files) {
|
||||
extensionDetails.composite_files.forEach((item) => {
|
||||
const index = String(uploadCount++);
|
||||
const uploadModel = {
|
||||
...defaultModel,
|
||||
description: item.description || item.name,
|
||||
optional: item.optional,
|
||||
};
|
||||
Vue.set(uploadItems.value, index, uploadModel);
|
||||
});
|
||||
}
|
||||
restoreStatus();
|
||||
}
|
||||
|
||||
/** Refresh init state if any user changes attributes */
|
||||
function restoreStatus() {
|
||||
uploadValues.value.forEach((model) => {
|
||||
model.percentage = 0;
|
||||
model.status = "init";
|
||||
});
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
enableStart,
|
||||
listExtensions,
|
||||
showHelper,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="upload-wrapper">
|
||||
<div class="upload-header"> </div>
|
||||
<div class="upload-box">
|
||||
<div v-show="showHelper" v-localize class="upload-helper">Select a composite type</div>
|
||||
<div v-show="!showHelper">
|
||||
<CompositeRow
|
||||
v-for="(uploadItem, uploadIndex) in uploadItems"
|
||||
:key="uploadIndex"
|
||||
:index="uploadIndex"
|
||||
:file-description="uploadItem.description"
|
||||
:file-content="uploadItem.fileContent"
|
||||
:file-mode="uploadItem.fileMode"
|
||||
:file-name="uploadItem.fileName"
|
||||
:file-size="uploadItem.fileSize"
|
||||
:info="uploadItem.info"
|
||||
:has-remote-files="hasRemoteFiles"
|
||||
:optional="uploadItem.optional"
|
||||
:percentage="uploadItem.percentage"
|
||||
:space-to-tab="uploadItem.spaceToTab"
|
||||
:status="uploadItem.status"
|
||||
:to-posix-lines="uploadItem.toPosixLines"
|
||||
@input="eventInput" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="upload-footer">
|
||||
<span v-localize class="upload-footer-title">Composite Type:</span>
|
||||
<UploadSelect
|
||||
class="upload-footer-extension"
|
||||
:value="null"
|
||||
:options="listExtensions"
|
||||
:disabled="isRunning"
|
||||
what="file type"
|
||||
@input="inputExtension" />
|
||||
<span v-localize class="upload-footer-title">Reference:</span>
|
||||
<UploadSelect
|
||||
what="reference"
|
||||
:value="dbKey"
|
||||
:options="listDbKeys"
|
||||
:disabled="isRunning"
|
||||
@input="inputDbkey" />
|
||||
</div>
|
||||
<div class="upload-buttons d-flex justify-content-end">
|
||||
<BButton
|
||||
id="btn-start"
|
||||
:disabled="!enableStart"
|
||||
title="Start"
|
||||
:variant="enableStart ? 'primary' : null"
|
||||
@click="eventStart">
|
||||
<span v-localize>Start</span>
|
||||
</BButton>
|
||||
<BButton id="btn-reset" title="Reset" @click="eventReset">
|
||||
<span v-localize>Reset</span>
|
||||
</BButton>
|
||||
<BButton id="btn-close" title="Close" @click="$emit('dismiss')">
|
||||
<span v-if="hasCallback" v-localize>Close</span>
|
||||
<span v-else v-localize>Cancel</span>
|
||||
</BButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,225 +0,0 @@
|
||||
<script setup>
|
||||
import {
|
||||
faCheck,
|
||||
faEdit,
|
||||
faExclamation,
|
||||
faExclamationTriangle,
|
||||
faFolderOpen,
|
||||
faLaptop,
|
||||
faSpinner,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { BDropdown, BDropdownItem } from "bootstrap-vue";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import { filesDialog } from "@/utils/dataModals";
|
||||
import { bytesToString } from "@/utils/utils";
|
||||
|
||||
import { DEFAULT_FILE_NAME } from "./utils";
|
||||
|
||||
import UploadSettings from "./UploadSettings.vue";
|
||||
|
||||
const props = defineProps({
|
||||
fileContent: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fileDescription: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
fileMode: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fileName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
fileSize: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
index: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
info: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
hasRemoteFiles: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
optional: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
percentage: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
spaceToTab: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
toPosixLines: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["input"]);
|
||||
|
||||
const isDisabled = computed(() => props.status === "running");
|
||||
const isDragging = ref(false);
|
||||
const uploadFile = ref(null);
|
||||
|
||||
function inputFileContent(newFileContent) {
|
||||
emit("input", props.index, {
|
||||
fileContent: newFileContent,
|
||||
fileSize: newFileContent.length,
|
||||
});
|
||||
}
|
||||
|
||||
function inputDialog(files) {
|
||||
if (files && files.length > 0) {
|
||||
emit("input", props.index, {
|
||||
fileData: files[0],
|
||||
fileMode: "local",
|
||||
fileName: files[0].name,
|
||||
filePath: null,
|
||||
fileSize: files[0].size,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function inputPaste() {
|
||||
emit("input", props.index, {
|
||||
fileData: null,
|
||||
fileMode: "new",
|
||||
fileName: DEFAULT_FILE_NAME,
|
||||
filePath: null,
|
||||
fileSize: 0,
|
||||
});
|
||||
}
|
||||
|
||||
/** Show remote files dialog or FTP files */
|
||||
function inputRemoteFiles() {
|
||||
filesDialog(
|
||||
(item) => {
|
||||
emit("input", props.index, {
|
||||
fileData: null,
|
||||
fileMode: "url",
|
||||
fileName: item.label,
|
||||
filePath: item.url,
|
||||
fileSize: item.size,
|
||||
});
|
||||
},
|
||||
{ multiple: false },
|
||||
);
|
||||
}
|
||||
|
||||
function inputSettings(settingId) {
|
||||
const newSettings = {};
|
||||
newSettings[settingId] = !props[settingId];
|
||||
emit("input", props.index, newSettings);
|
||||
}
|
||||
|
||||
/** Handle files dropped into the upload row **/
|
||||
function onDrop(evt) {
|
||||
isDragging.value = false;
|
||||
const droppedFile = evt.dataTransfer && evt.dataTransfer.files && evt.dataTransfer.files[0];
|
||||
if (droppedFile) {
|
||||
emit("input", props.index, {
|
||||
fileData: droppedFile,
|
||||
fileMode: "local",
|
||||
fileName: droppedFile.name,
|
||||
filePath: null,
|
||||
fileSize: droppedFile.size,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:id="`upload-row-${index}`"
|
||||
class="upload-row rounded my-1 p-2"
|
||||
:class="[`upload-${status}`, isDragging && 'bg-warning']"
|
||||
@dragover.prevent="isDragging = true"
|
||||
@dragleave.prevent="isDragging = false"
|
||||
@drop.prevent="onDrop">
|
||||
<div class="d-flex justify-content-around">
|
||||
<div>
|
||||
<BDropdown
|
||||
:id="`upload-type-${index}`"
|
||||
class="upload-source"
|
||||
:disabled="isDisabled"
|
||||
text="Select"
|
||||
:variant="fileSize > 0 ? 'secondary' : 'primary'">
|
||||
<BDropdownItem @click="uploadFile.click()">
|
||||
<FontAwesomeIcon :icon="faLaptop" />
|
||||
<span v-localize>Choose local file</span>
|
||||
</BDropdownItem>
|
||||
<BDropdownItem v-if="hasRemoteFiles" @click="inputRemoteFiles">
|
||||
<FontAwesomeIcon :icon="faFolderOpen" />
|
||||
<span v-localize>Choose from repository</span>
|
||||
</BDropdownItem>
|
||||
<BDropdownItem @click="inputPaste">
|
||||
<FontAwesomeIcon :icon="faEdit" />
|
||||
<span v-localize>Paste/Fetch data</span>
|
||||
</BDropdownItem>
|
||||
</BDropdown>
|
||||
</div>
|
||||
<div class="upload-title">
|
||||
{{ fileDescription }}
|
||||
</div>
|
||||
<div class="upload-title">
|
||||
{{ fileName || "-" }}
|
||||
</div>
|
||||
<div class="upload-size">
|
||||
{{ bytesToString(fileSize) }}
|
||||
</div>
|
||||
<UploadSettings
|
||||
:disabled="isDisabled"
|
||||
:to-posix-lines="toPosixLines"
|
||||
:space-to-tab="spaceToTab"
|
||||
@input="inputSettings" />
|
||||
<div class="upload-progress">
|
||||
<div class="progress">
|
||||
<div
|
||||
class="upload-progress-bar progress-bar progress-bar-success"
|
||||
:style="{ width: `${percentage}%` }" />
|
||||
<div class="upload-percentage">{{ percentage }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<FontAwesomeIcon v-if="['running', 'queued'].includes(status)" :icon="faSpinner" spin fixed-width />
|
||||
<FontAwesomeIcon v-else-if="status === 'error'" :icon="faExclamationTriangle" fixed-width />
|
||||
<FontAwesomeIcon v-else-if="fileSize > 0" :icon="faCheck" fixed-width />
|
||||
<FontAwesomeIcon v-else-if="optional" class="text-info" :icon="faCheck" fixed-width />
|
||||
<FontAwesomeIcon v-else :icon="faExclamation" fixed-width />
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="info" v-localize class="upload-text-message font-weight-bold">
|
||||
{{ info }}
|
||||
</div>
|
||||
<div v-if="fileMode == 'new'">
|
||||
<div class="upload-text-message">
|
||||
Download data from the web by entering URLs (one per line) or directly paste content.
|
||||
</div>
|
||||
<b-textarea
|
||||
:value="fileContent"
|
||||
class="upload-text-content form-control"
|
||||
:disabled="isDisabled"
|
||||
@input="inputFileContent" />
|
||||
</div>
|
||||
<input ref="uploadFile" type="file" @change="inputDialog($event.target.files)" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,101 +0,0 @@
|
||||
import { createTestingPinia } from "@pinia/testing";
|
||||
import { getLocalVue } from "@tests/vitest/helpers";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import flushPromises from "flush-promises";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import DefaultBox from "./DefaultBox.vue";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
type IntersectionObserverType = {
|
||||
new (callback: IntersectionObserverCallback, options?: IntersectionObserverInit): IntersectionObserver;
|
||||
prototype: IntersectionObserver;
|
||||
};
|
||||
|
||||
function getWrapper() {
|
||||
return mount(DefaultBox as object, {
|
||||
propsData: {
|
||||
chunkUploadSize: 100,
|
||||
defaultDbKey: "?",
|
||||
defaultExtension: "auto",
|
||||
effectiveExtensions: [{ id: "ab1" }],
|
||||
fileSourcesConfigured: true,
|
||||
ftpUploadSite: null,
|
||||
historyId: "historyId",
|
||||
lazyLoad: 3,
|
||||
listDbKeys: [],
|
||||
},
|
||||
localVue,
|
||||
stubs: {
|
||||
FontAwesomeIcon: true,
|
||||
},
|
||||
pinia: createTestingPinia({ createSpy: vi.fn }),
|
||||
});
|
||||
}
|
||||
|
||||
describe("Default", () => {
|
||||
let UnpatchedIntersectionObserver: IntersectionObserverType;
|
||||
|
||||
beforeEach(() => {
|
||||
UnpatchedIntersectionObserver = global.IntersectionObserver;
|
||||
|
||||
// The use of b-textarea in this DefaultRow causes the following warning:
|
||||
// [Vue warn]: Error in directive b-visible unbind hook: "TypeError: this.observer.disconnect is not a function"
|
||||
// I don't think there is a problem with the usage so I think this a bug in bootstrap vue, it can be worked around
|
||||
// with the following code - but just suppressing the warning is probably better?
|
||||
const observerMock = vi.fn(function IntersectionObserver(this: any, callback: IntersectionObserverCallback) {
|
||||
this.observe = vi.fn();
|
||||
this.disconnect = vi.fn();
|
||||
this.trigger = (mockedMutationsList: IntersectionObserverEntry[]) => {
|
||||
callback(mockedMutationsList, this);
|
||||
};
|
||||
});
|
||||
global.IntersectionObserver = observerMock as unknown as IntersectionObserverType;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
global.IntersectionObserver = UnpatchedIntersectionObserver;
|
||||
});
|
||||
|
||||
it("rendering", async () => {
|
||||
const wrapper = getWrapper();
|
||||
expect((wrapper.vm as any).counterAnnounce).toBe(0);
|
||||
expect((wrapper.vm as any).showHelper).toBe(true);
|
||||
expect((wrapper.vm as any).listExtensions[0].id).toBe("ab1");
|
||||
expect(wrapper.find("#btn-reset").classes()).toEqual(expect.arrayContaining(["g-disabled"]));
|
||||
expect(wrapper.find("#btn-start").classes()).toEqual(expect.arrayContaining(["g-disabled"]));
|
||||
expect(wrapper.find("#btn-stop").classes()).toEqual(expect.arrayContaining(["g-disabled"]));
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("resets properly", async () => {
|
||||
const wrapper = getWrapper();
|
||||
expect((wrapper.vm as any).showHelper).toBe(true);
|
||||
await wrapper.find("#btn-new").trigger("click");
|
||||
expect((wrapper.vm as any).showHelper).toBe(false);
|
||||
expect((wrapper.vm as any).counterAnnounce).toBe(1);
|
||||
await wrapper.find("#btn-reset").trigger("click");
|
||||
expect((wrapper.vm as any).showHelper).toBe(true);
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("does render remote files / repository button", async () => {
|
||||
const wrapper = getWrapper();
|
||||
expect(wrapper.find("#btn-remote-files").exists()).toBeTruthy();
|
||||
await wrapper.setProps({ fileSourcesConfigured: false });
|
||||
expect(wrapper.find("#btn-remote-files").exists()).toBeFalsy();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
it("renders a limited set", async () => {
|
||||
const wrapper = getWrapper();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await wrapper.find("#btn-new").trigger("click");
|
||||
}
|
||||
expect(wrapper.findAll(".upload-row").length).toBe(3);
|
||||
const textMessage = wrapper.find("[data-description='lazyload message']");
|
||||
expect(textMessage.text()).toBe("Only showing first 3 of 5 entries.");
|
||||
await flushPromises();
|
||||
});
|
||||
});
|
||||
@@ -1,600 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { faCopy, faEdit, faFolderOpen, faLaptop } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { BBadge } from "bootstrap-vue";
|
||||
import Vue, { computed, type Ref, ref } from "vue";
|
||||
import { useRouter } from "vue-router/composables";
|
||||
|
||||
import type { HDASummary } from "@/api";
|
||||
import type { CollectionBuilderType } from "@/components/Collections/common/buildCollectionModal";
|
||||
import type { SelectionItem } from "@/components/SelectionDialog/selectionTypes";
|
||||
import { monitorUploadedHistoryItems } from "@/composables/monitorUploadedHistoryItems";
|
||||
import type { DbKey, ExtensionDetails } from "@/composables/uploadConfigurations";
|
||||
import { archiveExplorerEventBus, type ArchiveSource } from "@/composables/zipExplorer";
|
||||
import { useActivityStore } from "@/stores/activityStore";
|
||||
import { filesDialog } from "@/utils/dataModals";
|
||||
import { UploadQueue } from "@/utils/upload-queue.js";
|
||||
|
||||
import type { ComponentSize } from "../BaseComponents/componentVariants";
|
||||
import type { UploadFile, UploadRowModel } from "./model";
|
||||
import { defaultModel, isLocalFile } from "./model";
|
||||
import { COLLECTION_TYPES, DEFAULT_FILE_NAME, hasBrowserSupport } from "./utils";
|
||||
|
||||
import GButton from "../BaseComponents/GButton.vue";
|
||||
import DefaultRow from "./DefaultRow.vue";
|
||||
import UploadBox from "./UploadBox.vue";
|
||||
import UploadSelect from "./UploadSelect.vue";
|
||||
import UploadSelectExtension from "./UploadSelectExtension.vue";
|
||||
import CollectionCreatorIndex from "@/components/Collections/CollectionCreatorIndex.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const activityStore = useActivityStore("default");
|
||||
|
||||
interface Props {
|
||||
chunkUploadSize: number;
|
||||
defaultDbKey: string;
|
||||
defaultExtension: string;
|
||||
effectiveExtensions: ExtensionDetails[];
|
||||
fileSourcesConfigured: boolean;
|
||||
ftpUploadSite?: string;
|
||||
historyId: string;
|
||||
multiple?: boolean;
|
||||
hasCallback?: boolean;
|
||||
lazyLoad?: number;
|
||||
listDbKeys: DbKey[];
|
||||
isCollection?: boolean;
|
||||
disableFooter?: boolean;
|
||||
emitUploaded?: boolean;
|
||||
size?: ComponentSize;
|
||||
showUploadActivity?: boolean;
|
||||
showBetaUpload?: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
ftpUploadSite: undefined,
|
||||
multiple: true,
|
||||
lazyLoad: 150,
|
||||
size: "medium",
|
||||
isCollection: false,
|
||||
showUploadActivity: true,
|
||||
showBetaUpload: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "dismiss"): void;
|
||||
(e: "progress", value: number | null, variant?: string): void;
|
||||
(e: "uploaded", value: HDASummary[]): void;
|
||||
}>();
|
||||
|
||||
const collectionModalShow = ref(false);
|
||||
const collectionType = ref<CollectionBuilderType>("list");
|
||||
const counterAnnounce = ref(0);
|
||||
const counterError = ref(0);
|
||||
const counterRunning = ref(0);
|
||||
const counterSuccess = ref(0);
|
||||
const extension = ref(props.defaultExtension);
|
||||
const dbKey = ref(props.defaultDbKey);
|
||||
const queueStopping = ref(false);
|
||||
const uploadCompleted = ref(0);
|
||||
const uploadFile = ref<HTMLInputElement | null>(null);
|
||||
const uploadItems = ref<Record<string, UploadRowModel>>({});
|
||||
const uploadSize = ref(0);
|
||||
const queue = ref(createUploadQueue());
|
||||
const selectedItemsForModal = ref<HDASummary[]>([]);
|
||||
|
||||
const counterNonRunning = computed(() => counterAnnounce.value + counterSuccess.value + counterError.value);
|
||||
const creatingPairedType = computed(() => props.isCollection && collectionType.value === "list:paired");
|
||||
const enableBuild = computed(
|
||||
() =>
|
||||
!isRunning.value &&
|
||||
counterAnnounce.value == 0 &&
|
||||
counterSuccess.value > 0 &&
|
||||
uploadedHistoryItemsReady.value &&
|
||||
uploadedHistoryItemsOk.value.length > 0 &&
|
||||
(!creatingPairedType.value || uploadedHistoryItemsOk.value.length % 2 === 0),
|
||||
);
|
||||
const enableReset = computed(() => !isRunning.value && counterNonRunning.value > 0);
|
||||
const enableStart = computed(() => !isRunning.value && counterAnnounce.value > 0);
|
||||
const enableSources = computed(() => !isRunning.value && (props.multiple || counterNonRunning.value == 0));
|
||||
const isRunning = computed(() => counterRunning.value > 0);
|
||||
const hasRemoteFiles = computed(() => props.fileSourcesConfigured || !!props.ftpUploadSite);
|
||||
const historyId = computed(() => props.historyId);
|
||||
const listExtensions = computed(() => props.effectiveExtensions.filter((ext) => !ext.composite_files));
|
||||
const showHelper = computed(() => Object.keys(uploadItems.value).length === 0);
|
||||
const uploadValues = computed(() => Object.values(uploadItems.value));
|
||||
|
||||
const { uploadedHistoryItemsOk, uploadedHistoryItemsReady, historyItemsStateInfo } = monitorUploadedHistoryItems(
|
||||
uploadValues as Ref<UploadRowModel[]>,
|
||||
historyId,
|
||||
enableStart,
|
||||
creatingPairedType,
|
||||
);
|
||||
|
||||
function createUploadQueue() {
|
||||
return new UploadQueue({
|
||||
announce: eventAnnounce,
|
||||
chunkSize: props.chunkUploadSize,
|
||||
complete: eventComplete,
|
||||
error: eventError,
|
||||
get: (index: string) => uploadItems.value[index],
|
||||
multiple: props.multiple,
|
||||
progress: eventProgress,
|
||||
success: eventSuccess,
|
||||
warning: eventWarning,
|
||||
});
|
||||
}
|
||||
|
||||
/** Add files to queue */
|
||||
function addFiles(files: FileList, immediate = false) {
|
||||
if (!isRunning.value) {
|
||||
if (immediate || !props.multiple) {
|
||||
eventReset();
|
||||
}
|
||||
if (props.multiple) {
|
||||
queue.value.add(files);
|
||||
} else if (files.length > 0) {
|
||||
queue.value.add([files[0]]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addFileFromInput(eventTarget: EventTarget | null) {
|
||||
if (!eventTarget) {
|
||||
return;
|
||||
}
|
||||
const { files } = eventTarget as HTMLInputElement;
|
||||
if (files) {
|
||||
addFiles(files);
|
||||
}
|
||||
}
|
||||
|
||||
/** A new file has been announced to the upload queue */
|
||||
function eventAnnounce(index: string, file: UploadFile) {
|
||||
counterAnnounce.value++;
|
||||
const mode = file.mode || "local";
|
||||
let deferred: boolean | undefined = false;
|
||||
if (mode === "local") {
|
||||
deferred = undefined;
|
||||
}
|
||||
const uploadModel = {
|
||||
...defaultModel,
|
||||
id: index,
|
||||
dbKey: dbKey.value,
|
||||
extension: extension.value,
|
||||
fileData: file,
|
||||
fileMode: mode,
|
||||
deferred: deferred,
|
||||
fileName: file.name,
|
||||
filePath: file.path,
|
||||
fileSize: file.size,
|
||||
fileUri: file.uri,
|
||||
};
|
||||
Vue.set(uploadItems.value, index, uploadModel);
|
||||
}
|
||||
|
||||
/** Populates and opens collection builder with uploaded files, or emits uploads */
|
||||
async function eventBuild(openModal = false) {
|
||||
if (openModal) {
|
||||
selectedItemsForModal.value = uploadedHistoryItemsOk.value;
|
||||
collectionModalShow.value = true;
|
||||
} else {
|
||||
emit("uploaded", uploadedHistoryItemsOk.value);
|
||||
counterRunning.value = 0;
|
||||
eventReset();
|
||||
emit("dismiss");
|
||||
}
|
||||
}
|
||||
|
||||
/** Queue is done */
|
||||
function eventComplete() {
|
||||
uploadValues.value.forEach((model) => {
|
||||
if (model.status === "queued") {
|
||||
model.status = "init";
|
||||
}
|
||||
});
|
||||
counterRunning.value = 0;
|
||||
queueStopping.value = false;
|
||||
}
|
||||
|
||||
/** Create a new file */
|
||||
function eventCreate() {
|
||||
queue.value.add([{ name: DEFAULT_FILE_NAME, size: 0, mode: "new" }]);
|
||||
}
|
||||
|
||||
/** Error */
|
||||
function eventError(index: string, message: string) {
|
||||
const it = uploadItems.value[index];
|
||||
if (it) {
|
||||
it.percentage = 100;
|
||||
it.status = "error";
|
||||
it.info = message;
|
||||
uploadCompleted.value += it.fileSize * 100;
|
||||
counterAnnounce.value--;
|
||||
counterError.value++;
|
||||
emit("progress", uploadPercentage(100, it.fileSize), "danger");
|
||||
}
|
||||
}
|
||||
|
||||
/** Update model */
|
||||
function eventInput(index: string, newData: Partial<UploadRowModel>) {
|
||||
const it = uploadItems.value[index];
|
||||
if (it) {
|
||||
Object.entries(newData).forEach(([key, value]) => {
|
||||
(it as any)[key] = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Reflect upload progress */
|
||||
function eventProgress(index: string, percentage: number) {
|
||||
const it = uploadItems.value[index];
|
||||
if (it) {
|
||||
it.percentage = percentage;
|
||||
emit("progress", uploadPercentage(percentage, it.fileSize));
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove model from upload list */
|
||||
function eventRemove(index: string) {
|
||||
const it = uploadItems.value[index];
|
||||
if (it) {
|
||||
var status = it.status;
|
||||
if (status == "success") {
|
||||
counterSuccess.value--;
|
||||
} else if (status == "error") {
|
||||
counterError.value--;
|
||||
} else {
|
||||
counterAnnounce.value--;
|
||||
}
|
||||
Vue.delete(uploadItems.value, index);
|
||||
queue.value.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
async function eventExplore(archiveSource: ArchiveSource) {
|
||||
await router.push({ name: "ZipImportWizard" });
|
||||
archiveExplorerEventBus.emit("set-archive-source", archiveSource);
|
||||
emit("dismiss");
|
||||
}
|
||||
|
||||
/** Show remote files dialog or FTP files */
|
||||
function eventRemoteFiles() {
|
||||
filesDialog(
|
||||
(items: SelectionItem[]) => {
|
||||
queue.value.add(
|
||||
items.map((item) => {
|
||||
const rval = {
|
||||
mode: "url",
|
||||
name: item.label,
|
||||
size: item.entry.size,
|
||||
path: item.url,
|
||||
hashes: item.entry.hashes,
|
||||
};
|
||||
return rval;
|
||||
}),
|
||||
);
|
||||
},
|
||||
{ multiple: true },
|
||||
(route: string) => {
|
||||
router.push(route);
|
||||
emit("dismiss");
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/** Remove all */
|
||||
function eventReset() {
|
||||
if (!isRunning.value) {
|
||||
counterAnnounce.value = 0;
|
||||
counterSuccess.value = 0;
|
||||
counterError.value = 0;
|
||||
queue.value.reset();
|
||||
uploadItems.value = {};
|
||||
extension.value = props.defaultExtension;
|
||||
dbKey.value = props.defaultDbKey;
|
||||
emit("progress", 0);
|
||||
}
|
||||
}
|
||||
|
||||
/** Success */
|
||||
function eventSuccess(index: string, incoming: any) {
|
||||
var it = uploadItems.value[index];
|
||||
if (it) {
|
||||
it.percentage = 100;
|
||||
it.status = "success";
|
||||
it.outputs = incoming.outputs || incoming.data.outputs || {};
|
||||
emit("progress", uploadPercentage(100, it.fileSize));
|
||||
uploadCompleted.value += it.fileSize * 100;
|
||||
counterAnnounce.value--;
|
||||
counterSuccess.value++;
|
||||
}
|
||||
}
|
||||
|
||||
/** Start upload process */
|
||||
function eventStart() {
|
||||
if (!isRunning.value && counterAnnounce.value > 0) {
|
||||
uploadSize.value = 0;
|
||||
uploadCompleted.value = 0;
|
||||
uploadValues.value.forEach((model) => {
|
||||
if (model.status === "init") {
|
||||
model.status = "queued";
|
||||
if (!model.targetHistoryId) {
|
||||
// Associate with current history once upload starts
|
||||
// This will not change if the current history is changed during upload
|
||||
model.targetHistoryId = historyId.value;
|
||||
}
|
||||
uploadSize.value += model.fileSize;
|
||||
}
|
||||
});
|
||||
emit("progress", 0, "success");
|
||||
counterRunning.value = counterAnnounce.value;
|
||||
queue.value.start();
|
||||
}
|
||||
}
|
||||
|
||||
/** Pause upload process */
|
||||
function eventStop() {
|
||||
if (isRunning.value) {
|
||||
emit("progress", null, "info");
|
||||
queueStopping.value = true;
|
||||
queue.value.stop();
|
||||
}
|
||||
}
|
||||
|
||||
/** Display warning */
|
||||
function eventWarning(index: string, message: string) {
|
||||
const it = uploadItems.value[index];
|
||||
if (it) {
|
||||
it.status = "warning";
|
||||
it.info = message;
|
||||
}
|
||||
}
|
||||
|
||||
/** Update collection type */
|
||||
function updateCollectionType(newCollectionType: CollectionBuilderType) {
|
||||
collectionType.value = newCollectionType;
|
||||
}
|
||||
|
||||
/* Update extension type for all entries */
|
||||
function updateExtension(newExtension: string) {
|
||||
extension.value = newExtension;
|
||||
uploadValues.value.forEach((model) => {
|
||||
if (model.status === "init" && model.extension === props.defaultExtension) {
|
||||
model.extension = newExtension;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Update reference dataset for all entries */
|
||||
function updateDbKey(newDbKey: string) {
|
||||
dbKey.value = newDbKey;
|
||||
uploadValues.value.forEach((model) => {
|
||||
if (model.status === "init" && model.dbKey === props.defaultDbKey) {
|
||||
model.dbKey = newDbKey;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Calculate percentage of all queued uploads */
|
||||
function uploadPercentage(percentage: number, size: number) {
|
||||
return (uploadCompleted.value + percentage * size) / uploadSize.value;
|
||||
}
|
||||
|
||||
function openUploadActivity() {
|
||||
const uploadActivity = activityStore.findById("upload");
|
||||
if (uploadActivity) {
|
||||
if (!uploadActivity.visible) {
|
||||
activityStore.ensureVisible(uploadActivity.id);
|
||||
activityStore.setPosition(uploadActivity.id, 0);
|
||||
}
|
||||
activityStore.ensureSideBarOpen(uploadActivity.id);
|
||||
emit("dismiss");
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
addFiles,
|
||||
counterAnnounce,
|
||||
listExtensions,
|
||||
showHelper,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="upload-wrapper">
|
||||
<div class="upload-header">
|
||||
<div v-if="queueStopping" v-localize>Queue will pause after completing the current file...</div>
|
||||
<div v-else-if="counterAnnounce === 0">
|
||||
<div v-if="!!hasBrowserSupport"> </div>
|
||||
<div v-else>
|
||||
Browser does not support Drag & Drop. Try Firefox 4+, Chrome 7+, IE 10+, Opera 12+ or Safari 6+.
|
||||
</div>
|
||||
</div>
|
||||
<div v-else>
|
||||
<div v-if="!isRunning">
|
||||
You added {{ counterAnnounce }} file(s) to the queue. Add more files or click 'Start' to proceed.
|
||||
</div>
|
||||
<div v-else>Please wait...{{ counterAnnounce }} out of {{ counterRunning }} remaining...</div>
|
||||
</div>
|
||||
</div>
|
||||
<UploadBox @add="addFiles">
|
||||
<div v-show="showHelper" class="upload-helper">
|
||||
<FontAwesomeIcon class="mr-1" :icon="faCopy" />
|
||||
<span v-localize>Drop files here</span>
|
||||
</div>
|
||||
<div v-show="!showHelper">
|
||||
<DefaultRow
|
||||
v-for="[uploadIndex, uploadItem] in Object.entries(uploadItems).slice(0, lazyLoad)"
|
||||
:key="uploadIndex"
|
||||
:index="uploadIndex"
|
||||
:db-key="uploadItem.dbKey"
|
||||
:deferred="uploadItem.deferred"
|
||||
:extension="uploadItem.extension"
|
||||
:file-data="isLocalFile(uploadItem.fileData) ? uploadItem.fileData : undefined"
|
||||
:file-content="uploadItem.fileContent"
|
||||
:file-mode="uploadItem.fileMode"
|
||||
:file-name="uploadItem.fileName"
|
||||
:file-size="uploadItem.fileSize"
|
||||
:info="uploadItem.info || undefined"
|
||||
:list-extensions="!isCollection && listExtensions.length > 1 ? listExtensions : undefined"
|
||||
:list-db-keys="!isCollection && listDbKeys.length > 1 ? listDbKeys : undefined"
|
||||
:percentage="uploadItem.percentage"
|
||||
:space-to-tab="uploadItem.spaceToTab"
|
||||
:status="uploadItem.status"
|
||||
:to-posix-lines="uploadItem.toPosixLines"
|
||||
@remove="eventRemove"
|
||||
@input="eventInput"
|
||||
@explore="eventExplore" />
|
||||
<div
|
||||
v-if="uploadValues.length > lazyLoad"
|
||||
v-localize
|
||||
class="upload-text-message"
|
||||
data-description="lazyload message">
|
||||
Only showing first {{ lazyLoad }} of {{ uploadValues.length }} entries.
|
||||
</div>
|
||||
</div>
|
||||
<label class="sr-only" for="upload-file">Uploaded File</label>
|
||||
<input
|
||||
id="upload-file"
|
||||
ref="uploadFile"
|
||||
type="file"
|
||||
:multiple="multiple"
|
||||
@change="addFileFromInput($event.target)" />
|
||||
</UploadBox>
|
||||
<div v-if="!disableFooter" class="upload-footer text-center">
|
||||
<span v-if="isCollection" v-localize class="upload-footer-title">Collection:</span>
|
||||
<UploadSelect
|
||||
v-if="isCollection"
|
||||
class="upload-footer-collection-type"
|
||||
:value="collectionType"
|
||||
:disabled="isRunning"
|
||||
:options="COLLECTION_TYPES"
|
||||
:searchable="false"
|
||||
placeholder="Select Type"
|
||||
@input="updateCollectionType" />
|
||||
<span v-localize class="upload-footer-title">Type (set all):</span>
|
||||
<UploadSelectExtension
|
||||
class="upload-footer-extension"
|
||||
:value="extension"
|
||||
:disabled="isRunning"
|
||||
:list-extensions="listExtensions"
|
||||
@input="updateExtension">
|
||||
</UploadSelectExtension>
|
||||
<span v-localize class="upload-footer-title">Reference (set all):</span>
|
||||
<UploadSelect
|
||||
class="upload-footer-genome"
|
||||
:value="dbKey"
|
||||
:disabled="isRunning"
|
||||
:options="listDbKeys"
|
||||
what="reference"
|
||||
placeholder="Select Reference"
|
||||
@input="updateDbKey" />
|
||||
</div>
|
||||
<slot name="footer" />
|
||||
<div
|
||||
class="d-flex justify-content-between flex-wrap"
|
||||
:class="{
|
||||
'upload-buttons': !disableFooter,
|
||||
'flex-gapx-1': disableFooter,
|
||||
}">
|
||||
<div class="d-flex">
|
||||
<GButton
|
||||
v-if="props.showUploadActivity ?? props.showBetaUpload"
|
||||
id="btn-upload-activity"
|
||||
size="small"
|
||||
title="Use the upload activity"
|
||||
@click="openUploadActivity">
|
||||
<span v-localize>Upload activity</span>
|
||||
</GButton>
|
||||
</div>
|
||||
<div class="d-flex justify-content-end flex-wrap">
|
||||
<GButton id="btn-local" :size="size" :disabled="!enableSources" @click="uploadFile?.click()">
|
||||
<FontAwesomeIcon :icon="faLaptop" />
|
||||
<span v-localize>Choose local file</span>
|
||||
</GButton>
|
||||
<GButton
|
||||
v-if="hasRemoteFiles"
|
||||
id="btn-remote-files"
|
||||
:size="size"
|
||||
:disabled="!enableSources"
|
||||
@click="eventRemoteFiles">
|
||||
<FontAwesomeIcon :icon="faFolderOpen" />
|
||||
<span v-localize>Choose from repository</span>
|
||||
</GButton>
|
||||
<GButton
|
||||
id="btn-new"
|
||||
:size="size"
|
||||
title="Paste/Fetch data"
|
||||
:disabled="!enableSources"
|
||||
@click="eventCreate">
|
||||
<FontAwesomeIcon :icon="faEdit" />
|
||||
<span v-localize>Paste/Fetch data</span>
|
||||
</GButton>
|
||||
<GButton
|
||||
id="btn-start"
|
||||
:size="size"
|
||||
:disabled="!enableStart"
|
||||
title="Start"
|
||||
:variant="enableStart ? 'primary' : null"
|
||||
@click="eventStart">
|
||||
<span v-localize>Start</span>
|
||||
</GButton>
|
||||
<GButton
|
||||
v-if="isCollection && !collectionModalShow"
|
||||
id="btn-build"
|
||||
:size="size"
|
||||
:disabled="!enableBuild"
|
||||
:tooltip="!enableBuild && Boolean(historyItemsStateInfo?.message)"
|
||||
:disabled-title="historyItemsStateInfo?.message || 'Build is not available'"
|
||||
title="Build"
|
||||
:color="historyItemsStateInfo?.color ? historyItemsStateInfo.color : undefined"
|
||||
@click="() => eventBuild(true)">
|
||||
<FontAwesomeIcon
|
||||
v-if="historyItemsStateInfo?.icon"
|
||||
:icon="historyItemsStateInfo.icon"
|
||||
:spin="historyItemsStateInfo.spin" />
|
||||
<span v-localize>Build</span>
|
||||
</GButton>
|
||||
<GButton
|
||||
v-if="emitUploaded"
|
||||
id="btn-emit"
|
||||
:size="size"
|
||||
:disabled="!enableBuild"
|
||||
:tooltip="Boolean(historyItemsStateInfo?.message)"
|
||||
:disabled-title="historyItemsStateInfo?.message || 'Upload Valid Files to Use'"
|
||||
:title="historyItemsStateInfo?.message || 'Use Uploaded Files'"
|
||||
:color="historyItemsStateInfo?.color ? historyItemsStateInfo.color : undefined"
|
||||
@click="() => eventBuild(false)">
|
||||
<FontAwesomeIcon
|
||||
v-if="historyItemsStateInfo?.icon"
|
||||
:icon="historyItemsStateInfo.icon"
|
||||
:spin="historyItemsStateInfo.spin" />
|
||||
<span v-localize>Use Uploaded</span>
|
||||
<span v-if="uploadedHistoryItemsOk.length < counterSuccess">
|
||||
({{ uploadedHistoryItemsOk.length }}/{{ counterSuccess }})
|
||||
</span>
|
||||
<span v-else> ({{ counterSuccess }}) </span>
|
||||
</GButton>
|
||||
<GButton id="btn-stop" :size="size" title="Pause" :disabled="!isRunning" @click="eventStop">
|
||||
<span v-localize>Pause</span>
|
||||
</GButton>
|
||||
<GButton id="btn-reset" :size="size" title="Reset" :disabled="!enableReset" @click="eventReset">
|
||||
<span v-localize>Reset</span>
|
||||
</GButton>
|
||||
<GButton id="btn-close" :size="size" title="Close" @click="$emit('dismiss')">
|
||||
<span v-if="hasCallback" v-localize>Cancel</span>
|
||||
<span v-else v-localize>Close</span>
|
||||
</GButton>
|
||||
</div>
|
||||
</div>
|
||||
<CollectionCreatorIndex
|
||||
v-if="isCollection && historyId"
|
||||
:history-id="historyId"
|
||||
:collection-type="collectionType"
|
||||
:extended-collection-type="{}"
|
||||
:selected-items="selectedItemsForModal"
|
||||
:show.sync="collectionModalShow"
|
||||
default-hide-source-items
|
||||
@on-hide="emit('dismiss')" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,234 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
faCheck,
|
||||
faEdit,
|
||||
faExclamation,
|
||||
faExclamationTriangle,
|
||||
faFolderOpen,
|
||||
faLaptop,
|
||||
faSpinner,
|
||||
faTrash,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, onMounted, type Ref, ref } from "vue";
|
||||
|
||||
import type { DbKey, ExtensionDetails } from "@/composables/uploadConfigurations";
|
||||
import { type ArchiveSource, isLocalZipFile, isRemoteZipFile } from "@/composables/zipExplorer";
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
import { bytesToString } from "@/utils/utils";
|
||||
|
||||
import type { UploadRowModel } from "./model";
|
||||
import { isLocalFile } from "./model";
|
||||
|
||||
import GButton from "../BaseComponents/GButton.vue";
|
||||
import UploadExtension from "./UploadExtension.vue";
|
||||
import UploadSelect from "./UploadSelect.vue";
|
||||
import UploadSettings from "./UploadSettings.vue";
|
||||
|
||||
const { isAnonymous } = storeToRefs(useUserStore());
|
||||
|
||||
const fileField: Ref<HTMLInputElement | null> = ref(null);
|
||||
|
||||
interface Props {
|
||||
deferred?: boolean;
|
||||
extension: string;
|
||||
fileContent: string;
|
||||
fileMode: string;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
fileData?: File;
|
||||
dbKey: string;
|
||||
index: string;
|
||||
info?: string;
|
||||
listDbKeys?: DbKey[];
|
||||
listExtensions?: ExtensionDetails[];
|
||||
percentage: number;
|
||||
spaceToTab: boolean;
|
||||
status: string;
|
||||
toPosixLines: boolean;
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
deferred: undefined,
|
||||
info: "",
|
||||
listDbKeys: undefined,
|
||||
listExtensions: undefined,
|
||||
fileData: undefined,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "input", index: string, value: Partial<UploadRowModel>): void;
|
||||
(e: "remove", index: string): void;
|
||||
(e: "explore", archiveSource: ArchiveSource): void;
|
||||
}>();
|
||||
|
||||
const isExplorable = ref(false);
|
||||
|
||||
const isDisabled = computed(() => props.status !== "init");
|
||||
function inputExtension(newExtension: string) {
|
||||
emit("input", props.index, { extension: newExtension });
|
||||
}
|
||||
|
||||
async function inputFileContent(newFileContent: string) {
|
||||
emit("input", props.index, { fileContent: newFileContent, fileSize: newFileContent.length });
|
||||
isExplorable.value = await isRemoteExplorableArchiveDebounced(newFileContent);
|
||||
}
|
||||
|
||||
function inputFileName(newFileName: string) {
|
||||
emit("input", props.index, { fileName: newFileName });
|
||||
}
|
||||
|
||||
function inputDbKey(newDbKey: string) {
|
||||
emit("input", props.index, { dbKey: newDbKey });
|
||||
}
|
||||
|
||||
function inputSettings(settingId: string) {
|
||||
const newSettings: Record<string, any> = {};
|
||||
newSettings[settingId] = !(props as any)[settingId];
|
||||
emit("input", props.index, newSettings);
|
||||
}
|
||||
|
||||
function removeUpload() {
|
||||
if (["init", "success", "error"].indexOf(props.status) !== -1) {
|
||||
emit("remove", props.index);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
autoSelectFileInput();
|
||||
});
|
||||
|
||||
function autoSelectFileInput() {
|
||||
fileField.value?.select();
|
||||
}
|
||||
|
||||
const isRemoteExplorableArchiveDebounced = useDebounceFn(async (url: string) => {
|
||||
return isRemoteZipFile(url);
|
||||
}, 1000);
|
||||
|
||||
function initializeExplorableArchive() {
|
||||
if (props.fileMode === "local" && isLocalFile(props.fileData)) {
|
||||
isExplorable.value = isLocalZipFile(props.fileData);
|
||||
} else if (props.fileMode === "new" && props.fileContent) {
|
||||
isRemoteZipFile(props.fileContent).then((result) => {
|
||||
isExplorable.value = result;
|
||||
});
|
||||
} else {
|
||||
// Remote File Source URIs are not explorable because they don't support byte range requests
|
||||
isExplorable.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function exploreZipContents() {
|
||||
if (props.fileMode === "local" && props.fileData) {
|
||||
emit("explore", props.fileData);
|
||||
} else if (props.fileMode === "new" && props.fileContent) {
|
||||
emit("explore", props.fileContent);
|
||||
}
|
||||
}
|
||||
|
||||
initializeExplorableArchive();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :id="`upload-row-${index}`" class="upload-row rounded my-1 p-2" :class="`upload-${status}`">
|
||||
<div class="d-flex justify-content-around align-items-center">
|
||||
<div>
|
||||
<FontAwesomeIcon v-if="fileMode == 'new'" :icon="faEdit" fixed-width />
|
||||
<FontAwesomeIcon v-if="fileMode == 'local'" :icon="faLaptop" fixed-width />
|
||||
<FontAwesomeIcon v-if="fileMode == 'url'" :icon="faFolderOpen" fixed-width />
|
||||
</div>
|
||||
<b-input
|
||||
ref="fileField"
|
||||
:value="fileName"
|
||||
class="upload-title p-1 border rounded"
|
||||
:disabled="isDisabled"
|
||||
@input="inputFileName" />
|
||||
<div class="upload-size">
|
||||
{{ bytesToString(fileSize) }}
|
||||
</div>
|
||||
<UploadSelect
|
||||
v-if="listExtensions"
|
||||
class="upload-extension"
|
||||
:value="extension"
|
||||
:disabled="isDisabled"
|
||||
:options="listExtensions"
|
||||
placeholder="Select Type"
|
||||
what="file type"
|
||||
@input="inputExtension" />
|
||||
<UploadExtension v-if="listExtensions" :extension="extension" :list-extensions="listExtensions" />
|
||||
<UploadSelect
|
||||
v-if="listDbKeys"
|
||||
class="upload-genome"
|
||||
:value="dbKey"
|
||||
:disabled="isDisabled"
|
||||
:options="listDbKeys"
|
||||
placeholder="Select Reference"
|
||||
what="reference"
|
||||
@input="inputDbKey" />
|
||||
<UploadSettings
|
||||
class="upload-settings"
|
||||
:deferred="deferred"
|
||||
:disabled="isDisabled"
|
||||
:to-posix-lines="toPosixLines"
|
||||
:space-to-tab="spaceToTab"
|
||||
@input="inputSettings" />
|
||||
<div class="upload-progress">
|
||||
<div class="progress">
|
||||
<div
|
||||
class="upload-progress-bar progress-bar progress-bar-success"
|
||||
:style="{ width: `${percentage}%` }" />
|
||||
<div class="upload-percentage">{{ percentage }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<FontAwesomeIcon v-if="['running', 'queued'].includes(status)" :icon="faSpinner" spin />
|
||||
<FontAwesomeIcon
|
||||
v-else-if="status === 'error'"
|
||||
class="cursor-pointer"
|
||||
:icon="faExclamationTriangle"
|
||||
fixed-width
|
||||
@click="removeUpload" />
|
||||
<FontAwesomeIcon
|
||||
v-else-if="status === 'init'"
|
||||
class="cursor-pointer"
|
||||
:icon="faTrash"
|
||||
fixed-width
|
||||
@click="removeUpload" />
|
||||
<FontAwesomeIcon
|
||||
v-else-if="status === 'success'"
|
||||
class="cursor-pointer"
|
||||
:icon="faCheck"
|
||||
fixed-width
|
||||
@click="removeUpload" />
|
||||
<FontAwesomeIcon v-else :icon="faExclamation" />
|
||||
</div>
|
||||
|
||||
<GButton
|
||||
v-if="isExplorable"
|
||||
class="btn-explore-archive"
|
||||
size="small"
|
||||
title="Explore the contents of a remote or local compressed archive and upload individual files"
|
||||
:disabled="isAnonymous"
|
||||
disabled-title="You must be logged in to use this feature"
|
||||
@click="exploreZipContents">
|
||||
<span v-localize>Explore</span>
|
||||
</GButton>
|
||||
</div>
|
||||
<div v-if="info" v-localize class="upload-text-message font-weight-bold">
|
||||
{{ info }}
|
||||
</div>
|
||||
<div v-if="fileMode == 'new'">
|
||||
<div v-localize class="upload-text-message">
|
||||
Download data from the web by entering URLs (one per line) or directly paste content.
|
||||
</div>
|
||||
<b-textarea
|
||||
:value="fileContent"
|
||||
class="upload-text-content form-control"
|
||||
:disabled="isDisabled"
|
||||
@input="inputFileContent" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,33 +0,0 @@
|
||||
import { getLocalVue } from "@tests/vitest/helpers";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import mountTarget from "./RulesInput.vue";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
function getWrapper() {
|
||||
return mount(mountTarget, {
|
||||
propsData: {
|
||||
fileSourcesConfigured: true,
|
||||
ftpUploadSite: null,
|
||||
historyId: "historyId",
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
}
|
||||
|
||||
describe("RulesInput", () => {
|
||||
it("rendering and reset", async () => {
|
||||
const wrapper = getWrapper();
|
||||
expect(wrapper.find("#btn-reset").classes()).toEqual(expect.arrayContaining(["disabled"]));
|
||||
const textInput = wrapper.find(".upload-rule-source-content");
|
||||
expect(textInput.element.value).toBe("");
|
||||
await textInput.setValue("a b c d");
|
||||
expect(textInput.element.value).toBe("a b c d");
|
||||
expect(wrapper.find("#btn-reset").classes()).not.toEqual(expect.arrayContaining(["disabled"]));
|
||||
await wrapper.find("#btn-reset").trigger("click");
|
||||
expect(textInput.element.value).toBe("");
|
||||
expect(wrapper.find("#btn-reset").classes()).toEqual(expect.arrayContaining(["disabled"]));
|
||||
});
|
||||
});
|
||||
@@ -1,181 +0,0 @@
|
||||
<script setup>
|
||||
import { faEdit, faFile, faFolderOpen, faLock } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { BAlert, BButton } from "bootstrap-vue";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import { getGalaxyInstance } from "@/app";
|
||||
import { buildCollectionFromRules } from "@/components/Collections/common/buildCollectionModal";
|
||||
import { getRemoteEntries, getRemoteEntriesAt } from "@/components/Upload/utils";
|
||||
import { filesDialog } from "@/utils/dataModals";
|
||||
import { urlData } from "@/utils/url";
|
||||
|
||||
import { RULES_TYPES } from "./utils.js";
|
||||
|
||||
import UploadSelect from "./UploadSelect.vue";
|
||||
|
||||
const props = defineProps({
|
||||
hasCallback: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
fileSourcesConfigured: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
ftpUploadSite: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
historyId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["dismiss"]);
|
||||
|
||||
const dataType = ref("datasets");
|
||||
const errorMessage = ref(null);
|
||||
const ftpFiles = ref([]);
|
||||
const selectedDatasetId = ref(null);
|
||||
const selectionType = ref("raw");
|
||||
const sourceContent = ref(null);
|
||||
const uris = ref([]);
|
||||
|
||||
const isDisabled = computed(() => selectionType.value !== "raw");
|
||||
|
||||
function eventBuild() {
|
||||
const entry = {
|
||||
dataType: dataType.value,
|
||||
selectionType: selectionType.value,
|
||||
};
|
||||
if (entry.selectionType == "ftp") {
|
||||
entry.elements = ftpFiles.value;
|
||||
entry.ftpUploadSite = props.ftpUploadSite;
|
||||
} else if (entry.selectionType === "raw") {
|
||||
entry.content = sourceContent.value;
|
||||
} else if (entry.selectionType == "remote_files") {
|
||||
entry.elements = uris.value;
|
||||
}
|
||||
buildCollectionFromRules(entry, null, true);
|
||||
emit("dismiss");
|
||||
}
|
||||
|
||||
function eventReset() {
|
||||
selectedDatasetId.value = null;
|
||||
selectionType.value = "raw";
|
||||
sourceContent.value = null;
|
||||
}
|
||||
|
||||
function inputDialog() {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
Galaxy.data.dialog(
|
||||
(response) => {
|
||||
selectedDatasetId.value = response.id;
|
||||
urlData({ url: `/api/histories/${props.historyId}/contents/${selectedDatasetId.value}/display` })
|
||||
.then((newSourceContent) => {
|
||||
selectionType.value = "raw";
|
||||
sourceContent.value = newSourceContent;
|
||||
})
|
||||
.catch((error) => {
|
||||
errorMessage.value = error;
|
||||
});
|
||||
},
|
||||
{
|
||||
multiple: false,
|
||||
library: false,
|
||||
format: null,
|
||||
allowUpload: false,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
function inputFtp() {
|
||||
getRemoteEntries((ftp_files) => {
|
||||
selectionType.value = "ftp";
|
||||
sourceContent.value = ftp_files.map((file) => file["path"]).join("\n");
|
||||
ftpFiles.value = ftp_files;
|
||||
});
|
||||
}
|
||||
|
||||
function inputPaste() {
|
||||
selectionType.value = "raw";
|
||||
selectedDatasetId.value = null;
|
||||
sourceContent.value = null;
|
||||
}
|
||||
|
||||
function inputRemote() {
|
||||
function handleRemoteFilesUri(record) {
|
||||
getRemoteEntriesAt(record.url).then((files) => {
|
||||
files = files.filter((file) => file["class"] == "File");
|
||||
selectionType.value = "remote_files";
|
||||
sourceContent.value = files.map((file) => file["uri"]).join("\n");
|
||||
uris.value = files;
|
||||
});
|
||||
}
|
||||
filesDialog(handleRemoteFilesUri, { mode: "directory" });
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="upload-wrapper d-flex flex-column">
|
||||
<BAlert v-if="errorMessage" variant="danger" show>{{ errorMessage }}</BAlert>
|
||||
<div v-localize class="upload-header">Insert tabular source data to extract collection files and metadata.</div>
|
||||
<textarea
|
||||
v-model="sourceContent"
|
||||
class="upload-box upload-rule-source-content"
|
||||
:placeholder="localize('Insert tabular source data here.')"
|
||||
:disabled="isDisabled" />
|
||||
<FontAwesomeIcon v-if="isDisabled" class="upload-text-lock" :icon="faLock" />
|
||||
<div class="upload-footer text-center">
|
||||
<span v-localize class="upload-footer-title">Upload type:</span>
|
||||
<UploadSelect v-model="dataType" class="rule-data-type" :options="RULES_TYPES" :searchable="false" />
|
||||
</div>
|
||||
<div class="upload-buttons d-flex justify-content-end">
|
||||
<BButton @click="inputPaste">
|
||||
<FontAwesomeIcon :icon="faEdit" />
|
||||
<span v-localize>Paste data</span>
|
||||
</BButton>
|
||||
<BButton data-description="rules dataset dialog" @click="inputDialog">
|
||||
<FontAwesomeIcon :icon="faFile" />
|
||||
<span v-localize>Choose dataset</span>
|
||||
</BButton>
|
||||
<BButton v-if="ftpUploadSite" @click="inputFtp">
|
||||
<FontAwesomeIcon :icon="faFolderOpen" />
|
||||
<span v-localize>Import FTP files</span>
|
||||
</BButton>
|
||||
<BButton @click="inputRemote">
|
||||
<FontAwesomeIcon :icon="faFolderOpen" />
|
||||
<span v-localize>Choose from repository</span>
|
||||
</BButton>
|
||||
<BButton
|
||||
id="btn-build"
|
||||
:disabled="!sourceContent"
|
||||
title="Build"
|
||||
:variant="sourceContent ? 'primary' : ''"
|
||||
@click="eventBuild">
|
||||
<span v-localize>Build</span>
|
||||
</BButton>
|
||||
<BButton id="btn-reset" title="Reset" :disabled="!sourceContent" @click="eventReset">
|
||||
<span v-localize>Reset</span>
|
||||
</BButton>
|
||||
<BButton id="btn-close" title="Close" @click="$emit('dismiss')">
|
||||
<span v-localize>Close</span>
|
||||
</BButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.upload-rule-source-content {
|
||||
resize: none;
|
||||
}
|
||||
.upload-text-lock {
|
||||
bottom: 22%;
|
||||
font-size: 1.275rem;
|
||||
opacity: 0.2;
|
||||
right: 3%;
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
@@ -1,32 +0,0 @@
|
||||
<script setup>
|
||||
import { ref } from "vue";
|
||||
|
||||
const isDragging = ref(false);
|
||||
|
||||
const emit = defineEmits(["add"]);
|
||||
|
||||
/** Handle files dropped into the upload box **/
|
||||
function onDrop(evt) {
|
||||
isDragging.value = false;
|
||||
if (evt.dataTransfer) {
|
||||
emit("add", evt.dataTransfer.files);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="upload-box"
|
||||
:class="{ highlight: isDragging }"
|
||||
@dragover.prevent="isDragging = true"
|
||||
@dragleave.prevent="isDragging = false"
|
||||
@drop.prevent="onDrop">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.upload {
|
||||
height: 300px;
|
||||
}
|
||||
</style>
|
||||
@@ -1,30 +0,0 @@
|
||||
<script setup>
|
||||
import { faSquare } from "@fortawesome/free-regular-svg-icons";
|
||||
import { faCheckSquare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits("click");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr @click="emit('click')">
|
||||
<td>
|
||||
<FontAwesomeIcon v-if="value" class="px-2" :icon="faCheckSquare" fa-fw />
|
||||
<FontAwesomeIcon v-else class="px-2" :icon="faSquare" fa-fw />
|
||||
</td>
|
||||
<td class="text-left">
|
||||
<span v-localize class="pr-2">{{ title }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -1,84 +0,0 @@
|
||||
<script setup>
|
||||
import { faCog } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
|
||||
import UploadOption from "./UploadOption.vue";
|
||||
import Popper from "@/components/Popper/Popper.vue";
|
||||
|
||||
defineProps({
|
||||
deferred: {
|
||||
type: Boolean,
|
||||
default: undefined,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
spaceToTab: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
toPosixLines: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(["input"]);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Popper placement="bottom" title="Upload Configuration" mode="primary-title" trigger="click">
|
||||
<template v-slot:reference>
|
||||
<FontAwesomeIcon class="cursor-pointer" :icon="faCog" />
|
||||
</template>
|
||||
<div class="upload-settings-content px-2 py-2 no-highlight">
|
||||
<table class="upload-settings-table grid">
|
||||
<tbody>
|
||||
<UploadOption
|
||||
class="upload-space-to-tab"
|
||||
title="Convert spaces to tabs"
|
||||
:value="spaceToTab"
|
||||
@click="emit('input', 'spaceToTab')" />
|
||||
<UploadOption
|
||||
class="upload-to-posix-lines"
|
||||
title="Use POSIX standard"
|
||||
:value="toPosixLines"
|
||||
@click="emit('input', 'toPosixLines')" />
|
||||
<UploadOption
|
||||
v-if="deferred !== undefined"
|
||||
class="upload-deferred"
|
||||
title="Defer dataset resolution"
|
||||
:value="deferred"
|
||||
@click="emit('input', 'deferred')" />
|
||||
</tbody>
|
||||
</table>
|
||||
<div v-if="disabled" class="upload-settings-cover" />
|
||||
</div>
|
||||
</Popper>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import "@/style/scss/theme/blue.scss";
|
||||
.upload-settings-content {
|
||||
position: relative;
|
||||
.upload-settings-cover {
|
||||
background: $white;
|
||||
cursor: no-drop;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
opacity: 0.25;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
}
|
||||
.upload-settings-table {
|
||||
tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
tr:hover {
|
||||
background-color: lighten($brand-success, 20%);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user