Merge pull request #22813 from davelopez/26.1_fix_import_workflow_usability

[26.1] Improve import workflow usability
This commit is contained in:
Marius van den Beek
2026-06-09 17:02:18 +02:00
committed by GitHub
11 changed files with 316 additions and 162 deletions
@@ -1,44 +1,40 @@
<script setup lang="ts">
import { faCloudUploadAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import axios from "axios";
import { BAlert, BForm, BFormFile, BFormGroup } from "bootstrap-vue";
import { BForm, BFormFile, BFormGroup } from "bootstrap-vue";
import { computed, type Ref, ref, watch } from "vue";
import { useRouter } from "vue-router/composables";
import { getRedirectOnImportPath } from "@/components/Workflow/redirectPath";
import { useFileDrop } from "@/composables/fileDrop";
import { withPrefix } from "@/utils/redirect";
import { errorMessageAsString } from "@/utils/simple-error";
import { validateWorkflowFile, WORKFLOW_FILE_ACCEPT } from "./workflowValidation";
import GAlert from "@/components/BaseComponents/GAlert.vue";
import GButton from "@/components/BaseComponents/GButton.vue";
import LoadingSpan from "@/components/LoadingSpan.vue";
interface Props {
hideSubmitButton?: boolean;
/** A file dropped on the parent's method card, to be consumed on mount */
droppedFile?: File | null;
}
const props = withDefaults(defineProps<Props>(), {
hideSubmitButton: false,
});
const props = defineProps<Props>();
const emit = defineEmits<{
(e: "input-valid", valid: boolean): void;
}>();
const loading = ref(false);
const sourceFile: Ref<string | null> = ref(null);
const sourceFile: Ref<File | null> = ref(null);
const errorMessage: Ref<string | null> = ref(null);
const acceptedWorkflowFormats = ".ga, .yml";
const dropZoneRef = ref<HTMLElement | null>(null);
const fileSetViaDrop = ref(false);
const isImportDisabled = computed(() => {
return !sourceFile.value;
});
const importTooltip = computed(() => {
return isImportDisabled.value ? "You must provide a workflow file." : "Import workflow from File";
});
const hasErrorMessage = computed(() => {
return errorMessage.value != null;
});
const hasErrorMessage = computed(() => errorMessage.value != null);
// Validation state for wizard mode
const isValid = computed(() => sourceFile.value !== null);
@@ -47,15 +43,65 @@ watch(isValid, (newValue) => {
emit("input-valid", newValue);
});
// Hide submit button in wizard mode (or when hideSubmitButton is true)
const showSubmitButton = computed(() => {
return !props.hideSubmitButton;
// Consume a file dropped on the parent's method card
if (props.droppedFile) {
sourceFile.value = props.droppedFile;
fileSetViaDrop.value = true;
}
// Drop zone handling
async function onDrop(evt: DragEvent) {
if (evt.dataTransfer?.files?.length) {
const file = evt.dataTransfer.files[0];
if (!file) {
return;
}
const result = await validateWorkflowFile(file, { checkContent: true });
if (result.valid) {
sourceFile.value = file;
fileSetViaDrop.value = true;
} else {
errorMessage.value = result.error ?? "Invalid workflow file.";
}
}
}
const { isFileOverDropZone } = useFileDrop({
dropZone: dropZoneRef,
onDrop,
onDropCancel: () => {},
solo: false,
idleTime: 10000,
ignoreChildrenOnLeave: true,
});
/** When the user selects a file via the native input, validate it */
async function onFileInput(file: File | null) {
if (!file) {
sourceFile.value = null;
fileSetViaDrop.value = false;
return;
}
const result = await validateWorkflowFile(file, { checkContent: true });
if (result.valid) {
sourceFile.value = file;
fileSetViaDrop.value = false;
} else {
sourceFile.value = null;
fileSetViaDrop.value = false;
errorMessage.value = result.error ?? "Invalid workflow file.";
}
}
/** Clear the current file and reset to the browse state */
function clearFile() {
sourceFile.value = null;
fileSetViaDrop.value = false;
}
const router = useRouter();
async function submit(ev: SubmitEvent) {
ev.preventDefault();
async function submit() {
const formData = new FormData();
if (sourceFile.value) {
@@ -78,38 +124,71 @@ async function submit(ev: SubmitEvent) {
// Expose method for wizard submit
async function attemptImport() {
await submit(new Event("submit") as SubmitEvent);
await submit();
}
defineExpose({ attemptImport });
</script>
<template>
<BForm class="mt-4 workflow-import-file" @submit="submit">
<h2 class="h-sm">Import from a workflow file</h2>
<BForm class="mt-4 workflow-import-file" @submit.prevent="submit">
<div
ref="dropZoneRef"
class="workflow-drop-zone"
:class="{ 'workflow-drop-zone-active': isFileOverDropZone }"
data-galaxy-file-drop-target>
<div v-if="!sourceFile" class="mb-3">
<FontAwesomeIcon :icon="faCloudUploadAlt" size="3x" class="text-muted" />
<p class="mt-2 mb-1 font-weight-medium">Drag a workflow file here</p>
<p class="text-muted small mb-2">or</p>
</div>
<BFormGroup :label="sourceFile ? 'Workflow File' : 'Browse for a file'">
<!-- When a file was set via drop, the native file input can't reflect
it programmatically, so we show the filename manually instead. -->
<div v-if="fileSetViaDrop && sourceFile" class="text-center py-2">
<span class="font-weight-bold text-break">{{ sourceFile.name }}</span>
<div class="mt-1">
<GButton transparent size="small" @click="clearFile">Choose another</GButton>
</div>
</div>
<BFormFile
v-show="!fileSetViaDrop"
v-model="sourceFile"
:accept="WORKFLOW_FILE_ACCEPT"
@input="onFileInput" />
<span v-if="!sourceFile" class="text-muted small">
Accepted formats: <code>*.ga</code>, <code>*.yml</code>, <code>*.yaml</code>
</span>
</BFormGroup>
</div>
<BFormGroup label="Archived Workflow File">
<BFormFile v-model="sourceFile" :accept="acceptedWorkflowFormats" />
If the workflow is in a file on your computer, choose it and then click Import.
</BFormGroup>
<BAlert :show="hasErrorMessage" variant="danger">
<GAlert :show="hasErrorMessage" variant="danger">
{{ errorMessage }}
</BAlert>
</GAlert>
<BAlert v-if="loading" show variant="info">
<GAlert v-if="loading" variant="info">
<LoadingSpan message="Loading your workflow, this may take a while - please be patient." />
</BAlert>
<GButton
v-if="showSubmitButton"
id="workflow-import-button"
type="submit"
:disabled="isImportDisabled"
:title="importTooltip"
tooltip
color="blue">
Import workflow
</GButton>
</GAlert>
</BForm>
</template>
<style scoped lang="scss">
@import "@/style/scss/theme/blue.scss";
.workflow-drop-zone {
border: 2px dashed $border-color;
border-radius: $border-radius-large;
text-align: center;
background-color: $gray-100;
padding: 1.5rem;
margin-bottom: 1rem;
transition:
border-color 0.3s ease,
background-color 0.3s ease;
&.workflow-drop-zone-active {
border-color: $brand-primary;
background-color: lighten($brand-primary, 60%);
}
}
</style>
@@ -1,6 +1,7 @@
import { getLocalVue } from "@tests/vitest/helpers";
import { mount } from "@vue/test-utils";
import { describe, expect, it, vi } from "vitest";
import VueRouter from "vue-router";
import FromFile from "./FromFile.vue";
import FromUrl from "./FromUrl.vue";
@@ -17,6 +18,7 @@ vi.mock("axios", () => ({
}));
const localVue = getLocalVue(true);
localVue.use(VueRouter);
const sharedUrl = "http://127.0.0.1:8081/u/admin/w/unnamed-workflow";
const sharedUrlTrailingSlash = "http://127.0.0.1:8081/u/admin/w/unnamed-workflow/";
@@ -25,7 +27,8 @@ const invalidUrl = "http://127.0.0.1:8081/u/admin/w/unnamed-workflow/additional-
describe("FromUrl", () => {
it("converts shared urls to json urls", async () => {
const wrapper = mount(FromUrl as object, { localVue });
const router = new VueRouter();
const wrapper = mount(FromUrl as object, { localVue, router });
{
const input = wrapper.find("#workflow-import-url-input");
@@ -71,7 +74,8 @@ describe("FromUrl", () => {
describe("FromFile", () => {
it("can mount the component", async () => {
const wrapper = mount(FromFile as object, { localVue });
expect(wrapper.find("#workflow-import-button").exists()).toBe(true);
const router = new VueRouter();
const wrapper = mount(FromFile as object, { localVue, router });
expect(wrapper.find("form").exists()).toBe(true);
});
});
@@ -2,6 +2,7 @@
import axios from "axios";
import { BAlert, BForm, BFormGroup, BFormInput } from "bootstrap-vue";
import { computed, type Ref, ref, watch } from "vue";
import { RouterLink } from "vue-router";
import { useRouter } from "vue-router/composables";
import { getRedirectOnImportPath } from "@/components/Workflow/redirectPath";
@@ -9,14 +10,6 @@ import { withPrefix } from "@/utils/redirect";
import LoadingSpan from "@/components/LoadingSpan.vue";
interface Props {
hideSubmitButton?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
hideSubmitButton: false,
});
const emit = defineEmits<{
(e: "input-valid", valid: boolean): void;
}>();
@@ -25,14 +18,6 @@ const loading = ref(false);
const sourceURL: Ref<string | null> = ref(null);
const errorMessage: Ref<string | null> = ref(null);
const isImportDisabled = computed(() => {
return !sourceURL.value;
});
const importTooltip = computed(() => {
return isImportDisabled.value ? "You must provide a workflow archive URL." : "Import workflow from URL";
});
const hasErrorMessage = computed(() => {
return errorMessage.value != null;
});
@@ -46,11 +31,6 @@ watch(isValid, (newValue) => {
emit("input-valid", newValue);
});
// Hide submit button in wizard mode (or when hideSubmitButton is true)
const showSubmitButton = computed(() => {
return !props.hideSubmitButton;
});
function autoAppendJson(urlString: string): string {
const sharedWorkflowRegex = /^(https?:\/\/[\S]+\/u\/[\S]+\/w\/[^\s/]+)\/?$/;
const matches = urlString.match(sharedWorkflowRegex);
@@ -65,8 +45,7 @@ function autoAppendJson(urlString: string): string {
const router = useRouter();
async function submit(ev: SubmitEvent) {
ev.preventDefault();
async function submit() {
const formData = new FormData();
if (sourceURL.value) {
@@ -94,14 +73,14 @@ async function submit(ev: SubmitEvent) {
// Expose method for wizard submit
async function attemptImport() {
await submit(new Event("submit") as SubmitEvent);
await submit();
}
defineExpose({ attemptImport });
</script>
<template>
<BForm class="mt-4 workflow-import-url" @submit="submit">
<BForm class="mt-4 workflow-import-url" @submit.prevent="submit">
<h2 class="h-sm">Import from a Galaxy workflow export URL</h2>
<BFormGroup label="Workflow Archive URL">
@@ -111,7 +90,7 @@ defineExpose({ attemptImport });
aria-label="Workflow Import URL"
type="url" />
If your URL is from a workflow repository and doesn't end in <code>.ga</code>, you might need to use the
<a href="/workflows/import?trs_url=Enter%20a%20TRS%20URL">TRS import method</a>
<RouterLink to="/workflows/import?trs_url=Enter%20a%20TRS%20URL">TRS import method</RouterLink>
instead.
</BFormGroup>
@@ -122,16 +101,5 @@ defineExpose({ attemptImport });
<BAlert v-if="loading" show variant="info">
<LoadingSpan message="Loading your workflow, this may take a while - please be patient." />
</BAlert>
<GButton
v-if="showSubmitButton"
id="workflow-import-button"
type="submit"
:disabled="isImportDisabled"
:title="importTooltip"
tooltip
color="blue">
Import workflow
</GButton>
</BForm>
</template>
@@ -134,8 +134,6 @@ defineExpose({ triggerImport });
<template>
<div class="workflow-import-trs-id">
<h2 class="h-sm">Import from TRS ID</h2>
<div>
<b>TRS Server:</b>
@@ -2,16 +2,12 @@
import { BForm, BFormGroup, BFormInput } from "bootstrap-vue";
import { computed, ref, watch } from "vue";
import GButton from "@/components/BaseComponents/GButton.vue";
interface Props {
queryTrsUrl?: string;
hideSubmitButton?: boolean;
}
const props = withDefaults(defineProps<Props>(), {
queryTrsUrl: "",
hideSubmitButton: false,
});
const emit = defineEmits<{
@@ -21,14 +17,6 @@ const emit = defineEmits<{
const trsUrl = ref(props.queryTrsUrl);
const isImportDisabled = computed(() => {
return !trsUrl.value;
});
const importTooltip = computed(() => {
return isImportDisabled.value ? "You must provide a TRS URL." : "Import workflow from TRS URL";
});
// Validation state for wizard mode
const isValid = computed(() => {
return trsUrl.value !== null && trsUrl.value !== undefined && trsUrl.value.length > 0;
@@ -42,11 +30,6 @@ watch(
{ immediate: true },
);
// Hide submit button in wizard mode (or when hideSubmitButton is true)
const showSubmitButton = computed(() => {
return !props.hideSubmitButton;
});
function submit(ev: SubmitEvent) {
ev.preventDefault();
@@ -67,22 +50,9 @@ defineExpose({ triggerImport });
<template>
<BForm class="mt-4" @submit="submit">
<h2 class="h-sm">Import from TRS URL</h2>
<BFormGroup label="TRS URL:" label-class="font-weight-bold">
<BFormInput id="trs-import-url-input" v-model="trsUrl" aria-label="TRS URL" type="url" />
If the workflow is accessible via a TRS URL, enter the URL above and click Import.
</BFormGroup>
<GButton
v-if="showSubmitButton"
id="trs-url-import-button"
type="submit"
:disabled="isImportDisabled"
:title="importTooltip"
tooltip
color="blue">
Import workflow
</GButton>
</BForm>
</template>
@@ -0,0 +1,69 @@
/** Accepted workflow file extensions */
export const WORKFLOW_FILE_EXTENSIONS = [".ga", ".gxwf.yml", ".gxwf.yaml", ".yml", ".yaml"] as const;
/** Human-readable list of accepted formats for file inputs */
export const WORKFLOW_FILE_ACCEPT = ".ga, .yml, .yaml";
/**
* Check if a filename has a valid workflow file extension.
* Supports .ga (Galaxy Archive), .yml/.yaml (Galaxy Workflow Format), and .gxwf.yml/.gxwf.yaml
*/
export function hasWorkflowFileExtension(fileName: string): boolean {
const lowerName = fileName.toLowerCase();
return WORKFLOW_FILE_EXTENSIONS.some((ext) => lowerName.endsWith(ext));
}
/**
* Validate a workflow file by checking its extension and optionally its content.
*
* For .ga files (JSON format), checks for `"a_galaxy_workflow": "true"`.
* For .yml/.yaml files (YAML format), checks for `class: GalaxyWorkflow` or `class: GalaxyWorkflow`.
*
* Content validation reads only the first 1KB of the file for efficiency.
*/
export async function validateWorkflowFile(
file: File,
options: { checkContent?: boolean } = {},
): Promise<{ valid: boolean; error?: string }> {
if (!hasWorkflowFileExtension(file.name)) {
return { valid: false, error: `Invalid file type. Accepted formats: ${WORKFLOW_FILE_ACCEPT}` };
}
if (options.checkContent) {
const lowerName = file.name.toLowerCase();
if (lowerName.endsWith(".ga")) {
const hasMarker = await readFileMarker(file, '"a_galaxy_workflow"');
if (!hasMarker) {
return {
valid: false,
error: "File does not appear to be a Galaxy workflow (.ga). Missing workflow marker.",
};
}
} else if (lowerName.endsWith(".yml") || lowerName.endsWith(".yaml")) {
const hasMarker = await readFileMarker(file, "class:");
if (!hasMarker) {
return {
valid: false,
error: "File does not appear to be a Galaxy workflow (.yml/.yaml). Missing 'class:' marker.",
};
}
}
}
return { valid: true };
}
/**
* Read the first 1KB of a file and check if it contains the given marker string.
* This is efficient and avoids loading the entire file into memory.
*/
async function readFileMarker(file: File, marker: string): Promise<boolean> {
try {
const slice = file.slice(0, 1024);
const text = await slice.text();
return text.includes(marker);
} catch {
return false;
}
}
@@ -1,10 +1,14 @@
<script setup lang="ts">
import { faCloudUploadAlt, faFileImport, faIdBadge, faLink, faSearch } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { computed, nextTick, onMounted, type Ref, ref } from "vue";
import { useRoute } from "vue-router/composables";
import { useWizard } from "@/components/Common/Wizard/useWizard";
import type { TrsSelection } from "@/components/Workflow/Import/types";
import { validateWorkflowFile } from "@/components/Workflow/Import/workflowValidation";
import { Services } from "@/components/Workflow/services";
import { useFileDrop } from "@/composables/fileDrop";
import GCard from "@/components/Common/GCard.vue";
import GenericWizard from "@/components/Common/Wizard/GenericWizard.vue";
@@ -22,6 +26,8 @@ const trsServers: Ref<TrsSelection[]> = ref([]);
const fileComponent = ref<InstanceType<typeof FromFile>>();
const urlComponent = ref<InstanceType<typeof FromUrl>>();
const trsComponent = ref<InstanceType<typeof TrsImport>>();
const uploadCardRef = ref<HTMLElement | null>(null);
const droppedFile: Ref<File | null> = ref(null);
// Validation states for each step
const uploadValid = ref(false);
@@ -30,6 +36,32 @@ const trsSearchValid = ref(false);
const trsUrlValid = ref(false);
const trsIdValid = ref(false);
// Drop zone on the "Upload file" method card — allows dropping a workflow file
// directly on the card to skip straight to the upload step with the file pre-populated
async function onUploadCardDrop(evt: DragEvent) {
if (evt.dataTransfer?.files?.length) {
const file = evt.dataTransfer.files[0];
if (!file) {
return;
}
const result = await validateWorkflowFile(file, { checkContent: true });
if (result.valid) {
droppedFile.value = file;
selectMethod("upload");
}
// If invalid, the drop is silently ignored — the user can still click the card
}
}
const { isFileOverDropZone: isFileOverUploadCard } = useFileDrop({
dropZone: uploadCardRef,
onDrop: onUploadCardDrop,
onDropCancel: () => {},
solo: false,
idleTime: 10000,
ignoreChildrenOnLeave: true,
});
const queryParams = computed(() => ({
trsId: route.query.trs_id as string | undefined,
trsUrl: route.query.trs_url as string | undefined,
@@ -153,10 +185,26 @@ onMounted(async () => {
function selectMethod(method: ImportMethod) {
selectedMethod.value = method;
// Navigate to the appropriate step based on the selected method
if (method === "upload") {
wizard.goTo("upload-file");
} else if (method === "fetch") {
wizard.goTo("fetch-url");
} else if (method === "repository") {
wizard.goTo("select-trs-method");
}
}
function selectTrsMethod(method: TrsMethod) {
selectedTrsMethod.value = method;
// Navigate to the appropriate TRS step
if (method === "search") {
wizard.goTo("trs-search");
} else if (method === "url") {
wizard.goTo("trs-url");
} else if (method === "id") {
wizard.goTo("trs-id");
}
}
function onSubmit() {
@@ -202,17 +250,24 @@ function onTrsIdValid(e: boolean) {
<div v-if="wizard.isCurrent('select-method')" class="method-selection">
<div class="row">
<div class="col-xl-4 my-5">
<GCard
class="h-100 workflow-import-file-link text-center"
:class="{ selected: selectedMethod === 'upload' }"
:clickable="true"
@click="selectMethod('upload')">
<h4>Upload file</h4>
<p class="text-muted mb-0">
Upload a <code>*.ga</code> file from your computer. These files can be downloaded from
Galaxy servers or workflow repositories.
</p>
</GCard>
<div
ref="uploadCardRef"
class="h-100"
:class="{ 'drop-zone-active': isFileOverUploadCard }"
data-galaxy-file-drop-target>
<GCard
class="h-100 workflow-import-file-link text-center"
:class="{ selected: selectedMethod === 'upload' }"
:clickable="true"
@click="selectMethod('upload')">
<FontAwesomeIcon :icon="faFileImport" size="3x" class="mb-3 text-muted" />
<h4>Upload file</h4>
<p class="text-muted mb-0">
Upload a <code>*.ga</code> or <code>*.gxwf.yml</code> file from your computer. You
can also drag and drop a file here.
</p>
</GCard>
</div>
</div>
<div class="col-xl-4 my-5">
@@ -221,10 +276,11 @@ function onTrsIdValid(e: boolean) {
:class="{ selected: selectedMethod === 'fetch' }"
:clickable="true"
@click="selectMethod('fetch')">
<FontAwesomeIcon :icon="faLink" size="3x" class="mb-3 text-muted" />
<h4>Fetch URL</h4>
<p class="text-muted mb-0">
Fetch a remote <code>*.ga</code> file from any publicly accessible URL. This can be
generated by any Galaxy server, or public repositories like GitHub.
Fetch a remote <code>Galaxy Workflow</code> file from any publicly accessible URL. This
can be generated by any Galaxy server, or public repositories like GitHub.
</p>
</GCard>
</div>
@@ -235,6 +291,7 @@ function onTrsIdValid(e: boolean) {
:class="{ selected: selectedMethod === 'repository' }"
:clickable="true"
@click="selectMethod('repository')">
<FontAwesomeIcon :icon="faCloudUploadAlt" size="3x" class="mb-3 text-muted" />
<h4>Import from repository</h4>
<p class="text-muted mb-0">
Search and import workflows from our configured workflow repositories ({{
@@ -248,7 +305,7 @@ function onTrsIdValid(e: boolean) {
<div v-else-if="wizard.isCurrent('upload-file')" class="import-form">
<div class="container-narrow">
<FromFile ref="fileComponent" @input-valid="onUploadValid" />
<FromFile ref="fileComponent" :dropped-file="droppedFile" @input-valid="onUploadValid" />
</div>
</div>
@@ -266,6 +323,7 @@ function onTrsIdValid(e: boolean) {
:class="{ selected: selectedTrsMethod === 'search' }"
:clickable="true"
@click="selectTrsMethod('search')">
<FontAwesomeIcon :icon="faSearch" size="3x" class="mb-3 text-muted" />
<h4>Search workflow registries</h4>
<p class="text-muted mb-0">Search for workflows across configured GA4GH servers.</p>
</GCard>
@@ -277,6 +335,7 @@ function onTrsIdValid(e: boolean) {
:class="{ selected: selectedTrsMethod === 'url' }"
:clickable="true"
@click="selectTrsMethod('url')">
<FontAwesomeIcon :icon="faLink" size="3x" class="mb-3 text-muted" />
<h4>TRS URL</h4>
<p class="text-muted mb-0">Import directly from any GA4GH server with a TRS URL.</p>
</GCard>
@@ -288,6 +347,7 @@ function onTrsIdValid(e: boolean) {
:class="{ selected: selectedTrsMethod === 'id' }"
:clickable="true"
@click="selectTrsMethod('id')">
<FontAwesomeIcon :icon="faIdBadge" size="3x" class="mb-3 text-muted" />
<h4>TRS ID</h4>
<p class="text-muted mb-0">
When you know the TRS ID for a workflow in one of the configured GA4GH servers.
@@ -355,4 +415,9 @@ function onTrsIdValid(e: boolean) {
width: 100%;
max-width: 600px;
}
.drop-zone-active {
border-radius: 0.3rem;
outline: 2px dashed var(--brand-primary, #2c3143);
outline-offset: 2px;
}
</style>
+3
View File
@@ -115,6 +115,9 @@ $h6-font-size: $font-size-base * 0.85; // default 1
$dropdown-link-hover-color: $brand-light;
$dropdown-link-hover-bg: $brand-primary;
// Code elements (override Bootstrap default $code-color: $pink which fails WCAG AA contrast)
$code-color: #c2266e;
// Color of input elements
$input-color: $text-color;
$input-color-placeholder: $text-muted;
+5 -9
View File
@@ -385,26 +385,23 @@ class NavigatesGalaxy(HasDriverProxy[WaitType]):
self.navigate_to(self.build_url("workflows/trs_import"))
self.components.masthead._.wait_for_visible()
# The wizard auto-navigates to TRS method selection on mount
# Click the "Search workflow registries" card then advance the wizard
# Clicking the "Search workflow registries" card auto-navigates to the search form
self.components.workflows.import_trs_search_link.wait_for_and_click()
self.components.workflows.import_next_button.wait_for_and_click()
def go_to_trs_by_id(self) -> None:
self.navigate_to(self.build_url("workflows/trs_import"))
self.components.masthead._.wait_for_visible()
# The wizard auto-navigates to TRS method selection on mount
# Click the TRS ID sub-card then advance the wizard
# Clicking the TRS ID card auto-navigates to the TRS ID form
self.components.workflows.import_trs_id_link.wait_for_and_click()
self.components.workflows.import_next_button.wait_for_and_click()
def go_to_trs_by_url(self) -> None:
self.navigate_to(self.build_url("workflows/trs_import"))
self.components.masthead._.wait_for_visible()
# The wizard auto-navigates to TRS method selection on mount
# Click the TRS URL sub-card then advance the wizard
# Clicking the TRS URL card auto-navigates to the TRS URL form
self.components.workflows.import_trs_url_link.wait_for_and_click()
self.components.workflows.import_next_button.wait_for_and_click()
# Wait for the URL input to be visible after wizard navigates to the form
# Wait for the URL input to be visible after wizard auto-navigates to the form
self.components.trs_import.url_input.wait_for_visible()
def go_to_workflow_sharing(self, workflow_id: str) -> None:
@@ -2037,9 +2034,8 @@ class NavigatesGalaxy(HasDriverProxy[WaitType]):
def workflow_import_submit_url(self, url):
# Click the "Fetch URL" card to select that import method
# (auto-navigates to the URL input step)
self.components.workflows.import_url_link.wait_for_and_click()
# Click the wizard's Next button to proceed to the URL input step
self.wait_for_and_click_selector(".wizard-actions .go-next-btn")
# Enter the URL
url_element = self.wait_for_selector_visible("#workflow-import-url-input")
url_element.send_keys(url)
@@ -33,28 +33,23 @@ class TestWorkflowManagement(SeleniumTestCase, TestsGalaxyPagers, UsesWorkflowAs
workflows = self.components.workflows
self.workflow_index_click_import()
# Clicking the method card auto-navigates to the upload step
workflows.import_file_link.wait_for_and_click()
# Click Next to navigate to the file upload view
self.wait_for_and_click_selector(".wizard-actions .go-next-btn")
workflows.import_file.assert_no_axe_violations_with_impact_of_at_least("moderate")
self.navigate_to_workflows_import()
# Clicking the TRS card auto-navigates to the TRS method selection step
workflows.import_trs_link.wait_for_and_click()
# Click Next to navigate to TRS method selection
self.wait_for_and_click_selector(".wizard-actions .go-next-btn")
# Clicking the search card auto-navigates to the TRS search form
workflows.import_trs_search_link.wait_for_and_click()
# Click Next to navigate to TRS search view
self.wait_for_and_click_selector(".wizard-actions .go-next-btn")
# moderate violation relating to header ordering
workflows.import_trs_search.assert_no_axe_violations_with_impact_of_at_least("serious")
self.navigate_to_workflows_import()
# Clicking the TRS card auto-navigates to the TRS method selection step
workflows.import_trs_link.wait_for_and_click()
# Click Next to navigate to TRS method selection
self.wait_for_and_click_selector(".wizard-actions .go-next-btn")
# Clicking the TRS ID card auto-navigates to the TRS ID form
workflows.import_trs_id_link.wait_for_and_click()
# Click Next to navigate to TRS ID view
self.wait_for_and_click_selector(".wizard-actions .go-next-btn")
# ditto - moderate violation relating to header ordering
workflows.import_trs_id.assert_no_axe_violations_with_impact_of_at_least("serious")
+18 -11
View File
@@ -51,6 +51,18 @@ class TestTrsImport(SeleniumIntegrationTestCase):
def trs_config_dir(cls):
return cls.temp_config_dir("trs")
def _click_wizard_import_button(self, wait_for_validation=False):
"""Click the wizard's Import button after ensuring it's enabled.
The Import button is disabled until the current step's validation passes.
Set wait_for_validation=True to sleep briefly before clicking (needed when
user input was just entered and Vue validation hasn't propagated yet).
"""
if wait_for_validation:
self.sleep_for(self.wait_types.UX_RENDER)
import_button = self.wait_for_selector_clickable(".wizard-actions .go-next-btn.btn-primary:not([disabled])")
import_button.click()
def assert_workflow_imported(self, name):
# surround name with quotes to consider case where name contains colons
self.workflow_index_search_for(f'"{name}"')
@@ -69,8 +81,7 @@ class TestTrsImport(SeleniumIntegrationTestCase):
full_url = self.build_url(import_url)
self.driver.get(full_url)
self.components.workflows.workflow_trs_import.wait_for_visible()
# Wait for the wizard's Import button to become enabled and click it
self.components.workflows.import_next_button.wait_for_and_click()
self._click_wizard_import_button()
self.sleep_for(self.wait_types.UX_RENDER)
self.workflow_index_open()
self.assert_workflow_imported(WORKFLOW_NAME)
@@ -81,8 +92,7 @@ class TestTrsImport(SeleniumIntegrationTestCase):
self.components.trs_search.search_result(
workflow_name="galaxy-workflow-dockstore-example-1"
).wait_for_and_click()
# Wait for the wizard's Import button to become enabled and click it
self.components.workflows.import_next_button.wait_for_and_click()
self._click_wizard_import_button(wait_for_validation=True)
self.sleep_for(self.wait_types.UX_RENDER)
self.workflow_index_open()
self.assert_workflow_imported("Test Workflow")
@@ -94,8 +104,7 @@ class TestTrsImport(SeleniumIntegrationTestCase):
# Select version from dropdown
version_select = self.components.trs_search.version_select.wait_for_visible()
Select(version_select).select_by_visible_text("v0.4")
# Wait for the wizard's Import button to become enabled and click it
self.components.workflows.import_next_button.wait_for_and_click()
self._click_wizard_import_button(wait_for_validation=True)
self.sleep_for(self.wait_types.UX_RENDER)
self.workflow_index_open()
self.assert_workflow_imported(WORKFLOW_NAME)
@@ -109,8 +118,7 @@ class TestTrsImport(SeleniumIntegrationTestCase):
self.components.trs_search.select_server(server="workflowhub").wait_for_and_click()
self.components.trs_search.search.wait_for_and_send_keys(WORKFLOW_NAME)
self.components.trs_search.search_result(workflow_name=WORKFLOW_NAME).wait_for_and_click()
# Wait for the wizard's Import button to become enabled and click it
self.components.workflows.import_next_button.wait_for_and_click()
self._click_wizard_import_button(wait_for_validation=True)
self.sleep_for(self.wait_types.UX_RENDER)
self.workflow_index_open()
self.assert_workflow_imported(WORKFLOW_NAME)
@@ -146,8 +154,7 @@ class TestTrsImport(SeleniumIntegrationTestCase):
# Select version from dropdown
version_select = self.components.trs_import.version_select.wait_for_visible()
Select(version_select).select_by_visible_text("v0.4")
# Wait for the wizard's Import button to become enabled and click it
self.components.workflows.import_next_button.wait_for_and_click()
self._click_wizard_import_button(wait_for_validation=True)
self.sleep_for(self.wait_types.UX_RENDER)
self.workflow_index_open()
self.assert_workflow_imported(WORKFLOW_NAME)
@@ -155,7 +162,7 @@ class TestTrsImport(SeleniumIntegrationTestCase):
def _import_by_trs_url(self, trs_url):
self.go_to_trs_by_url()
self.components.trs_import.url_input.wait_for_and_send_keys(trs_url)
self.components.trs_import.url_import_button.wait_for_and_click()
self._click_wizard_import_button(wait_for_validation=True)
self.sleep_for(self.wait_types.UX_RENDER)
self.workflow_index_open()
self.assert_workflow_imported(WORKFLOW_NAME)