diff --git a/client/src/components/Panels/WorkflowBox.vue b/client/src/components/Panels/WorkflowBox.vue index e36a3b018227..bef5d682603d 100644 --- a/client/src/components/Panels/WorkflowBox.vue +++ b/client/src/components/Panels/WorkflowBox.vue @@ -47,7 +47,7 @@ function userTitle(title: string) { variant="link" :title="userTitle('Create new workflow')" :disabled="isAnonymous" - @click="$router.push('/workflows/create')"> + @click="$router.push('/workflows/edit')"> Name - +
Version @@ -30,6 +34,7 @@
These notes will be visible when this workflow is viewed.
@@ -184,7 +189,7 @@ export default { onTags(tags) { this.tagsCurrent = tags; this.onAttributes({ tags }); - this.$emit("input", this.tagsCurrent); + this.$emit("onTags", this.tagsCurrent); }, onVersion() { this.$emit("onVersion", this.versionCurrent); @@ -200,9 +205,11 @@ export default { this.messageVariant = "danger"; }, onAttributes(data) { - this.services.updateWorkflow(this.id, data).catch((error) => { - this.onError(error); - }); + if (this.id !== "new_temp_workflow") { + this.services.updateWorkflow(this.id, data).catch((error) => { + this.onError(error); + }); + } }, }, }; diff --git a/client/src/components/Workflow/Editor/Index.test.ts b/client/src/components/Workflow/Editor/Index.test.ts index 135ddf7b914a..62e9673f3673 100644 --- a/client/src/components/Workflow/Editor/Index.test.ts +++ b/client/src/components/Workflow/Editor/Index.test.ts @@ -47,9 +47,9 @@ describe("Index", () => { }); wrapper = shallowMount(Index, { propsData: { - id: "workflow_id", + workflowId: "workflow_id", initialVersion: 1, - tags: ["moo", "cow"], + workflowTags: ["moo", "cow"], moduleSections: [], dataManagers: [], workflows: [], diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue index b28414828809..cd82d0bf4339 100644 --- a/client/src/components/Workflow/Editor/Index.vue +++ b/client/src/components/Workflow/Editor/Index.vue @@ -41,7 +41,8 @@
Workflow Editor - {{ name }} + {{ name }} + Create New Workflow
[], }, moduleSections: { type: Array, @@ -235,7 +242,7 @@ export default { setup(props, { emit }) { const { datatypes, datatypesMapper, datatypesMapperLoading } = useDatatypesMapper(); - const { connectionStore, stepStore, stateStore } = provideScopedWorkflowStores(props.id); + const { connectionStore, stepStore, stateStore } = provideScopedWorkflowStores(props.workflowId); const { getStepIndex, steps } = storeToRefs(stepStore); const { activeNodeId } = storeToRefs(stateStore); @@ -280,6 +287,7 @@ export default { }, data() { return { + id: this.workflowId, isCanvas: true, markdownConfig: null, markdownText: null, @@ -291,6 +299,7 @@ export default { creator: null, annotation: null, name: null, + tags: this.workflowTags, stateMessages: [], insertedStateMessages: [], refactorActions: [], @@ -324,10 +333,13 @@ export default { hasActiveNodeTool() { return this.activeStep?.type == "tool"; }, + isNewTempWorkflow() { + return this.id === NEW_TEMP_ID; + }, }, watch: { id(newId, oldId) { - if (oldId) { + if (oldId && oldId !== NEW_TEMP_ID) { this._loadCurrent(newId); } }, @@ -347,7 +359,9 @@ export default { }, created() { this.lastQueue = new LastQueue(); - this._loadCurrent(this.id, this.version); + if (!this.isNewTempWorkflow) { + this._loadCurrent(this.id, this.version); + } hide_modal(); }, methods: { @@ -541,6 +555,40 @@ export default { const step = { ...this.steps[nodeId], annotation: newAnnotation }; this.onUpdateStep(step); }, + async onCreate() { + if (!this.name || !this.annotation) { + const response = "Must provide name and annotation before creation..."; + this.onWorkflowError("Creating workflow failed...", response, { + Ok: () => { + this.hideModal(); + }, + }); + this.onAttributes(); + return; + } + const payload = { + workflow_name: this.name, + workflow_annotation: this.annotation, + workflow_tags: this.tags, + }; + + try { + const { data } = await axios.put(withPrefix("/workflow/create"), payload); + const { id, message } = data; + this.id = id; + this.onWorkflowMessage("Success", message); + const editUrl = `/workflows/edit?id=${id}`; + this.onNavigate(editUrl); + } catch (e) { + this.onWorkflowError("Couldn't create workflow..."), + e, + { + Ok: () => { + this.hideModal(); + }, + }; + } + }, onSetData(stepId, newData) { this.lastQueue .enqueue(() => getModule(newData, stepId, this.stateStore.setLoadingState)) @@ -699,6 +747,11 @@ export default { this.onWorkflowError("Loading workflow failed...", response); }); }, + onTags(tags) { + if (this.tags != tags) { + this.tags = tags; + } + }, onLicense(license) { if (this.license != license) { this.hasChanges = true; diff --git a/client/src/components/Workflow/Editor/Options.vue b/client/src/components/Workflow/Editor/Options.vue index 654292b65056..344c51c485bc 100644 --- a/client/src/components/Workflow/Editor/Options.vue +++ b/client/src/components/Workflow/Editor/Options.vue @@ -7,6 +7,7 @@ import { useConfirmDialog } from "@/composables/confirmDialog"; const emit = defineEmits<{ (e: "onAttributes"): void; (e: "onSave"): void; + (e: "onCreate"): void; (e: "onReport"): void; (e: "onSaveAs"): void; (e: "onLayout"): void; @@ -17,6 +18,7 @@ const emit = defineEmits<{ }>(); const props = defineProps<{ + isNewTempWorkflow?: boolean; hasChanges?: boolean; hasInvalidConnections?: boolean; requiredReindex?: boolean; @@ -25,7 +27,9 @@ const props = defineProps<{ const { confirm } = useConfirmDialog(); const saveHover = computed(() => { - if (!props.hasChanges) { + if (props.isNewTempWorkflow) { + return "Create a new workflow"; + } else if (!props.hasChanges) { return "Workflow has no changes"; } else if (props.hasInvalidConnections) { return "Workflow has invalid connections, review and remove invalid connections"; @@ -34,6 +38,14 @@ const saveHover = computed(() => { } }); +function emitSaveOrCreate() { + if (props.isNewTempWorkflow) { + emit("onCreate"); + } else { + emit("onSave"); + } +} + async function onSave() { if (props.hasInvalidConnections) { console.log("getting confirmation"); @@ -45,10 +57,10 @@ async function onSave() { } ); if (confirmed) { - emit("onSave"); + emitSaveOrCreate(); } } else { - emit("onSave"); + emitSaveOrCreate(); } } @@ -56,7 +68,7 @@ async function onSave() {
+ class="editor-button-options" + :disabled="isNewTempWorkflow"> @@ -113,12 +127,13 @@ async function onSave() { diff --git a/client/src/components/Workflow/Editor/modules/services.js b/client/src/components/Workflow/Editor/modules/services.js index e0bb9c2c4e1c..9dbe3922b478 100644 --- a/client/src/components/Workflow/Editor/modules/services.js +++ b/client/src/components/Workflow/Editor/modules/services.js @@ -54,7 +54,7 @@ export async function loadWorkflow({ id, version = null }) { export async function saveWorkflow(workflow) { if (workflow.hasChanges) { try { - const requestData = { workflow: toSimple(workflow), from_tool_form: true }; + const requestData = { workflow: toSimple(workflow), from_tool_form: true, tags: workflow.tags }; const { data } = await axios.put(`${getAppRoot()}api/workflows/${workflow.id}`, requestData); workflow.name = data.name; workflow.hasChanges = false; diff --git a/client/src/components/Workflow/InvocationsList.test.js b/client/src/components/Workflow/InvocationsList.test.js index 13885868164f..d2f230303a08 100644 --- a/client/src/components/Workflow/InvocationsList.test.js +++ b/client/src/components/Workflow/InvocationsList.test.js @@ -6,16 +6,20 @@ import axios from "axios"; import MockAdapter from "axios-mock-adapter"; import { formatDistanceToNow, parseISO } from "date-fns"; import { getLocalVue } from "tests/jest/helpers"; +import VueRouter from "vue-router"; import InvocationsList from "./InvocationsList"; import mockInvocationData from "./test/json/invocation.json"; const localVue = getLocalVue(); +localVue.use(VueRouter); +const router = new VueRouter(); const pinia = createTestingPinia(); describe("InvocationsList.vue", () => { let axiosMock; let wrapper; + let $router; beforeEach(async () => { axiosMock = new MockAdapter(axios); @@ -158,7 +162,9 @@ describe("InvocationsList.vue", () => { }, localVue, pinia, + router, }); + $router = wrapper.vm.$router; }); it("renders one row", async () => { @@ -194,8 +200,10 @@ describe("InvocationsList.vue", () => { }); it("calls executeWorkflow", async () => { + const mockMethod = jest.fn(); + $router.push = mockMethod; await wrapper.find(".workflow-run").trigger("click"); - expect(window.location).toBeAt("workflows/run?id=workflowId"); + expect(mockMethod).toHaveBeenCalledWith("/workflows/run?id=workflowId"); }); it("should not render pager", async () => { diff --git a/client/src/components/Workflow/WorkflowIndexActions.test.js b/client/src/components/Workflow/WorkflowIndexActions.test.js index 9a9999b9885e..9704a168c3db 100644 --- a/client/src/components/Workflow/WorkflowIndexActions.test.js +++ b/client/src/components/Workflow/WorkflowIndexActions.test.js @@ -35,7 +35,7 @@ describe("WorkflowIndexActions", () => { describe("naviation", () => { it("should create a workflow when create is clicked", async () => { await wrapper.find(ROOT_COMPONENT.workflows.new_button.selector).trigger("click"); - expect(getCurrentPath($router)).toBe("/workflows/create"); + expect(getCurrentPath($router)).toBe("/workflows/edit"); }); it("should import a workflow when create is clicked", async () => { diff --git a/client/src/components/Workflow/WorkflowIndexActions.vue b/client/src/components/Workflow/WorkflowIndexActions.vue index 7e549cdec6a2..c067b483dcf7 100644 --- a/client/src/components/Workflow/WorkflowIndexActions.vue +++ b/client/src/components/Workflow/WorkflowIndexActions.vue @@ -54,7 +54,7 @@ export default { }, methods: { navigateToCreate: function () { - this.$router.push("/workflows/create"); + this.$router.push("/workflows/edit"); }, navigateToImport: function () { this.$router.push("/workflows/import"); diff --git a/client/src/components/Workflow/WorkflowRunButton.vue b/client/src/components/Workflow/WorkflowRunButton.vue index 08d0c81ad492..26e40e8fe949 100644 --- a/client/src/components/Workflow/WorkflowRunButton.vue +++ b/client/src/components/Workflow/WorkflowRunButton.vue @@ -34,7 +34,7 @@ export default { }, methods: { executeWorkflow() { - window.location.assign(`${this.root}workflows/run?id=${this.id}`); + this.$router.push(`/workflows/run?id=${this.id}`); }, }, }; diff --git a/client/src/entry/analysis/modules/WorkflowEditor.vue b/client/src/entry/analysis/modules/WorkflowEditor.vue index 15b2d47fb770..3ef6913db658 100644 --- a/client/src/entry/analysis/modules/WorkflowEditor.vue +++ b/client/src/entry/analysis/modules/WorkflowEditor.vue @@ -1,11 +1,11 @@ diff --git a/lib/galaxy/webapps/galaxy/controllers/workflow.py b/lib/galaxy/webapps/galaxy/controllers/workflow.py index 3772a29cd10c..00747388bb01 100644 --- a/lib/galaxy/webapps/galaxy/controllers/workflow.py +++ b/lib/galaxy/webapps/galaxy/controllers/workflow.py @@ -451,12 +451,16 @@ def editor(self, trans, id=None, workflow_id=None, version=None, **kwargs): an iframe (necessary for scrolling to work properly), which is rendered by `editor_canvas`. """ + + new_workflow = False if not id: if workflow_id: stored_workflow = self.app.workflow_manager.get_stored_workflow(trans, workflow_id, by_stored_id=False) self.security_check(trans, stored_workflow, True, False) id = trans.security.encode_id(stored_workflow.id) - stored = self.get_stored_workflow(trans, id) + else: + new_workflow = True + # The following query loads all user-owned workflows, # So that they can be copied or inserted in the workflow editor. workflows = ( @@ -466,10 +470,6 @@ def editor(self, trans, id=None, workflow_id=None, version=None, **kwargs): .options(joinedload(model.StoredWorkflow.latest_workflow).joinedload(model.Workflow.steps)) .all() ) - if version is None: - version = len(stored.workflows) - 1 - else: - version = int(version) # create workflow module models module_sections = [] @@ -501,6 +501,18 @@ def editor(self, trans, id=None, workflow_id=None, version=None, **kwargs): } ) + stored = None + if new_workflow is False: + stored = self.get_stored_workflow(trans, id) + + if version is None: + version = len(stored.workflows) - 1 + else: + version = int(version) + + # identify item tags + item_tags = stored.make_tag_string_list() + # create workflow models workflows = [ { @@ -510,24 +522,28 @@ def editor(self, trans, id=None, workflow_id=None, version=None, **kwargs): "name": workflow.name, } for workflow in workflows - if workflow.id != stored.id + if new_workflow or workflow.id != stored.id ] - # identify item tags - item_tags = stored.make_tag_string_list() - # build workflow editor model editor_config = { - "id": trans.security.encode_id(stored.id), - "name": stored.name, - "tags": item_tags, - "initialVersion": version, - "annotation": self.get_item_annotation_str(trans.sa_session, trans.user, stored), "moduleSections": module_sections, "dataManagers": data_managers, "workflows": workflows, } + # if existing workflow, add add its data to the model + if new_workflow is False: + editor_config.update( + { + "id": trans.security.encode_id(stored.id), + "name": stored.name, + "tags": item_tags, + "initialVersion": version, + "annotation": self.get_item_annotation_str(trans.sa_session, trans.user, stored), + } + ) + # parse to mako return editor_config