TRS Server:
diff --git a/client/src/components/Workflow/Import/TrsUrlImport.vue b/client/src/components/Workflow/Import/TrsUrlImport.vue
index c9f9986c36e..d64a1890d7d 100644
--- a/client/src/components/Workflow/Import/TrsUrlImport.vue
+++ b/client/src/components/Workflow/Import/TrsUrlImport.vue
@@ -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
(), {
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 });
- Import from TRS URL
-
If the workflow is accessible via a TRS URL, enter the URL above and click Import.
-
-
- Import workflow
-
diff --git a/client/src/components/Workflow/Import/workflowValidation.ts b/client/src/components/Workflow/Import/workflowValidation.ts
new file mode 100644
index 00000000000..19b4f69b4ae
--- /dev/null
+++ b/client/src/components/Workflow/Import/workflowValidation.ts
@@ -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 {
+ try {
+ const slice = file.slice(0, 1024);
+ const text = await slice.text();
+ return text.includes(marker);
+ } catch {
+ return false;
+ }
+}
diff --git a/client/src/components/Workflow/WorkflowImport.vue b/client/src/components/Workflow/WorkflowImport.vue
index 3c8d1239899..8d3e149be18 100644
--- a/client/src/components/Workflow/WorkflowImport.vue
+++ b/client/src/components/Workflow/WorkflowImport.vue
@@ -1,10 +1,14 @@