mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #9346 from guerler/workflow_editor_node_terminals
Remove Backbone and Globals dependency from Workflow client code
This commit is contained in:
@@ -93,8 +93,7 @@ export { panelManagement } from "onload/globalInits/panelManagement";
|
||||
export { mountMakoTags } from "components/Tags";
|
||||
export { mountJobMetrics } from "components/JobMetrics";
|
||||
export { mountJobParameters } from "components/JobParameters";
|
||||
export { mountWorkflowEditor, mountWorkflowPanel } from "components/Workflow/Editor/mount";
|
||||
export { mountToolBoxWorkflow } from "components/Panels/mount";
|
||||
export { mountWorkflowEditor } from "components/Workflow/Editor/mount";
|
||||
export { mountPageDisplay } from "components/PageDisplay";
|
||||
|
||||
// Used in common.mako
|
||||
|
||||
@@ -65,33 +65,26 @@ describe("ToolSection", () => {
|
||||
}
|
||||
});
|
||||
expect(wrapper.vm.opened).to.equal(true);
|
||||
|
||||
const $sectionName = wrapper.find(".name");
|
||||
$sectionName.trigger("click");
|
||||
await Vue.nextTick();
|
||||
expect(wrapper.vm.opened).to.equal(false);
|
||||
|
||||
wrapper.vm.queryFilter = "";
|
||||
wrapper.setProps({ queryFilter: "" });
|
||||
await Vue.nextTick();
|
||||
expect(wrapper.vm.opened).to.equal(false);
|
||||
|
||||
wrapper.vm.queryFilter = "test";
|
||||
wrapper.setProps({ queryFilter: "test" });
|
||||
await Vue.nextTick();
|
||||
expect(wrapper.vm.opened).to.equal(true);
|
||||
|
||||
wrapper.vm.disableFilter = true;
|
||||
wrapper.setProps({ disableFilter: true });
|
||||
await Vue.nextTick();
|
||||
expect(wrapper.vm.opened).to.equal(true);
|
||||
|
||||
wrapper.vm.queryFilter = "";
|
||||
wrapper.setProps({ queryFilter: "" });
|
||||
await Vue.nextTick();
|
||||
expect(wrapper.vm.opened).to.equal(false);
|
||||
|
||||
$sectionName.trigger("click");
|
||||
await Vue.nextTick();
|
||||
expect(wrapper.vm.opened).to.equal(true);
|
||||
|
||||
wrapper.vm.queryFilter = "test";
|
||||
wrapper.setProps({ queryFilter: "test" });
|
||||
await Vue.nextTick();
|
||||
expect(wrapper.vm.opened).to.equal(false);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
<template>
|
||||
<div :id="side" class="unified-panel-outer-wrap" :style="styles">
|
||||
<component :is="currentPanel" v-bind="currentPanelProperties" />
|
||||
<slot name="panel">
|
||||
<component :is="currentPanel" v-bind="currentPanelProperties" />
|
||||
</slot>
|
||||
<div class="unified-panel-footer">
|
||||
<div
|
||||
class="panel-collapse"
|
||||
|
||||
@@ -22,8 +22,8 @@
|
||||
@onClick="onInsertModule"
|
||||
/>
|
||||
<tool-section
|
||||
:category="dataManagers"
|
||||
:key="dataManagers.id"
|
||||
:category="dataManagerSection"
|
||||
:key="dataManagerSection.id"
|
||||
:query-filter="query"
|
||||
:disable-filter="true"
|
||||
@onClick="onInsertTool"
|
||||
@@ -40,7 +40,7 @@
|
||||
<tool-section
|
||||
:category="workflowSection"
|
||||
:key="workflowSection.name"
|
||||
operation-icon="fa fa-copy"
|
||||
operation-icon="fa fa-files-o"
|
||||
operation-title="Insert individual steps."
|
||||
:query-filter="query"
|
||||
:disable-filter="true"
|
||||
@@ -53,6 +53,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _l from "utils/localization";
|
||||
import ToolSection from "./Common/ToolSection";
|
||||
import ToolSearch from "./Common/ToolSearch";
|
||||
import { filterToolSections } from "./utilities";
|
||||
@@ -74,20 +75,29 @@ export default {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
workflowSection: {
|
||||
type: Object
|
||||
},
|
||||
workflowGlobals: {
|
||||
type: Object
|
||||
workflows: {
|
||||
type: Array
|
||||
},
|
||||
dataManagers: {
|
||||
type: Object
|
||||
type: Array
|
||||
},
|
||||
moduleSections: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
workflowSection() {
|
||||
return {
|
||||
name: _l("Workflows"),
|
||||
elems: this.workflows
|
||||
};
|
||||
},
|
||||
dataManagerSection() {
|
||||
return {
|
||||
name: _l("Data Managers"),
|
||||
elems: this.dataManagers
|
||||
};
|
||||
},
|
||||
categories() {
|
||||
return filterToolSections(this.toolsLayout, this.results);
|
||||
},
|
||||
@@ -114,18 +124,18 @@ export default {
|
||||
},
|
||||
onInsertTool(tool, evt) {
|
||||
evt.preventDefault();
|
||||
this.workflowGlobals.app.add_node_for_tool(tool.id, tool.name);
|
||||
this.$emit("onInsertTool", tool.id, tool.name);
|
||||
},
|
||||
onInsertModule(module, evt) {
|
||||
evt.preventDefault();
|
||||
this.workflowGlobals.app.add_node_for_module(module.name, module.title);
|
||||
this.$emit("onInsertModule", module.name, module.title);
|
||||
},
|
||||
onInsertWorkflow(workflow, evt) {
|
||||
evt.preventDefault();
|
||||
this.workflowGlobals.app.add_node_for_subworkflow(workflow.latest_id, workflow.name);
|
||||
this.$emit("onInsertWorkflow", workflow.latest_id, workflow.name);
|
||||
},
|
||||
onInsertWorkflowSteps(workflow) {
|
||||
this.workflowGlobals.app.copy_into_workflow(workflow.id, workflow.step_count);
|
||||
this.$emit("onInsertWorkflowSteps", workflow.id, workflow.step_count);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
/**
|
||||
* Endpoint for mounting Panel components
|
||||
*/
|
||||
import Vue from "vue";
|
||||
import _l from "utils/localization";
|
||||
import SidePanel from "./SidePanel";
|
||||
import ToolBoxWorkflow from "./ToolBoxWorkflow";
|
||||
|
||||
export const mountToolBoxWorkflow = options => {
|
||||
const component = Vue.extend(SidePanel);
|
||||
return new component({
|
||||
propsData: {
|
||||
side: "left",
|
||||
currentPanel: ToolBoxWorkflow,
|
||||
currentPanelProperties: {
|
||||
toolbox: options.toolbox,
|
||||
workflowGlobals: options.workflow_globals,
|
||||
moduleSections: options.module_sections,
|
||||
dataManagers: {
|
||||
name: _l("Data Managers"),
|
||||
elems: options.data_managers
|
||||
},
|
||||
workflowSection: {
|
||||
name: _l("Workflows"),
|
||||
elems: options.workflows
|
||||
}
|
||||
}
|
||||
},
|
||||
el: "#left"
|
||||
});
|
||||
};
|
||||
@@ -21,6 +21,10 @@ describe("SelectionDialog.vue", () => {
|
||||
options: "<tree-options />",
|
||||
search: "<cool-search />"
|
||||
},
|
||||
stubs: {
|
||||
"tree-options": "<div id='tree-options'/>",
|
||||
"cool-search": "<div id='cool-search'/>"
|
||||
},
|
||||
propsData: mockOptions,
|
||||
localVue
|
||||
});
|
||||
@@ -29,16 +33,16 @@ describe("SelectionDialog.vue", () => {
|
||||
it("loads correctly in loading state, shows options when optionsShow becomes true", async () => {
|
||||
expect(wrapper.find(".fa-spinner").text()).to.equals("");
|
||||
expect(wrapper.contains(".fa-spinner")).to.equals(true);
|
||||
expect(wrapper.contains("tree-options")).to.equals(false);
|
||||
expect(wrapper.contains("#tree-options")).to.equals(false);
|
||||
wrapper.setProps({ optionsShow: true });
|
||||
await localVue.nextTick();
|
||||
expect(wrapper.contains(".fa-spinner")).to.equals(false);
|
||||
expect(wrapper.contains("tree-options")).to.equals(true);
|
||||
expect(wrapper.contains("#tree-options")).to.equals(true);
|
||||
});
|
||||
|
||||
it("loads search correctly", async () => {
|
||||
await localVue.nextTick();
|
||||
expect(wrapper.contains("cool-search")).to.equals(true);
|
||||
expect(wrapper.contains("#cool-search")).to.equals(true);
|
||||
});
|
||||
|
||||
it("hideModal called on click cancel", async () => {
|
||||
|
||||
@@ -11,7 +11,7 @@ upstream component or environment that is accessed through props and events -->
|
||||
class="toggle-link"
|
||||
@click.prevent="toggleTagDisplay"
|
||||
>
|
||||
{{ linkText | localize }}
|
||||
{{ linkText }}
|
||||
</a>
|
||||
<vue-tags-input
|
||||
v-if="tagsVisible"
|
||||
@@ -20,7 +20,7 @@ upstream component or environment that is accessed through props and events -->
|
||||
:tags="tagModels"
|
||||
:autocomplete-items="autocompleteTags"
|
||||
:disabled="disabled"
|
||||
:placeholder="'Add Tags' | localize"
|
||||
placeholder="Add Tags"
|
||||
:add-on-key="triggerKeys"
|
||||
@before-adding-tag="beforeAddingTag"
|
||||
@before-deleting-tag="beforeDeletingTag"
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import Vue from "vue";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import Attributes from "./Attributes";
|
||||
import { __RewireAPI__ as rewire } from "./Attributes";
|
||||
|
||||
describe("Attributes", () => {
|
||||
beforeEach(() => {
|
||||
rewire.__Rewire__(
|
||||
"Services",
|
||||
class {
|
||||
async updateWorkflow() {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
it("test attributes", async () => {
|
||||
const wrapper = mount(Attributes, {
|
||||
propsData: {
|
||||
id: "workflow_id",
|
||||
name: "workflow_name",
|
||||
tags: ["workflow_tag_0", "workflow_tag_1"],
|
||||
parameters: ["workflow_parameter_0", "workflow_parameter_1"],
|
||||
versions: ["workflow_version_0"]
|
||||
}
|
||||
});
|
||||
const name = wrapper.find("#workflow-name");
|
||||
expect(name.element.value).to.equal("workflow_name");
|
||||
wrapper.setProps({ name: "new_workflow_name" });
|
||||
await Vue.nextTick();
|
||||
expect(name.element.value).to.equal("new_workflow_name");
|
||||
const parameters = wrapper.findAll(".list-group-item");
|
||||
expect(parameters.length).to.equal(2);
|
||||
expect(parameters.at(0).text()).to.equal("1: workflow_parameter_0");
|
||||
expect(parameters.at(1).text()).to.equal("2: workflow_parameter_1");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<div id="edit-attributes" class="right-content p-2">
|
||||
<b-alert :variant="messageVariant" :show="!!message">
|
||||
{{ message }}
|
||||
</b-alert>
|
||||
<div id="workflow-name-area">
|
||||
<b>Name</b>
|
||||
<b-input id="workflow-name" :value="name" @change="onRename" />
|
||||
</div>
|
||||
<div id="workflow-version-area" class="mt-2">
|
||||
<b>Version</b>
|
||||
<b-form-select v-model="versionCurrent" @change="onVersion">
|
||||
<b-form-select-option v-for="v in versionOptions" :key="v.version" :value="v.version">
|
||||
{{ v.label }}
|
||||
</b-form-select-option>
|
||||
</b-form-select>
|
||||
</div>
|
||||
<div v-if="hasParameters" id="workflow-parameters-area" class="mt-2">
|
||||
<b>Parameters</b>
|
||||
<b-list-group>
|
||||
<b-list-group-item v-for="[key, p] in parameters.entries()" :key="key"
|
||||
>{{ key + 1 }}: {{ p }}
|
||||
</b-list-group-item>
|
||||
</b-list-group>
|
||||
</div>
|
||||
<div id="workflow-annotation-area" class="mt-2">
|
||||
<b>Annotation</b>
|
||||
<b-textarea id="workflow-annotation" :value="annotation" @change="onAnnotation" />
|
||||
<div class="form-text text-muted">
|
||||
These notes will be visible when this workflow is viewed.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<b>Tags</b>
|
||||
<Tags :tags="tagsCurrent" @input="onTags" />
|
||||
<div class="form-text text-muted">
|
||||
Apply tags to make it easy to search for and find items with the same tag.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import { Services } from "components/Workflow/services";
|
||||
import Tags from "components/Common/Tags";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
export default {
|
||||
name: "Attributes",
|
||||
components: {
|
||||
Tags
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
annotation: {
|
||||
type: String
|
||||
},
|
||||
version: {
|
||||
type: Number
|
||||
},
|
||||
versions: {
|
||||
type: Array
|
||||
},
|
||||
parameters: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
message: null,
|
||||
messageVariant: null,
|
||||
tagsCurrent: this.tags,
|
||||
versionCurrent: this.version
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.services = new Services();
|
||||
},
|
||||
computed: {
|
||||
hasParameters() {
|
||||
return this.parameters.length > 0;
|
||||
},
|
||||
versionOptions() {
|
||||
const versions = [];
|
||||
for (let i = 0; i < this.versions.length; i++) {
|
||||
const current_wf = this.versions[i];
|
||||
let label = `Version ${current_wf.version}, ${current_wf.steps} steps`;
|
||||
if (i == this.version) {
|
||||
label = `${label} (active)`;
|
||||
}
|
||||
versions.push({
|
||||
version: i,
|
||||
label: label
|
||||
});
|
||||
}
|
||||
return versions;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onTags(tags) {
|
||||
this.tagsCurrent = tags;
|
||||
this.onAttributes({ tags });
|
||||
},
|
||||
onAnnotation(annotation) {
|
||||
this.onAttributes({ annotation });
|
||||
},
|
||||
onRename(name) {
|
||||
this.onAttributes({ name });
|
||||
this.$emit("onRename", name);
|
||||
},
|
||||
onVersion() {
|
||||
this.$emit("onVersion", this.versionCurrent);
|
||||
},
|
||||
onError(error) {
|
||||
this.message = error;
|
||||
this.messageVariant = "danger";
|
||||
},
|
||||
onAttributes(data) {
|
||||
this.services.updateWorkflow(this.id, data).catch(error => {
|
||||
this.onError(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
<template>
|
||||
<div class="unified-panel">
|
||||
<div class="unified-panel-header" unselectable="on">
|
||||
<div class="unified-panel-header-inner">
|
||||
<slot name="buttons" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="unified-panel-body workflow-right">
|
||||
<div v-if="canvas" class="m-1">
|
||||
<slot name="attributes" />
|
||||
<div id="right-content" class="right-content" />
|
||||
</div>
|
||||
<ReportHelp v-else />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ReportHelp from "./ReportHelp";
|
||||
|
||||
export default {
|
||||
name: "EditorPanel",
|
||||
components: {
|
||||
ReportHelp
|
||||
},
|
||||
props: {
|
||||
canvas: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,82 +1,230 @@
|
||||
<template>
|
||||
<div id="center" class="inbound">
|
||||
<div class="unified-panel-header" unselectable="on">
|
||||
<div class="unified-panel-header-inner">
|
||||
<span class="sr-only">Workflow Editor</span>
|
||||
{{ editorConfig.name }}
|
||||
<WorkflowOptions
|
||||
:canvas="isCanvas"
|
||||
@onSave="onSave"
|
||||
@onSaveAs="onSaveAs"
|
||||
@onRun="onRun"
|
||||
@onDownload="onDownload"
|
||||
@onReport="onReport"
|
||||
@onReportHelp="onReportHelp"
|
||||
@onLayout="onLayout"
|
||||
@onEdit="onEdit"
|
||||
@onAttributes="onAttributes"
|
||||
<div id="columns" class="workflow-client">
|
||||
<SidePanel id="left" side="left">
|
||||
<template v-slot:panel>
|
||||
<ToolBoxWorkflow
|
||||
:toolbox="toolbox"
|
||||
:module-sections="module_sections"
|
||||
:data-managers="data_managers"
|
||||
:workflows="workflows"
|
||||
@onInsertTool="onInsertTool"
|
||||
@onInsertModule="onInsertModule"
|
||||
@onInsertWorkflow="onInsertWorkflow"
|
||||
@onInsertWorkflowSteps="onInsertWorkflowSteps"
|
||||
/>
|
||||
</template>
|
||||
</SidePanel>
|
||||
<div id="center" class="workflow-center inbound">
|
||||
<div class="unified-panel-header" unselectable="on">
|
||||
<div class="unified-panel-header-inner">
|
||||
<span class="sr-only">Workflow Editor</span>
|
||||
{{ name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="unified-panel-body" id="workflow-canvas-body" v-show="isCanvas">
|
||||
<div id="canvas-viewport" class="workflow-canvas-content">
|
||||
<div id="canvas-container" />
|
||||
</div>
|
||||
<div id="workflow-parameters-box">
|
||||
<span class="workflow-parameters-box-title">
|
||||
Workflow Parameters
|
||||
</span>
|
||||
<div id="workflow-parameters-container" />
|
||||
</div>
|
||||
<div class="workflow-overview" aria-hidden="true">
|
||||
<div class="workflow-overview-body">
|
||||
<div id="overview">
|
||||
<canvas width="0" height="0" id="overview-canvas" />
|
||||
<div id="overview-viewport" />
|
||||
<div id="workflow-canvas" class="unified-panel-body workflow-canvas" v-show="isCanvas">
|
||||
<ZoomControl :zoom-level="zoomLevel" @onZoom="onZoom" />
|
||||
<div id="canvas-viewport">
|
||||
<div ref="canvas" id="canvas-container" />
|
||||
</div>
|
||||
<div class="workflow-overview" aria-hidden="true">
|
||||
<div class="workflow-overview-body">
|
||||
<div id="overview-container">
|
||||
<canvas width="0" height="0" id="overview-canvas" />
|
||||
<div id="overview-viewport" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="unified-panel-body workflow-report-body" v-show="!isCanvas">
|
||||
<MarkdownEditor ref="report-editor" initial-markdown="" :onupdate="onReportUpdate" :toolbar="false" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="unified-panel-body workflow-report-body" v-show="!isCanvas">
|
||||
<markdown-editor ref="report-editor" initial-markdown="" :onupdate="onReportUpdate" :toolbar="false" />
|
||||
</div>
|
||||
<SidePanel id="right" side="right">
|
||||
<template v-slot:panel>
|
||||
<EditorPanel :canvas="isCanvas" class="workflow-panel">
|
||||
<template v-slot:attributes>
|
||||
<WorkflowAttributes
|
||||
:id="id"
|
||||
:name="name"
|
||||
:tags="tags"
|
||||
:parameters="parameters"
|
||||
:annotation="annotation"
|
||||
:version="version"
|
||||
:versions="versions"
|
||||
@onVersion="onVersion"
|
||||
@onRename="onRename"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:buttons>
|
||||
<WorkflowOptions
|
||||
:canvas="isCanvas"
|
||||
@onSave="onSave"
|
||||
@onSaveAs="onSaveAs"
|
||||
@onRun="onRun"
|
||||
@onDownload="onDownload"
|
||||
@onReport="onReport"
|
||||
@onLayout="onLayout"
|
||||
@onEdit="onEdit"
|
||||
@onAttributes="onAttributes"
|
||||
/>
|
||||
</template>
|
||||
</EditorPanel>
|
||||
</template>
|
||||
</SidePanel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { WorkflowView } from "mvc/workflow/workflow-view";
|
||||
import { getDatatypes, getModule, getVersions, saveWorkflow, loadWorkflow } from "./services";
|
||||
import {
|
||||
showWarnings,
|
||||
showUpgradeMessage,
|
||||
copyIntoWorkflow,
|
||||
getWorkflowParameters,
|
||||
showAttributes,
|
||||
showForm,
|
||||
saveAs
|
||||
} from "./utilities";
|
||||
import WorkflowManager from "mvc/workflow/workflow-manager";
|
||||
import WorkflowOptions from "./Options";
|
||||
import MarkdownEditor from "components/Markdown/MarkdownEditor";
|
||||
import ToolBoxWorkflow from "components/Panels/ToolBoxWorkflow";
|
||||
import SidePanel from "components/Panels/SidePanel";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { showReportHelp } from "./reportHelp";
|
||||
import reportDefault from "./reportDefault";
|
||||
import EditorPanel from "./EditorPanel";
|
||||
import { hide_modal, show_message, show_modal } from "layout/modal";
|
||||
import WorkflowAttributes from "./Attributes";
|
||||
import ZoomControl from "./ZoomControl";
|
||||
|
||||
export default {
|
||||
components: { MarkdownEditor, WorkflowOptions },
|
||||
components: {
|
||||
EditorPanel,
|
||||
MarkdownEditor,
|
||||
SidePanel,
|
||||
ToolBoxWorkflow,
|
||||
WorkflowOptions,
|
||||
WorkflowAttributes,
|
||||
ZoomControl
|
||||
},
|
||||
props: {
|
||||
editorConfig: {
|
||||
type: Object
|
||||
id: {
|
||||
type: String
|
||||
},
|
||||
version: {
|
||||
type: Number
|
||||
},
|
||||
name: {
|
||||
type: String
|
||||
},
|
||||
tags: {
|
||||
type: Array
|
||||
},
|
||||
annotation: {
|
||||
type: String
|
||||
},
|
||||
module_sections: {
|
||||
type: Array
|
||||
},
|
||||
data_managers: {
|
||||
type: Array
|
||||
},
|
||||
workflows: {
|
||||
type: Array
|
||||
},
|
||||
toolbox: {
|
||||
type: Array
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isCanvas: true
|
||||
isCanvas: true,
|
||||
versions: [],
|
||||
parameters: [],
|
||||
zoomLevel: 7
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.workflowView = new WorkflowView(this.editorConfig, this.$refs["report-editor"]);
|
||||
created() {
|
||||
getDatatypes().then(response => {
|
||||
const datatypes = response.datatypes;
|
||||
const datatypes_mapping = response.datatypes_mapping;
|
||||
this.manager = new WorkflowManager({ datatypes_mapping }, this.$refs.canvas);
|
||||
this.manager
|
||||
.on("onRemoveNode", () => {
|
||||
showAttributes();
|
||||
})
|
||||
.on("onActiveNode", node => {
|
||||
showForm(this.manager, node, datatypes);
|
||||
})
|
||||
.on("onNodeChange", () => {
|
||||
this.parameters = getWorkflowParameters(this.manager.nodes);
|
||||
});
|
||||
this.loadCurrent(this.id, this.version);
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
onInsertTool(tool_id, tool_name) {
|
||||
if (!this.isCanvas) {
|
||||
this.isCanvas = true;
|
||||
return;
|
||||
}
|
||||
var node = this.manager.create_node("tool", tool_name, tool_id);
|
||||
const requestData = {
|
||||
type: "tool",
|
||||
tool_id: tool_id,
|
||||
_: "true"
|
||||
};
|
||||
getModule(requestData).then(response => {
|
||||
this.manager.set_node(node, response);
|
||||
});
|
||||
},
|
||||
onInsertModule(module_id, module_name) {
|
||||
if (!this.isCanvas) {
|
||||
this.isCanvas = true;
|
||||
return;
|
||||
}
|
||||
var node = this.manager.create_node(module_id, module_name);
|
||||
const requestData = {
|
||||
type: module_id,
|
||||
_: "true"
|
||||
};
|
||||
getModule(requestData).then(response => {
|
||||
this.manager.set_node(node, response);
|
||||
});
|
||||
},
|
||||
onInsertWorkflow(workflow_id, workflow_name) {
|
||||
if (!this.isCanvas) {
|
||||
this.isCanvas = true;
|
||||
return;
|
||||
}
|
||||
var node = this.manager.create_node("subworkflow", workflow_name, workflow_id);
|
||||
const requestData = {
|
||||
type: "subworkflow",
|
||||
content_id: workflow_id,
|
||||
_: "true"
|
||||
};
|
||||
getModule(requestData).then(response => {
|
||||
this.manager.set_node(node, response);
|
||||
});
|
||||
},
|
||||
onInsertWorkflowSteps(workflow_id, step_count) {
|
||||
if (!this.isCanvas) {
|
||||
this.isCanvas = true;
|
||||
return;
|
||||
}
|
||||
copyIntoWorkflow(this.manager, workflow_id, step_count);
|
||||
},
|
||||
onDownload() {
|
||||
window.location = `${getAppRoot()}api/workflows/${this.editorConfig.id}/download?format=json-download`;
|
||||
window.location = `${getAppRoot()}api/workflows/${this.id}/download?format=json-download`;
|
||||
},
|
||||
onSaveAs() {
|
||||
this.workflowView.workflow_save_as();
|
||||
saveAs(this.manager);
|
||||
},
|
||||
onLayout() {
|
||||
this.workflowView.layout_editor();
|
||||
this.manager.layout_auto();
|
||||
},
|
||||
onAttributes() {
|
||||
this.workflowView.workflow.clear_active_node();
|
||||
showAttributes();
|
||||
},
|
||||
onEdit() {
|
||||
this.isCanvas = true;
|
||||
@@ -84,94 +232,64 @@ export default {
|
||||
onReport() {
|
||||
this.isCanvas = false;
|
||||
},
|
||||
onRename(name) {
|
||||
this.name = name;
|
||||
},
|
||||
onReportUpdate(markdown) {
|
||||
this.workflowView.report_changed(markdown);
|
||||
this.manager.has_changes = true;
|
||||
this.manager.report.markdown = markdown;
|
||||
},
|
||||
onRun() {
|
||||
window.location = `${getAppRoot()}workflows/run?id=${this.editorConfig.id}`;
|
||||
window.location = `${getAppRoot()}workflows/run?id=${this.id}`;
|
||||
},
|
||||
onZoom(zoomLevel) {
|
||||
this.zoomLevel = this.manager.canvas_manager.setZoom(zoomLevel);
|
||||
},
|
||||
onSave() {
|
||||
this.workflowView.save_current_workflow();
|
||||
show_message("Saving workflow...", "progress");
|
||||
saveWorkflow(this.manager, this.id)
|
||||
.then(data => {
|
||||
showWarnings(data);
|
||||
getVersions(this.id).then(versions => {
|
||||
this.versions = versions;
|
||||
hide_modal();
|
||||
});
|
||||
})
|
||||
.catch(response => {
|
||||
show_modal("Saving workflow failed...", response, { Ok: hide_modal });
|
||||
});
|
||||
},
|
||||
onReportHelp() {
|
||||
showReportHelp();
|
||||
onVersion(version) {
|
||||
if (version != this.manager.workflow_version) {
|
||||
if (this.manager.has_changes) {
|
||||
const r = window.confirm(
|
||||
"There are unsaved changes to your workflow which will be lost. Continue ?"
|
||||
);
|
||||
if (r == false) {
|
||||
this.version = this.manager.workflow_version;
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.version = version;
|
||||
this.loadCurrent(this.id, version);
|
||||
}
|
||||
},
|
||||
loadCurrent(id, version) {
|
||||
show_message("Loading workflow...", "progress");
|
||||
loadWorkflow(this.manager, id, version)
|
||||
.then(data => {
|
||||
const report = data.report || {};
|
||||
const markdown = report.markdown || reportDefault;
|
||||
this.$refs["report-editor"].input = markdown;
|
||||
showUpgradeMessage(this.manager, data);
|
||||
getVersions(this.id).then(versions => {
|
||||
this.versions = versions;
|
||||
});
|
||||
})
|
||||
.catch(response => {
|
||||
show_modal("Loading workflow failed...", response, { Ok: hide_modal });
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#center {
|
||||
z-index: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.workflow-report-body {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.workflow-overview-body {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-top: solid gray 1px;
|
||||
border-left: solid grey 1px;
|
||||
}
|
||||
|
||||
#canvas-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#workflow-parameters-box {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0px;
|
||||
border: solid grey 1px;
|
||||
padding: 5px;
|
||||
background: #eeeeee;
|
||||
z-index: 20000;
|
||||
overflow: auto;
|
||||
max-width: 300px;
|
||||
max-height: 300px;
|
||||
}
|
||||
|
||||
.workflow-parameters-box-title {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
#overview {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
#overview-canvas {
|
||||
background: white;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#overview-viewport {
|
||||
position: absolute;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
border: solid blue 1px;
|
||||
z-index: 10;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
canvas {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
canvas.dragging {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import Vue from "vue";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import Node from "./Node";
|
||||
|
||||
describe("Node", () => {
|
||||
it("test attributes", async () => {
|
||||
const wrapper = mount(Node, {
|
||||
propsData: {
|
||||
id: "node-id",
|
||||
title: "node-title",
|
||||
type: "tool"
|
||||
}
|
||||
});
|
||||
const icon = wrapper.find("i");
|
||||
expect(icon.classes())
|
||||
.to.be.an("array")
|
||||
.that.includes("fa-wrench");
|
||||
const toolLinks = wrapper.findAll("a");
|
||||
expect(toolLinks.length).to.equal(2);
|
||||
wrapper.setProps({ type: "subworkflow" });
|
||||
await Vue.nextTick();
|
||||
expect(icon.classes())
|
||||
.to.be.an("array")
|
||||
.that.includes("fa-sitemap");
|
||||
const subworkflowLinks = wrapper.findAll("a");
|
||||
expect(subworkflowLinks.length).to.equal(1);
|
||||
const workflowTitle = wrapper.find(".node-title");
|
||||
expect(workflowTitle.text()).to.equal("node-title");
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,7 @@
|
||||
<div>
|
||||
<div class="toolFormTitle unselectable clearfix">
|
||||
<i :class="iconClass" />
|
||||
<span class="nodeTitle">{{ title }}</span>
|
||||
<span class="node-title">{{ title }}</span>
|
||||
<span class="sr-only"> Node</span>
|
||||
<div class="buttons float-right">
|
||||
<a
|
||||
@@ -23,15 +23,21 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="toolFormBody">
|
||||
<div><span class="fa fa-spinner fa-spin" /> Loading details...</div>
|
||||
<div>
|
||||
<loading-span message="Loading details" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import WorkflowIcons from "mvc/workflow/workflow-icons";
|
||||
import WorkflowIcons from "components/Workflow/icons";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingSpan
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
variant="link"
|
||||
aria-label="Save Workflow"
|
||||
v-b-tooltip.hover
|
||||
v-b-tooltip.bottom
|
||||
@click="$emit('onSave')"
|
||||
>
|
||||
<span class="fa fa-floppy-o" />
|
||||
@@ -20,7 +19,6 @@
|
||||
variant="link"
|
||||
aria-label="Edit Report"
|
||||
v-b-tooltip.hover
|
||||
v-b-tooltip.bottom
|
||||
@click="$emit('onReport')"
|
||||
>
|
||||
<span class="fa fa-edit" />
|
||||
@@ -34,7 +32,6 @@
|
||||
variant="link"
|
||||
aria-label="Workflow Options"
|
||||
v-b-tooltip.hover
|
||||
v-b-tooltip.bottom
|
||||
>
|
||||
<template v-slot:button-content>
|
||||
<span class="fa fa-cog" />
|
||||
@@ -51,26 +48,12 @@
|
||||
variant="link"
|
||||
aria-label="Run Workflow"
|
||||
v-b-tooltip.hover
|
||||
v-b-tooltip.bottom
|
||||
@click="$emit('onRun')"
|
||||
>
|
||||
<span class="fa fa-play" />
|
||||
</b-button>
|
||||
</span>
|
||||
<span v-else>
|
||||
<b-button
|
||||
id="workflow-report-help-button"
|
||||
role="button"
|
||||
title="Report Syntax Help"
|
||||
sm="size"
|
||||
variant="link"
|
||||
aria-label="Report Syntax Help"
|
||||
v-b-tooltip.hover
|
||||
v-b-tooltip.bottom
|
||||
@click="$emit('onReportHelp')"
|
||||
>
|
||||
<span class="fa fa-question" />
|
||||
</b-button>
|
||||
<b-button
|
||||
id="workflow-canvas-button"
|
||||
role="button"
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<h3>Workflow Reports</h3>
|
||||
<p>
|
||||
The <b>Markdown</b> document you see will be used to generate a workflow invocation report. You can use
|
||||
Markdown with embedded commands for extracting and displaying parts of the workflow, its invocation
|
||||
metadata, its inputs and outputs, etc..
|
||||
</p>
|
||||
<div v-html="markdownGeneralHelpHtml" />
|
||||
<h3>Workflow Commands</h3>
|
||||
<p>
|
||||
These commands do not take any arguments and do reference the whole workflow. For instance, the following
|
||||
example would display a representation of the workflow in the resulting report:
|
||||
</p>
|
||||
<pre>
|
||||
\`\`\`galaxy
|
||||
workflow_display()
|
||||
\`\`\`
|
||||
</pre
|
||||
>
|
||||
<dl>
|
||||
<dt><tt>workflow_display</tt></dt>
|
||||
<dd>Embed a description of the workflow itself in the resulting report.</dd>
|
||||
<dt><tt>invocation_inputs</tt></dt>
|
||||
<dd>Embed the labeled workflow inputs in the resulting report.</dd>
|
||||
<dt><tt>invocation_outputs</tt></dt>
|
||||
<dd>Embed the labeled workflow outputs in the resulting report.</dd>
|
||||
</dl>
|
||||
<h3>Step Commands</h3>
|
||||
<p>
|
||||
These commands reference a workflow step label and refer to job corresponding to that step. A current
|
||||
limitation is the report will break if these refer to a collection mapping step, these must identify a
|
||||
single job. For instance, the following example would show the job parameters the step labeled 'qc' was run
|
||||
with:
|
||||
</p>
|
||||
<pre>
|
||||
\`\`\`galaxy
|
||||
job_parameters(step="qc")
|
||||
\`\`\`
|
||||
</pre
|
||||
>
|
||||
<dt><tt>tool_stderr</tt></dt>
|
||||
<dd>Embed the tool standard error stream for this step in the resulting report.</dd>
|
||||
<dt><tt>tool_stdout</tt></dt>
|
||||
<dd>Embed the tool standard output stream for this step in the resulting report.</dd>
|
||||
<dt><tt>job_metrics</tt></dt>
|
||||
<dd>Embed the job metrics for this step in the resulting report (if user has permission).</dd>
|
||||
<dt><tt>job_parameters</tt></dt>
|
||||
<dd>Embed the tool parameters for this step in the resulting report.</dd>
|
||||
<h3>Input/Output Commands</h3>
|
||||
<p>
|
||||
These commands reference a workflow input or output by label. For instance, the following example would
|
||||
display the dataset collection corresponding to output "Merged BAM":
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
\`\`\`galaxy
|
||||
history_dataset_collection_display(output="Merged Bam")
|
||||
\`\`\`
|
||||
</pre
|
||||
>
|
||||
<div v-html="datasetCommandsHtml" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { datasetCommandsHtml, markdownGeneralHelpHtml } from "components/Markdown/help";
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
datasetCommandsHtml,
|
||||
markdownGeneralHelpHtml
|
||||
};
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -1,106 +0,0 @@
|
||||
<template>
|
||||
<div class="unified-panel">
|
||||
<div class="unified-panel-header" unselectable="on">
|
||||
<div class="unified-panel-header-inner">
|
||||
<div class="panel-header-text">Details</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="unified-panel-body workflow-right">
|
||||
<b-alert class="m-2" :variant="messageVariant" :show="!!message">
|
||||
{{ message }}
|
||||
</b-alert>
|
||||
<div id="edit-attributes" class="right-content p-2">
|
||||
<div id="workflow-name-area">
|
||||
<b>Name</b>
|
||||
<b-input id="workflow-name" :value="name" @change="onRename" />
|
||||
</div>
|
||||
<div id="workflow-version-area" class="mt-2">
|
||||
<b>Version</b>
|
||||
<div class="ui-select">
|
||||
<select class="ui-input" id="workflow-version-switch">
|
||||
Select version
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div id="workflow-annotation-area" class="mt-2">
|
||||
<b>Annotation</b>
|
||||
<b-textarea id="workflow-annotation" :value="annotation" @change="onAnnotation" />
|
||||
<div class="form-text text-muted">
|
||||
These notes will be visible when this workflow is viewed.
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<b>Tags</b>
|
||||
<Tags :tags="tags" @input="onTags" />
|
||||
<div class="form-text text-muted">
|
||||
Apply tags to make it easy to search for and find items with the same tag.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="right-content" class="right-content" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Tags from "components/Common/Tags";
|
||||
import { Services } from "components/Workflow/services";
|
||||
export default {
|
||||
name: "EditorPanel",
|
||||
components: {
|
||||
Tags
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
message: null,
|
||||
messageVariant: null
|
||||
};
|
||||
},
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
tags: {
|
||||
type: Array,
|
||||
required: true
|
||||
},
|
||||
annotation: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
created() {
|
||||
this.services = new Services();
|
||||
},
|
||||
methods: {
|
||||
onTags(tags) {
|
||||
this.tags = tags;
|
||||
this.services
|
||||
.updateWorkflow(this.id, {
|
||||
tags: this.tags
|
||||
})
|
||||
.catch(error => {
|
||||
this.onError(error);
|
||||
});
|
||||
},
|
||||
onAnnotation(annotation) {
|
||||
this.services.updateWorkflow(this.id, { annotation }).catch(error => {
|
||||
this.onError(error);
|
||||
});
|
||||
},
|
||||
onRename(name) {
|
||||
this.services.updateWorkflow(this.id, { name }).catch(error => {
|
||||
this.onError(error);
|
||||
});
|
||||
},
|
||||
onError: function(message) {
|
||||
this.message = message;
|
||||
this.messageVariant = "danger";
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,80 @@
|
||||
<template>
|
||||
<span class="zoom-control float-right btn-group-horizontal">
|
||||
<b-button
|
||||
role="button"
|
||||
class="fa fa-minus"
|
||||
title="Zoom Out"
|
||||
size="sm"
|
||||
aria-label="Zoom Out"
|
||||
v-b-tooltip.hover
|
||||
@click="onZoomOut"
|
||||
/>
|
||||
<b-button
|
||||
role="button"
|
||||
class="zoom-reset"
|
||||
variant="light"
|
||||
title="Reset Zoom Level"
|
||||
size="sm"
|
||||
aria-label="Reset Zoom Level"
|
||||
v-b-tooltip.hover
|
||||
@click="onZoomReset"
|
||||
>
|
||||
{{ this.zoomPercentage }}%
|
||||
</b-button>
|
||||
<b-button
|
||||
role="button"
|
||||
class="fa fa-plus"
|
||||
title="Zoom In"
|
||||
size="sm"
|
||||
aria-label="Zoom In"
|
||||
v-b-tooltip.hover
|
||||
@click="onZoomIn"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { zoomLevels } from "mvc/workflow/workflow-canvas";
|
||||
export default {
|
||||
props: {
|
||||
zoomLevel: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
zoomDefault: this.zoomLevel
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
zoomPercentage() {
|
||||
return Math.floor(zoomLevels[this.zoomLevel] * 100);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
onZoomIn() {
|
||||
this.$emit("onZoom", this.zoomLevel + 1);
|
||||
},
|
||||
onZoomOut() {
|
||||
this.$emit("onZoom", this.zoomLevel - 1);
|
||||
},
|
||||
onZoomReset() {
|
||||
this.$emit("onZoom", this.zoomDefault);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.zoom-reset {
|
||||
width: 4rem;
|
||||
}
|
||||
.zoom-control {
|
||||
position: absolute;
|
||||
left: 1rem;
|
||||
bottom: 1rem;
|
||||
cursor: pointer;
|
||||
z-index: 1002;
|
||||
}
|
||||
</style>
|
||||
@@ -2,26 +2,14 @@
|
||||
* Endpoint for mounting WorkflowEditor from non-Vue environment (editor.mako).
|
||||
*/
|
||||
import Vue from "vue";
|
||||
import SidePanel from "components/Panels/SidePanel";
|
||||
import Index from "./Index";
|
||||
import Node from "./Node";
|
||||
import WorkflowPanel from "./WorkflowPanel";
|
||||
|
||||
export const mountWorkflowEditor = editorConfig => {
|
||||
const propsData = { editorConfig };
|
||||
export const mountWorkflowEditor = propsData => {
|
||||
const component = Vue.extend(Index);
|
||||
return new component({ propsData: propsData, el: "#center" });
|
||||
};
|
||||
|
||||
export const mountWorkflowPanel = propsData => {
|
||||
const component = Vue.extend(SidePanel);
|
||||
return new component({
|
||||
propsData: {
|
||||
side: "right",
|
||||
currentPanel: WorkflowPanel,
|
||||
currentPanelProperties: propsData
|
||||
},
|
||||
el: "#right"
|
||||
propsData: propsData,
|
||||
el: "#columns"
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
export default `
|
||||
# Workflow Execution Report
|
||||
|
||||
## Workflow Inputs
|
||||
\`\`\`galaxy
|
||||
invocation_inputs()
|
||||
\`\`\`
|
||||
|
||||
## Workflow Outputs
|
||||
\`\`\`galaxy
|
||||
invocation_outputs()
|
||||
\`\`\`
|
||||
|
||||
## Workflow
|
||||
\`\`\`galaxy
|
||||
workflow_display()
|
||||
\`\`\`
|
||||
`;
|
||||
@@ -1,88 +0,0 @@
|
||||
import { datasetCommandsHtml, markdownGeneralHelpHtml } from "components/Markdown/help";
|
||||
|
||||
const reportHelp = `
|
||||
<div>
|
||||
<h3>Overview</h3>
|
||||
|
||||
<p>
|
||||
This document Markdown document will be used to generate a report
|
||||
for invocations of this workflow. This document should be Markdown
|
||||
with embedded command for extracting and displaying parts of the workflow,
|
||||
its invocation metadata, its inputs and outputs, etc..
|
||||
</p>
|
||||
|
||||
${markdownGeneralHelpHtml}
|
||||
|
||||
<h3>Workflow Commands</h3>
|
||||
|
||||
<p>
|
||||
These commands to do not take any arguments and reference the whole workflow. For
|
||||
instance, the following example would display a representation of the workflow in the
|
||||
resulting report:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
\`\`\`galaxy
|
||||
workflow_display()
|
||||
\`\`\`
|
||||
</pre>
|
||||
<dl>
|
||||
<dt><tt>workflow_display</tt></dt>
|
||||
<dd>Embed a description of the workflow itself in the resulting report.</dd>
|
||||
<dt><tt>invocation_inputs</tt></dt>
|
||||
<dd>Embed the labeled workflow inputs in the resulting report.</dd>
|
||||
<dt><tt>invocation_outputs</tt></dt>
|
||||
<dd>Embed the labeled workflow outputs in the resulting report.</dd>
|
||||
</dl>
|
||||
|
||||
<h3>Step Commands</h3>
|
||||
|
||||
<p>
|
||||
These commands reference a workflow step label and refer to job corresponding
|
||||
to that step. A current limitation is the report will break if these refer to
|
||||
a collection mapping step, these must identify a single job. For instance, the
|
||||
following example would show the job parameters the step labeled 'qc' was run
|
||||
with:
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
\`\`\`galaxy
|
||||
job_parameters(step="qc")
|
||||
\`\`\`
|
||||
</pre>
|
||||
|
||||
|
||||
<dt><tt>tool_stderr</tt></dt>
|
||||
<dd>Embed the tool standard error stream for this step in the resulting report.</dd>
|
||||
<dt><tt>tool_stdout</tt></dt>
|
||||
<dd>Embed the tool standard output stream for this step in the resulting report.</dd>
|
||||
<dt><tt>job_metrics</tt></dt>
|
||||
<dd>Embed the job metrics for this step in the resulting report (if user has permission).</dd>
|
||||
<dt><tt>job_parameters</tt></dt>
|
||||
<dd>Embed the tool parameters for this step in the resulting report.</dd>
|
||||
|
||||
<h3>Input/Output Commands</h3>
|
||||
|
||||
<p>
|
||||
These commands reference a workflow input or output by label. For instance, the
|
||||
following example would display the dataset collection corresponding to output "Merged BAM":
|
||||
</p>
|
||||
|
||||
<pre>
|
||||
\`\`\`galaxy
|
||||
history_dataset_collection_display(output="Merged Bam")
|
||||
\`\`\`
|
||||
</pre>
|
||||
|
||||
${datasetCommandsHtml}
|
||||
`;
|
||||
|
||||
import $ from "jquery";
|
||||
import { hide_modal, show_modal } from "layout/modal";
|
||||
|
||||
export function showReportHelp() {
|
||||
const reportHelpBody = $(reportHelp);
|
||||
show_modal("Workflow Invocation Report Help", reportHelpBody, {
|
||||
Ok: hide_modal
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import axios from "axios";
|
||||
import { rethrowSimple } from "utils/simple-error";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
|
||||
/** Workflow data request helper **/
|
||||
export async function getVersions(id) {
|
||||
try {
|
||||
const response = await axios.get(`${getAppRoot()}api/workflows/${id}/versions`);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
rethrowSimple(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getDatatypes() {
|
||||
try {
|
||||
const datatypesRequest = await axios.get(`${getAppRoot()}api/datatypes`);
|
||||
const datatypes = datatypesRequest.data;
|
||||
const mappingRequest = await axios.get(`${getAppRoot()}api/datatypes/mapping`);
|
||||
const datatypes_mapping = mappingRequest.data;
|
||||
return { datatypes, datatypes_mapping };
|
||||
} catch (e) {
|
||||
rethrowSimple(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function getModule(request_data) {
|
||||
try {
|
||||
const response = await axios.post(`${getAppRoot()}api/workflows/build_module`, request_data);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
rethrowSimple(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadWorkflow(workflow, id, version) {
|
||||
try {
|
||||
const versionQuery = version ? `version=${version}` : "";
|
||||
const { data } = await axios.get(`${getAppRoot()}workflow/load_workflow?_=true&id=${id}&${versionQuery}`);
|
||||
workflow.remove_all();
|
||||
workflow.from_simple(data, true);
|
||||
workflow.has_changes = false;
|
||||
workflow.fit_canvas_to_nodes();
|
||||
workflow.scroll_to_nodes();
|
||||
workflow.canvas_manager.draw_overview();
|
||||
return data;
|
||||
} catch (e) {
|
||||
rethrowSimple(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveWorkflow(workflow, id) {
|
||||
workflow.rectify_workflow_outputs();
|
||||
if (workflow.has_changes) {
|
||||
try {
|
||||
const requestData = { workflow: workflow.to_simple(), from_tool_form: true };
|
||||
const { data } = await axios.put(`${getAppRoot()}api/workflows/${id}`, requestData);
|
||||
workflow.name = data.name;
|
||||
workflow.has_changes = false;
|
||||
workflow.stored = true;
|
||||
workflow.workflow_version = data.version;
|
||||
return data;
|
||||
} catch (e) {
|
||||
rethrowSimple(e);
|
||||
}
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
import _ from "underscore";
|
||||
import $ from "jquery";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import _l from "utils/localization";
|
||||
import Utils from "utils/utils";
|
||||
import WorkflowIcons from "components/Workflow/icons";
|
||||
import { DefaultForm, ToolForm } from "mvc/workflow/workflow-forms";
|
||||
import { loadWorkflow } from "./services";
|
||||
import { hide_modal, show_message, show_modal } from "layout/modal";
|
||||
|
||||
export function copyIntoWorkflow(workflow, id = null, stepCount = null) {
|
||||
const _copy_into_workflow_ajax = () => {
|
||||
// Load workflow definition
|
||||
show_message("Importing workflow", "progress");
|
||||
loadWorkflow(workflow, id, null).then(data => {
|
||||
workflow.from_simple(data, false);
|
||||
// Determine if any parameters were 'upgraded' and provide message
|
||||
var upgrade_message = "";
|
||||
$.each(data.upgrade_messages, (k, v) => {
|
||||
upgrade_message += `<li>Step ${parseInt(k, 10) + 1}: ${workflow.nodes[k].name}<ul>`;
|
||||
$.each(v, (i, vv) => {
|
||||
upgrade_message += `<li>${vv}</li>`;
|
||||
});
|
||||
upgrade_message += "</ul></li>";
|
||||
});
|
||||
if (upgrade_message) {
|
||||
show_modal(
|
||||
"Subworkflow embedded with changes",
|
||||
`Problems were encountered loading this workflow (possibly a result of tool upgrades). Please review the following parameters and then save.<ul>${upgrade_message}</ul>`,
|
||||
{ Continue: hide_modal }
|
||||
);
|
||||
} else {
|
||||
hide_modal();
|
||||
}
|
||||
});
|
||||
};
|
||||
if (stepCount < 2) {
|
||||
_copy_into_workflow_ajax();
|
||||
} else {
|
||||
// don't ruin the workflow by adding 50 steps unprompted.
|
||||
show_modal(_l("Warning"), `This will copy ${stepCount} new steps into your workflow.`, {
|
||||
Cancel: hide_modal,
|
||||
Copy: _copy_into_workflow_ajax
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function showWarnings(data) {
|
||||
const body = $("<div/>").text(data.message);
|
||||
if (data.errors) {
|
||||
body.addClass("warningmark");
|
||||
var errlist = $("<ul/>");
|
||||
$.each(data.errors, (i, v) => {
|
||||
$("<li/>")
|
||||
.text(v)
|
||||
.appendTo(errlist);
|
||||
});
|
||||
body.append(errlist);
|
||||
} else {
|
||||
body.addClass("donemark");
|
||||
}
|
||||
if (data.errors) {
|
||||
show_modal("Saving workflow", body, {
|
||||
Ok: hide_modal
|
||||
});
|
||||
} else {
|
||||
hide_modal();
|
||||
}
|
||||
}
|
||||
|
||||
export function showAttributes() {
|
||||
$(".right-content").hide();
|
||||
$("#edit-attributes").show();
|
||||
}
|
||||
|
||||
export function showForm(workflow, node, datatypes) {
|
||||
if (node && node.config_form) {
|
||||
const content = node.config_form;
|
||||
const cls = "right-content";
|
||||
var id = `${cls}-${node.id}`;
|
||||
var $container = $(`#${cls}`);
|
||||
if ($container.find(`#${id}`).length === 0) {
|
||||
var $el = $(`<div id="${id}" class="${cls}"/>`);
|
||||
content.node = node;
|
||||
content.workflow = workflow;
|
||||
content.datatypes = datatypes;
|
||||
content.icon = WorkflowIcons[node.type];
|
||||
content.cls = "ui-portlet-section";
|
||||
let formWrapper = null;
|
||||
if (node.type == "tool") {
|
||||
formWrapper = new ToolForm(content);
|
||||
} else {
|
||||
formWrapper = new DefaultForm(content);
|
||||
}
|
||||
$el.append(formWrapper.form.$el);
|
||||
$container.append($el);
|
||||
}
|
||||
$(`.${cls}`).hide();
|
||||
$container.find(`#${id}`).show();
|
||||
$container.show();
|
||||
$container.scrollTop();
|
||||
}
|
||||
}
|
||||
|
||||
export function showUpgradeMessage(workflow, data) {
|
||||
// Determine if any parameters were 'upgraded' and provide message
|
||||
var upgrade_message = "";
|
||||
_.each(data.steps, (step, step_id) => {
|
||||
var details = "";
|
||||
if (step.errors) {
|
||||
details += `<li>${step.errors}</li>`;
|
||||
}
|
||||
_.each(data.upgrade_messages[step_id], m => {
|
||||
details += `<li>${m}</li>`;
|
||||
});
|
||||
if (details) {
|
||||
upgrade_message += `<li>Step ${parseInt(step_id, 10) + 1}: ${
|
||||
workflow.nodes[step_id].name
|
||||
}<ul>${details}</ul></li>`;
|
||||
}
|
||||
});
|
||||
if (upgrade_message) {
|
||||
show_modal(
|
||||
"Issues loading this workflow",
|
||||
`Please review the following issues, possibly resulting from tool upgrades or changes.<p><ul>${upgrade_message}</ul></p>`,
|
||||
{ Continue: hide_modal }
|
||||
);
|
||||
} else {
|
||||
hide_modal();
|
||||
}
|
||||
}
|
||||
|
||||
export function getWorkflowParameters(nodes) {
|
||||
const parameter_re = /\$\{.+?\}/g;
|
||||
const parameters = [];
|
||||
let matches = [];
|
||||
Object.entries(nodes).forEach(([k, node]) => {
|
||||
if (node.config_form && node.config_form.inputs) {
|
||||
Utils.deepeach(node.config_form.inputs, d => {
|
||||
if (typeof d.value == "string") {
|
||||
var form_matches = d.value.match(parameter_re);
|
||||
if (form_matches) {
|
||||
matches = matches.concat(form_matches);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (node.post_job_actions) {
|
||||
Object.entries(node.post_job_actions).forEach(([k, pja]) => {
|
||||
if (pja.action_arguments) {
|
||||
Object.entries(pja.action_arguments).forEach(([k, action_argument]) => {
|
||||
if (typeof action_argument === "string") {
|
||||
const arg_matches = action_argument.match(parameter_re);
|
||||
if (arg_matches) {
|
||||
matches = matches.concat(arg_matches);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (matches) {
|
||||
Object.entries(matches).forEach(([k, element]) => {
|
||||
if (parameters.indexOf(element) === -1) {
|
||||
parameters.push(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
Object.entries(parameters).forEach(([k, element]) => {
|
||||
parameters[k] = element.substring(2, element.length - 1);
|
||||
});
|
||||
return parameters;
|
||||
}
|
||||
|
||||
export function saveAs(workflow) {
|
||||
var body = $(
|
||||
'<form><label style="display:inline-block; width: 100%;">Save as name: </label><input type="text" id="workflow_rename" style="width: 80%;" autofocus/>' +
|
||||
'<br><label style="display:inline-block; width: 100%;">Annotation: </label><input type="text" id="wf_annotation" style="width: 80%;" /></form>'
|
||||
);
|
||||
show_modal("Save As a New Workflow", body, {
|
||||
OK: function() {
|
||||
var rename_name =
|
||||
$("#workflow_rename").val().length > 0 ? $("#workflow_rename").val() : `SavedAs_${workflow.name}`;
|
||||
var rename_annotation = $("#wf_annotation").val().length > 0 ? $("#wf_annotation").val() : "";
|
||||
$.ajax({
|
||||
url: `${getAppRoot()}workflow/save_workflow_as`,
|
||||
type: "POST",
|
||||
data: {
|
||||
workflow_name: rename_name,
|
||||
workflow_annotation: rename_annotation,
|
||||
from_tool_form: true,
|
||||
workflow_data: function() {
|
||||
return JSON.stringify(workflow.to_simple());
|
||||
}
|
||||
}
|
||||
})
|
||||
.done(id => {
|
||||
window.onbeforeunload = undefined;
|
||||
window.location = `${getAppRoot()}workflow/editor?id=${id}`;
|
||||
hide_modal();
|
||||
})
|
||||
.fail(() => {
|
||||
hide_modal();
|
||||
alert("Saving this workflow failed. Please contact this site's administrator.");
|
||||
});
|
||||
},
|
||||
Cancel: hide_modal
|
||||
});
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import Form from "mvc/form/form-view";
|
||||
import FormData from "mvc/form/form-data";
|
||||
import ToolFormBase from "mvc/tool/tool-form-base";
|
||||
import Modal from "mvc/ui/ui-modal";
|
||||
import WorkflowIcons from "mvc/workflow/workflow-icons";
|
||||
import WorkflowIcons from "components/Workflow/icons";
|
||||
|
||||
var View = Backbone.View.extend({
|
||||
initialize: function(options) {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import $ from "jquery";
|
||||
|
||||
// Colors used to render nodes in the workflow overview
|
||||
const NODE_COLOR = "#25537b";
|
||||
const NODE_ERROR_COLOR = "#e31a1e";
|
||||
|
||||
// Zoom levels to use for zooming the workflow canvas
|
||||
export const zoomLevels = [0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.33, 1.5, 2, 2.5, 3, 4];
|
||||
|
||||
// Default zoome level
|
||||
export const defaultZoomLevel = 7;
|
||||
|
||||
// FIXME: merge scroll panel into CanvasManager, clean up hardcoded stuff.
|
||||
class ScrollPanel {
|
||||
constructor(panel) {
|
||||
@@ -73,12 +83,6 @@ class ScrollPanel {
|
||||
}
|
||||
}
|
||||
|
||||
// Zoom levels to use for zooming the workflow canvas
|
||||
const zoomLevels = [0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.33, 1.5, 2, 2.5, 3, 4];
|
||||
|
||||
// Default zoome level (1)
|
||||
const defaultZoomLevel = 7;
|
||||
|
||||
class CanvasManager {
|
||||
constructor(app, canvas_viewport, overview) {
|
||||
this.app = app;
|
||||
@@ -90,7 +94,6 @@ class CanvasManager {
|
||||
// Initialize zooming
|
||||
this.zoomLevel = defaultZoomLevel;
|
||||
this.canvasZoom = zoomLevels[defaultZoomLevel];
|
||||
this.initZoomControls();
|
||||
// Make overview box draggable
|
||||
this.init_drag();
|
||||
// Initialize Copy & Paste events
|
||||
@@ -106,40 +109,8 @@ class CanvasManager {
|
||||
this.cv.css("width", `${100 / this.canvasZoom}%`);
|
||||
this.cv.css("height", `${100 / this.canvasZoom}%`);
|
||||
// Update canvas size
|
||||
this.app.workflow.fit_canvas_to_nodes();
|
||||
}
|
||||
initZoomControls() {
|
||||
var zoomControl = $('<div class="btn-group-horizontal workflow-canvas-content"/>').css({
|
||||
position: "absolute",
|
||||
left: "1rem",
|
||||
bottom: "1rem",
|
||||
cursor: "pointer"
|
||||
});
|
||||
const zoomButton = $(
|
||||
`<a class="btn btn-light" title="Reset Zoom Level">${zoomLevels[defaultZoomLevel] * 100}%</a>`
|
||||
).css({
|
||||
width: "4rem"
|
||||
});
|
||||
|
||||
zoomControl.append(
|
||||
$('<a class="btn btn-secondary fa fa-minus"/>').click(() => {
|
||||
this.setZoom(this.zoomLevel - 1);
|
||||
zoomButton.text(Math.floor(zoomLevels[this.zoomLevel] * 100) + "%");
|
||||
})
|
||||
);
|
||||
zoomControl.append(
|
||||
zoomButton.click(() => {
|
||||
this.setZoom(defaultZoomLevel);
|
||||
zoomButton.text(Math.floor(zoomLevels[this.zoomLevel] * 100) + "%");
|
||||
})
|
||||
);
|
||||
zoomControl.append(
|
||||
$('<a class="btn btn-secondary fa fa-plus"/>').click(() => {
|
||||
this.setZoom(this.zoomLevel + 1);
|
||||
zoomButton.text(Math.floor(zoomLevels[this.zoomLevel] * 100) + "%");
|
||||
})
|
||||
);
|
||||
this.cv.closest("#workflow-canvas-body").append(zoomControl);
|
||||
this.app.fit_canvas_to_nodes();
|
||||
return this.zoomLevel;
|
||||
}
|
||||
|
||||
init_drag() {
|
||||
@@ -179,7 +150,7 @@ class CanvasManager {
|
||||
move((d.offsetX + x_adjust) / this.canvasZoom, (d.offsetY + y_adjust) / this.canvasZoom);
|
||||
})
|
||||
.bind("dragend", () => {
|
||||
self.app.workflow.fit_canvas_to_nodes();
|
||||
self.app.fit_canvas_to_nodes();
|
||||
self.draw_overview();
|
||||
});
|
||||
this.overview.click(e => {
|
||||
@@ -193,7 +164,7 @@ class CanvasManager {
|
||||
var new_x_offset = e.pageX - self.oc.offset().left - self.ov.width() / 2;
|
||||
var new_y_offset = e.pageY - self.oc.offset().top - self.ov.height() / 2;
|
||||
move(-((new_x_offset / o_w) * in_w), -((new_y_offset / o_h) * in_h));
|
||||
self.app.workflow.fit_canvas_to_nodes();
|
||||
self.app.fit_canvas_to_nodes();
|
||||
self.draw_overview();
|
||||
}
|
||||
});
|
||||
@@ -210,7 +181,7 @@ class CanvasManager {
|
||||
})
|
||||
.bind("dragend", () => {
|
||||
self.overview.addClass("blockaclick");
|
||||
self.app.workflow.fit_canvas_to_nodes();
|
||||
self.app.fit_canvas_to_nodes();
|
||||
self.draw_overview();
|
||||
});
|
||||
// Dragging for overview border (resize)
|
||||
@@ -233,11 +204,11 @@ class CanvasManager {
|
||||
// If it appears that the user is trying to copy/paste text, we
|
||||
// pass that through.
|
||||
if (window.getSelection().toString() === "") {
|
||||
if (this.app.workflow.active_node && this.app.workflow.active_node.type !== "subworkflow") {
|
||||
if (this.app.active_node && this.app.active_node.type !== "subworkflow") {
|
||||
e.clipboardData.setData(
|
||||
"application/json",
|
||||
JSON.stringify({
|
||||
nodeId: this.app.workflow.active_node.id
|
||||
nodeId: this.app.active_node.id
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -259,8 +230,8 @@ class CanvasManager {
|
||||
} catch (error) {
|
||||
console.debug(error);
|
||||
}
|
||||
if (nodeId && Object.prototype.hasOwnProperty.call(this.app.workflow.nodes, nodeId)) {
|
||||
this.app.workflow.nodes[nodeId].clone();
|
||||
if (nodeId && Object.prototype.hasOwnProperty.call(this.app.nodes, nodeId)) {
|
||||
this.app.nodes[nodeId].clone();
|
||||
}
|
||||
e.preventDefault();
|
||||
}
|
||||
@@ -329,8 +300,8 @@ class CanvasManager {
|
||||
canvas_el.attr("width", o_w);
|
||||
canvas_el.attr("height", o_h);
|
||||
// Draw overview
|
||||
$.each(this.app.workflow.nodes, (id, node) => {
|
||||
c.fillStyle = "gray";
|
||||
$.each(this.app.nodes, (id, node) => {
|
||||
c.fillStyle = NODE_COLOR;
|
||||
var node_element = $(node.element);
|
||||
var position = node_element.position();
|
||||
var x = (position.left / in_w) * o_w;
|
||||
@@ -338,7 +309,7 @@ class CanvasManager {
|
||||
var w = (node_element.width() / in_w) * o_w;
|
||||
var h = (node_element.height() / in_h) * o_h;
|
||||
if (node.errors) {
|
||||
c.fillStyle = "#e31a1e";
|
||||
c.fillStyle = NODE_ERROR_COLOR;
|
||||
}
|
||||
c.fillRect(x, y, w, h);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,8 @@ import $ from "jquery";
|
||||
|
||||
import { Toast } from "ui/toast";
|
||||
|
||||
function Connector(handle1, handle2) {
|
||||
function Connector(manager, handle1, handle2) {
|
||||
this.manager = manager;
|
||||
this.canvas = null;
|
||||
this.dragging = false;
|
||||
this.inner_color = "#FFFFFF";
|
||||
@@ -46,8 +47,7 @@ $.extend(Connector.prototype, {
|
||||
const endRibbon = handle2 && handle2.isMappedOver();
|
||||
const canvasClass = `${startRibbon ? "start-ribbon" : ""} ${endRibbon ? "end-ribbon" : ""}`;
|
||||
var canvas_container = $("#canvas-container");
|
||||
// FIXME: global
|
||||
var canvasZoom = window.workflow_globals.canvas_manager.canvasZoom;
|
||||
var canvasZoom = this.manager.canvasZoom;
|
||||
if (!this.canvas) {
|
||||
this.canvas = document.createElement("canvas");
|
||||
canvas_container.append($(this.canvas));
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import $ from "jquery";
|
||||
import Backbone from "backbone";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import _l from "utils/localization";
|
||||
@@ -8,8 +7,8 @@ import Form from "mvc/form/form-view";
|
||||
import ToolFormBase from "mvc/tool/tool-form-base";
|
||||
|
||||
/** Default form wrapper for non-tool modules in the workflow editor. */
|
||||
var Default = Backbone.View.extend({
|
||||
initialize: function(options) {
|
||||
export class DefaultForm {
|
||||
constructor(options) {
|
||||
var self = this;
|
||||
var node = options.node;
|
||||
this.form = new Form(
|
||||
@@ -34,11 +33,11 @@ var Default = Backbone.View.extend({
|
||||
_addLabelAnnotation(this.form);
|
||||
this.form.render();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Tool form wrapper for the workflow editor. */
|
||||
var Tool = Backbone.View.extend({
|
||||
initialize: function(options) {
|
||||
export class ToolForm {
|
||||
constructor(options) {
|
||||
var self = this;
|
||||
var node = options.node;
|
||||
this.form = new ToolFormBase(
|
||||
@@ -90,9 +89,8 @@ var Tool = Backbone.View.extend({
|
||||
}
|
||||
})
|
||||
);
|
||||
},
|
||||
|
||||
_customize: function(form) {
|
||||
}
|
||||
_customize(form) {
|
||||
var options = form.model.attributes;
|
||||
Utils.deepeach(options.inputs, input => {
|
||||
if (input.type) {
|
||||
@@ -119,7 +117,7 @@ var Tool = Backbone.View.extend({
|
||||
_addSections(form);
|
||||
_addLabelAnnotation(form);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Augments the module form definition by adding label and annotation fields */
|
||||
function _addLabelAnnotation(form) {
|
||||
@@ -391,8 +389,3 @@ function _addSections(form) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Default: Default,
|
||||
Tool: Tool
|
||||
};
|
||||
|
||||
@@ -1,18 +1,72 @@
|
||||
import $ from "jquery";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import Connector from "mvc/workflow/workflow-connector";
|
||||
import { Toast } from "ui/toast";
|
||||
import { Node } from "mvc/workflow/workflow-node";
|
||||
import { mountWorkflowNode } from "components/Workflow/Editor/mount";
|
||||
import WorkflowCanvas from "mvc/workflow/workflow-canvas";
|
||||
import EventEmitter from "events";
|
||||
|
||||
class Workflow {
|
||||
constructor(app, canvas_container) {
|
||||
this.app = app;
|
||||
this.canvas_container = canvas_container;
|
||||
class Workflow extends EventEmitter {
|
||||
constructor(options, canvas_container) {
|
||||
super();
|
||||
this.ext_to_type = options.datatypes_mapping.ext_to_class_name;
|
||||
this.type_to_type = options.datatypes_mapping.class_to_classes;
|
||||
this.canvas_container = $(canvas_container);
|
||||
this.id_counter = 0;
|
||||
this.nodes = {};
|
||||
this.name = null;
|
||||
this.has_changes = false;
|
||||
this.active_form_has_changes = false;
|
||||
this.workflowOutputLabels = {};
|
||||
this.workflow_version = 0;
|
||||
|
||||
// Canvas overview management
|
||||
this.canvas_manager = new WorkflowCanvas(this, $("#canvas-viewport"), $("#overview-container"));
|
||||
|
||||
// On load, set the size to the pref stored in local storage if it exists
|
||||
var overview_size = localStorage.getItem("overview-size");
|
||||
if (overview_size !== undefined) {
|
||||
$(".workflow-overview").css({
|
||||
width: overview_size,
|
||||
height: overview_size
|
||||
});
|
||||
}
|
||||
|
||||
// Stores the size of the overview into local storage when it's resized
|
||||
$(".workflow-overview").bind("dragend", function(e, d) {
|
||||
var op = $(this).offsetParent();
|
||||
var opo = op.offset();
|
||||
var new_size = Math.max(op.width() - (d.offsetX - opo.left), op.height() - (d.offsetY - opo.top));
|
||||
localStorage.setItem("overview-size", `${new_size}px`);
|
||||
});
|
||||
|
||||
// Unload handler
|
||||
window.onbeforeunload = () => {
|
||||
if (this.has_changes) {
|
||||
return "There are unsaved changes to your workflow which will be lost.";
|
||||
}
|
||||
};
|
||||
}
|
||||
set_node(node, data) {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
node.init_field_data(data);
|
||||
node.update_field_data(data);
|
||||
// Post init/update, for new modules we want to default to
|
||||
// nodes being outputs
|
||||
// TODO: Overhaul the handling of all this when we modernize
|
||||
// the editor, replace callout image manipulation with a simple
|
||||
// class toggle, etc.
|
||||
$.each(node.output_terminals, (ot_id, ot) => {
|
||||
node.addWorkflowOutput(ot.name);
|
||||
var callout = $(node.element).find(`.callout.${ot.name.replace(/(?=[()])/g, "\\")}`);
|
||||
callout.find("img").attr("src", `${Galaxy.root}static/images/fugue/asterisk-small.png`);
|
||||
});
|
||||
this.activate_node(node);
|
||||
}
|
||||
isSubType(child, parent) {
|
||||
child = this.ext_to_type[child];
|
||||
parent = this.ext_to_type[parent];
|
||||
return this.type_to_type[child] && parent in this.type_to_type[child];
|
||||
}
|
||||
canLabelOutputWith(label) {
|
||||
if (label) {
|
||||
@@ -59,10 +113,10 @@ class Workflow {
|
||||
return true;
|
||||
}
|
||||
create_node(type, title_text, content_id) {
|
||||
var node = this.app.prebuildNode(type, title_text, content_id);
|
||||
var node = this.prebuildNode(type, title_text, content_id);
|
||||
this.add_node(node);
|
||||
this.fit_canvas_to_nodes();
|
||||
this.app.canvas_manager.draw_overview();
|
||||
this.canvas_manager.draw_overview();
|
||||
this.activate_node(node);
|
||||
return node;
|
||||
}
|
||||
@@ -75,12 +129,80 @@ class Workflow {
|
||||
this.has_changes = true;
|
||||
node.workflow = this;
|
||||
}
|
||||
prebuildNode(type, title_text, content_id) {
|
||||
var self = this;
|
||||
|
||||
// Create node wrapper
|
||||
const container = document.createElement("div");
|
||||
container.className = "toolForm toolFormInCanvas";
|
||||
document.getElementById("canvas-container").appendChild(container);
|
||||
var $f = $(container);
|
||||
|
||||
// Create backbone model and view
|
||||
var node = new Node(this, { element: $f });
|
||||
node.type = type;
|
||||
node.content_id = content_id;
|
||||
|
||||
// Mount node component as child dom to node wrapper
|
||||
const child = document.createElement("div");
|
||||
container.appendChild(child);
|
||||
mountWorkflowNode(child, {
|
||||
id: content_id,
|
||||
type: type,
|
||||
title: title_text,
|
||||
node: node
|
||||
});
|
||||
|
||||
// Set initial scroll position
|
||||
$f.css("left", $(window).scrollLeft() + 20);
|
||||
$f.css("top", $(window).scrollTop() + 20);
|
||||
|
||||
// Position in container
|
||||
var o = $("#canvas-container").position();
|
||||
var p = $("#canvas-container").parent();
|
||||
var width = $f.outerWidth() + 50;
|
||||
var height = $f.height();
|
||||
$f.css({
|
||||
left: -o.left + p.width() / 2 - width / 2,
|
||||
top: -o.top + p.height() / 2 - height / 2
|
||||
});
|
||||
$f.css("width", width);
|
||||
$f.bind("dragstart", () => {
|
||||
self.activate_node(node);
|
||||
})
|
||||
.bind("dragend", function() {
|
||||
self.node_changed(node);
|
||||
self.fit_canvas_to_nodes();
|
||||
self.canvas_manager.draw_overview();
|
||||
})
|
||||
.bind("dragclickonly", () => {
|
||||
self.activate_node(node);
|
||||
})
|
||||
.bind("drag", function(e, d) {
|
||||
// Move
|
||||
var po = $(this)
|
||||
.offsetParent()
|
||||
.offset();
|
||||
// Find relative offset and scale by zoom
|
||||
var x = (d.offsetX - po.left) / self.canvas_manager.canvasZoom;
|
||||
var y = (d.offsetY - po.top) / self.canvas_manager.canvasZoom;
|
||||
$(this).css({ left: x, top: y });
|
||||
// Redraw
|
||||
$(this)
|
||||
.find(".terminal")
|
||||
.each(function() {
|
||||
this.terminal.redraw();
|
||||
});
|
||||
});
|
||||
return node;
|
||||
}
|
||||
remove_node(node) {
|
||||
if (this.active_node == node) {
|
||||
this.clear_active_node();
|
||||
}
|
||||
delete this.nodes[node.id];
|
||||
this.has_changes = true;
|
||||
this.emit("onRemoveNode");
|
||||
}
|
||||
remove_all() {
|
||||
var wf = this;
|
||||
@@ -220,7 +342,7 @@ class Workflow {
|
||||
wf.workflow_version = data.version;
|
||||
wf.report = data.report || {};
|
||||
$.each(data.steps, (id, step) => {
|
||||
var node = wf.app.prebuildNode(step.type, step.name, step.content_id);
|
||||
var node = wf.prebuildNode(step.type, step.name, step.content_id);
|
||||
// If workflow being copied into another, wipe UUID and let
|
||||
// Galaxy assign new ones.
|
||||
if (!initialImport) {
|
||||
@@ -266,7 +388,7 @@ class Workflow {
|
||||
}
|
||||
$.each(v, (l, x) => {
|
||||
var other_node = wf.nodes[parseInt(x.id) + offset];
|
||||
var c = new Connector();
|
||||
var c = new Connector(this.canvas_manager);
|
||||
c.connect(other_node.output_terminals[x.output_name], node.input_terminals[k]);
|
||||
c.redraw();
|
||||
});
|
||||
@@ -284,17 +406,6 @@ class Workflow {
|
||||
}
|
||||
});
|
||||
}
|
||||
check_changes_in_active_form() {
|
||||
// If active form has changed, save it
|
||||
if (this.active_form_has_changes) {
|
||||
this.has_changes = true;
|
||||
// Submit form.
|
||||
$("#right-content")
|
||||
.find("form")
|
||||
.submit();
|
||||
this.active_form_has_changes = false;
|
||||
}
|
||||
}
|
||||
reload_active_node() {
|
||||
if (this.active_node) {
|
||||
var node = this.active_node;
|
||||
@@ -308,28 +419,44 @@ class Workflow {
|
||||
this.active_node = null;
|
||||
}
|
||||
document.activeElement.blur();
|
||||
this.app.showAttributes();
|
||||
this.emit("onClearActiveNode");
|
||||
}
|
||||
activate_node(node) {
|
||||
if (this.active_node != node) {
|
||||
this.check_changes_in_active_form();
|
||||
this.clear_active_node();
|
||||
this.app.showForm(node.config_form, node);
|
||||
node.make_active();
|
||||
this.active_node = node;
|
||||
}
|
||||
this.emit("onActiveNode", node);
|
||||
}
|
||||
node_changed(node, force) {
|
||||
node_changed(node) {
|
||||
this.has_changes = true;
|
||||
if (this.active_node == node && force) {
|
||||
// Force changes to be saved even on new connection (previously dumped)
|
||||
this.check_changes_in_active_form();
|
||||
this.app.showForm(node.config_form, node);
|
||||
this.emit("onNodeChange", node);
|
||||
}
|
||||
scroll_to_nodes() {
|
||||
var cv = $("#canvas-viewport");
|
||||
var cc = $("#canvas-container");
|
||||
var top;
|
||||
var left;
|
||||
if (cc.width() < cv.width()) {
|
||||
left = (cv.width() - cc.width()) / 2;
|
||||
} else {
|
||||
left = 0;
|
||||
}
|
||||
this.app.showWorkflowParameters();
|
||||
if (cc.height() < cv.height()) {
|
||||
top = (cv.height() - cc.height()) / 2;
|
||||
} else {
|
||||
top = 0;
|
||||
}
|
||||
cc.css({ left: left, top: top });
|
||||
}
|
||||
layout_auto() {
|
||||
this.layout();
|
||||
this.fit_canvas_to_nodes();
|
||||
this.scroll_to_nodes();
|
||||
this.canvas_manager.draw_overview();
|
||||
}
|
||||
layout() {
|
||||
this.check_changes_in_active_form();
|
||||
this.has_changes = true;
|
||||
// Prepare predecessor / successor tracking
|
||||
var n_pred = {};
|
||||
@@ -439,7 +566,7 @@ class Workflow {
|
||||
return 0;
|
||||
}
|
||||
// Span of all elements
|
||||
var canvasZoom = this.app.canvas_manager.canvasZoom;
|
||||
var canvasZoom = this.canvas_manager.canvasZoom;
|
||||
var bounds = this.bounds_for_all_nodes();
|
||||
var position = this.canvas_container.position();
|
||||
var parent = this.canvas_container.parent();
|
||||
|
||||
@@ -54,7 +54,7 @@ export class Node {
|
||||
changed = this.addWorkflowOutput(outputName, label);
|
||||
}
|
||||
if (changed) {
|
||||
this.app.workflow.updateOutputLabel(oldLabel, label);
|
||||
this.app.updateOutputLabel(oldLabel, label);
|
||||
this.markChanged();
|
||||
this.nodeView.redrawWorkflowOutputs();
|
||||
}
|
||||
@@ -163,8 +163,7 @@ export class Node {
|
||||
annotation: this.annotation,
|
||||
post_job_actions: this.post_job_actions
|
||||
};
|
||||
var node = this.app.workflow.create_node(this.type, this.name, this.content_id);
|
||||
|
||||
var node = this.app.create_node(this.type, this.name, this.content_id);
|
||||
Utils.request({
|
||||
type: "POST",
|
||||
url: `${getAppRoot()}api/workflows/build_module`,
|
||||
@@ -177,7 +176,7 @@ export class Node {
|
||||
var newData = Object.assign({}, data, copiedData);
|
||||
node.init_field_data(newData);
|
||||
node.update_field_data(newData);
|
||||
this.app.workflow.activate_node(node);
|
||||
this.app.activate_node(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -188,7 +187,7 @@ export class Node {
|
||||
$.each(this.output_terminals, (k, t) => {
|
||||
t.destroy();
|
||||
});
|
||||
this.app.workflow.remove_node(this);
|
||||
this.app.remove_node(this);
|
||||
$(this.element).remove();
|
||||
}
|
||||
make_active() {
|
||||
@@ -228,7 +227,7 @@ export class Node {
|
||||
this.uuid = data.uuid;
|
||||
this.workflow_outputs = data.workflow_outputs ? data.workflow_outputs : [];
|
||||
var node = this;
|
||||
var nodeView = new NodeView({
|
||||
var nodeView = new NodeView(this.app, {
|
||||
$el: this.element,
|
||||
node: node
|
||||
});
|
||||
@@ -244,7 +243,7 @@ export class Node {
|
||||
nodeView.addDataOutput(output);
|
||||
});
|
||||
nodeView.render();
|
||||
this.app.workflow.node_changed(this, true);
|
||||
this.app.node_changed(this);
|
||||
}
|
||||
update_field_data(data) {
|
||||
var node = this;
|
||||
@@ -347,9 +346,9 @@ export class Node {
|
||||
var tmp = `<div style='color: red; text-style: italic;'>${text}</div>`;
|
||||
this.config_form = tmp;
|
||||
b.html(tmp);
|
||||
this.app.workflow.node_changed(this);
|
||||
this.app.node_changed(this);
|
||||
}
|
||||
markChanged() {
|
||||
this.app.workflow.node_changed(this);
|
||||
this.app.node_changed(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
import $ from "jquery";
|
||||
import _ from "underscore";
|
||||
import Backbone from "backbone";
|
||||
|
||||
// TODO; tie into Galaxy state?
|
||||
window.workflow_globals = window.workflow_globals || {};
|
||||
import EventEmitter from "events";
|
||||
|
||||
function CollectionTypeDescription(collectionType) {
|
||||
this.collectionType = collectionType;
|
||||
@@ -106,35 +103,35 @@ $.extend(CollectionTypeDescription.prototype, {
|
||||
}
|
||||
});
|
||||
|
||||
var TerminalMapping = Backbone.Model.extend({
|
||||
initialize: function(attr) {
|
||||
class TerminalMapping extends EventEmitter {
|
||||
constructor(attr) {
|
||||
super();
|
||||
this.mapOver = attr.mapOver || NULL_COLLECTION_TYPE_DESCRIPTION;
|
||||
this.terminal = attr.terminal;
|
||||
this.terminal.terminalMapping = this;
|
||||
},
|
||||
disableMapOver: function() {
|
||||
this.setMapOver(NULL_COLLECTION_TYPE_DESCRIPTION);
|
||||
},
|
||||
setMapOver: function(collectionTypeDescription) {
|
||||
// TODO: Can I use "attributes" or something to auto trigger "change"
|
||||
// event?
|
||||
this.mapOver = collectionTypeDescription;
|
||||
this.trigger("change");
|
||||
}
|
||||
});
|
||||
disableMapOver() {
|
||||
this.setMapOver(NULL_COLLECTION_TYPE_DESCRIPTION);
|
||||
}
|
||||
setMapOver(collectionTypeDescription) {
|
||||
this.mapOver = collectionTypeDescription;
|
||||
this.emit("change");
|
||||
}
|
||||
}
|
||||
|
||||
var Terminal = Backbone.Model.extend({
|
||||
initialize: function(attr) {
|
||||
class Terminal {
|
||||
constructor(attr) {
|
||||
this.element = attr.element;
|
||||
this.attributes = attr;
|
||||
this.connectors = [];
|
||||
},
|
||||
connect: function(connector) {
|
||||
}
|
||||
connect(connector) {
|
||||
this.connectors.push(connector);
|
||||
if (this.node) {
|
||||
this.node.markChanged();
|
||||
}
|
||||
},
|
||||
disconnect: function(connector) {
|
||||
}
|
||||
disconnect(connector) {
|
||||
this.connectors.splice($.inArray(connector, this.connectors), 1);
|
||||
if (this.node) {
|
||||
this.node.markChanged();
|
||||
@@ -143,25 +140,25 @@ var Terminal = Backbone.Model.extend({
|
||||
connector.handle2.resetCollectionTypeSource();
|
||||
}
|
||||
}
|
||||
},
|
||||
redraw: function() {
|
||||
}
|
||||
redraw() {
|
||||
$.each(this.connectors, (_, c) => {
|
||||
c.redraw();
|
||||
});
|
||||
},
|
||||
destroy: function() {
|
||||
}
|
||||
destroy() {
|
||||
$.each(this.connectors.slice(), (_, c) => {
|
||||
c.destroy();
|
||||
});
|
||||
},
|
||||
destroyInvalidConnections: function() {
|
||||
}
|
||||
destroyInvalidConnections() {
|
||||
_.each(this.connectors, connector => {
|
||||
if (connector) {
|
||||
connector.destroyIfInvalid();
|
||||
}
|
||||
});
|
||||
},
|
||||
setMapOver: function(val) {
|
||||
}
|
||||
setMapOver(val) {
|
||||
let output_val = val;
|
||||
if (this.multiple) {
|
||||
// emulate list input
|
||||
@@ -179,22 +176,21 @@ var Terminal = Backbone.Model.extend({
|
||||
outputTerminal.setMapOver(output_val);
|
||||
});
|
||||
}
|
||||
},
|
||||
mapOver: function() {
|
||||
}
|
||||
mapOver() {
|
||||
if (!this.terminalMapping) {
|
||||
return NULL_COLLECTION_TYPE_DESCRIPTION;
|
||||
} else {
|
||||
return this.terminalMapping.mapOver;
|
||||
}
|
||||
},
|
||||
isMappedOver: function() {
|
||||
}
|
||||
isMappedOver() {
|
||||
return this.terminalMapping && this.terminalMapping.mapOver.isCollection;
|
||||
},
|
||||
resetMapping: function() {
|
||||
}
|
||||
resetMapping() {
|
||||
this.terminalMapping.disableMapOver();
|
||||
},
|
||||
|
||||
resetCollectionTypeSource: function() {
|
||||
}
|
||||
resetCollectionTypeSource() {
|
||||
const node = this.node;
|
||||
_.each(node.output_terminals, function(output_terminal) {
|
||||
const type_source = output_terminal.attributes.collection_type_source;
|
||||
@@ -203,18 +199,18 @@ var Terminal = Backbone.Model.extend({
|
||||
output_terminal.update(output_terminal.attributes);
|
||||
}
|
||||
});
|
||||
},
|
||||
}
|
||||
// Subclasses should override this...
|
||||
resetMappingIfNeeded() {}
|
||||
}
|
||||
|
||||
resetMappingIfNeeded: function() {} // Subclasses should override this...
|
||||
});
|
||||
|
||||
var OutputTerminal = Terminal.extend({
|
||||
initialize: function(attr) {
|
||||
Terminal.prototype.initialize.call(this, attr);
|
||||
class OutputTerminal extends Terminal {
|
||||
constructor(attr) {
|
||||
super(attr);
|
||||
this.datatypes = attr.datatypes;
|
||||
this.optional = attr.optional;
|
||||
this.force_datatype = attr.force_datatype;
|
||||
},
|
||||
}
|
||||
/*
|
||||
update: function(output) {
|
||||
// This isn't needed for datatypes or optional because WorkflowNode.update_field_data just updates the
|
||||
@@ -222,7 +218,7 @@ var OutputTerminal = Terminal.extend({
|
||||
// don't disconnect from non-optional inputs.
|
||||
},
|
||||
*/
|
||||
resetMappingIfNeeded: function() {
|
||||
resetMappingIfNeeded() {
|
||||
// If inputs were only mapped over to preserve
|
||||
// an output just disconnected reset these...
|
||||
if (!this.node.hasConnectedOutputTerminals() && !this.node.hasConnectedMappedInputTerminals()) {
|
||||
@@ -235,9 +231,8 @@ var OutputTerminal = Terminal.extend({
|
||||
if (noMappedInputs) {
|
||||
this.resetMapping();
|
||||
}
|
||||
},
|
||||
|
||||
resetMapping: function() {
|
||||
}
|
||||
resetMapping() {
|
||||
this.terminalMapping.disableMapOver();
|
||||
_.each(this.connectors, connector => {
|
||||
var connectedInput = connector.handle2;
|
||||
@@ -250,14 +245,15 @@ var OutputTerminal = Terminal.extend({
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var BaseInputTerminal = Terminal.extend({
|
||||
initialize: function(attr) {
|
||||
Terminal.prototype.initialize.call(this, attr);
|
||||
class BaseInputTerminal extends Terminal {
|
||||
constructor(attr) {
|
||||
super(attr);
|
||||
this.app = attr.app;
|
||||
this.update(attr.input); // subclasses should implement this...
|
||||
},
|
||||
canAccept: function(other) {
|
||||
}
|
||||
canAccept(other) {
|
||||
if (this._inputFilled()) {
|
||||
return new ConnectionAcceptable(
|
||||
false,
|
||||
@@ -266,8 +262,8 @@ var BaseInputTerminal = Terminal.extend({
|
||||
} else {
|
||||
return this.attachable(other);
|
||||
}
|
||||
},
|
||||
resetMappingIfNeeded: function() {
|
||||
}
|
||||
resetMappingIfNeeded() {
|
||||
var mapOver = this.mapOver();
|
||||
if (!mapOver.isCollection) {
|
||||
return;
|
||||
@@ -279,8 +275,8 @@ var BaseInputTerminal = Terminal.extend({
|
||||
if (reset) {
|
||||
this.resetMapping();
|
||||
}
|
||||
},
|
||||
resetMapping: function() {
|
||||
}
|
||||
resetMapping() {
|
||||
this.terminalMapping.disableMapOver();
|
||||
if (!this.node.hasMappedOverInputTerminals()) {
|
||||
_.each(this.node.output_terminals, terminal => {
|
||||
@@ -289,11 +285,11 @@ var BaseInputTerminal = Terminal.extend({
|
||||
terminal.resetMapping();
|
||||
});
|
||||
}
|
||||
},
|
||||
connected: function() {
|
||||
}
|
||||
connected() {
|
||||
return this.connectors.length !== 0;
|
||||
},
|
||||
_inputFilled: function() {
|
||||
}
|
||||
_inputFilled() {
|
||||
var inputFilled;
|
||||
if (!this.connected()) {
|
||||
inputFilled = false;
|
||||
@@ -307,8 +303,8 @@ var BaseInputTerminal = Terminal.extend({
|
||||
}
|
||||
}
|
||||
return inputFilled;
|
||||
},
|
||||
_collectionAttached: function() {
|
||||
}
|
||||
_collectionAttached() {
|
||||
if (!this.connected()) {
|
||||
return false;
|
||||
} else {
|
||||
@@ -327,8 +323,8 @@ var BaseInputTerminal = Terminal.extend({
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
_mappingConstraints: function() {
|
||||
}
|
||||
_mappingConstraints() {
|
||||
// If this is a connected terminal, return list of collection types
|
||||
// other terminals connected to node are constraining mapping to.
|
||||
if (!this.node) {
|
||||
@@ -349,8 +345,8 @@ var BaseInputTerminal = Terminal.extend({
|
||||
constraints.push(_.first(_.values(this.node.output_terminals)).mapOver());
|
||||
}
|
||||
return constraints;
|
||||
},
|
||||
_producesAcceptableDatatype: function(other) {
|
||||
}
|
||||
_producesAcceptableDatatype(other) {
|
||||
// other is a non-collection output...
|
||||
for (var t in this.datatypes) {
|
||||
var thisDatatype = this.datatypes[t];
|
||||
@@ -366,7 +362,7 @@ var BaseInputTerminal = Terminal.extend({
|
||||
if (
|
||||
other_datatype == "input" ||
|
||||
other_datatype == "_sniff_" ||
|
||||
window.workflow_globals.app.isSubType(cat_outputs[other_datatype_i], thisDatatype)
|
||||
this.app.isSubType(cat_outputs[other_datatype_i], thisDatatype)
|
||||
) {
|
||||
return new ConnectionAcceptable(true, null);
|
||||
}
|
||||
@@ -376,14 +372,14 @@ var BaseInputTerminal = Terminal.extend({
|
||||
false,
|
||||
`Effective output data type(s) [${cat_outputs}] do not appear to match input type(s) [${this.datatypes}].`
|
||||
);
|
||||
},
|
||||
}
|
||||
_producesAcceptableDatatypeAndOptionalness(other) {
|
||||
if (!this.optional && !this.multiple && other.optional) {
|
||||
return new ConnectionAcceptable(false, "Cannot connect an optional output to a non-optional input");
|
||||
}
|
||||
return this._producesAcceptableDatatype(other);
|
||||
},
|
||||
_otherCollectionType: function(other) {
|
||||
}
|
||||
_otherCollectionType(other) {
|
||||
var otherCollectionType = NULL_COLLECTION_TYPE_DESCRIPTION;
|
||||
if (other.isCollection) {
|
||||
otherCollectionType = other.collectionType;
|
||||
@@ -394,17 +390,17 @@ var BaseInputTerminal = Terminal.extend({
|
||||
}
|
||||
return otherCollectionType;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var InputTerminal = BaseInputTerminal.extend({
|
||||
update: function(input) {
|
||||
class InputTerminal extends BaseInputTerminal {
|
||||
update(input) {
|
||||
this.datatypes = input.extensions;
|
||||
this.multiple = input.multiple;
|
||||
this.optional = input.optional;
|
||||
this.collection = false;
|
||||
},
|
||||
connect: function(connector) {
|
||||
BaseInputTerminal.prototype.connect.call(this, connector);
|
||||
}
|
||||
connect(connector) {
|
||||
super.connect(connector);
|
||||
var other_output = connector.handle1;
|
||||
if (!other_output) {
|
||||
return;
|
||||
@@ -413,8 +409,8 @@ var InputTerminal = BaseInputTerminal.extend({
|
||||
if (otherCollectionType.isCollection) {
|
||||
this.setMapOver(otherCollectionType);
|
||||
}
|
||||
},
|
||||
attachable: function(other) {
|
||||
}
|
||||
attachable(other) {
|
||||
var otherCollectionType = this._otherCollectionType(other);
|
||||
var thisMapOver = this.mapOver();
|
||||
if (otherCollectionType.isCollection) {
|
||||
@@ -473,28 +469,25 @@ var InputTerminal = BaseInputTerminal.extend({
|
||||
}
|
||||
return this._producesAcceptableDatatypeAndOptionalness(other);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var InputParameterTerminal = BaseInputTerminal.extend({
|
||||
update: function(input) {
|
||||
class InputParameterTerminal extends BaseInputTerminal {
|
||||
update(input) {
|
||||
this.type = input.type;
|
||||
this.optional = input.optional;
|
||||
},
|
||||
connect: function(connector) {
|
||||
BaseInputTerminal.prototype.connect.call(this, connector);
|
||||
},
|
||||
effectiveType: function(parameterType) {
|
||||
}
|
||||
effectiveType(parameterType) {
|
||||
return parameterType == "select" ? "text" : parameterType;
|
||||
},
|
||||
attachable: function(other) {
|
||||
}
|
||||
attachable(other) {
|
||||
const effectiveThisType = this.effectiveType(this.type);
|
||||
const effectiveOtherType = this.effectiveType(other.attributes.type);
|
||||
return new ConnectionAcceptable(effectiveThisType == effectiveOtherType, "");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var InputCollectionTerminal = BaseInputTerminal.extend({
|
||||
update: function(input) {
|
||||
class InputCollectionTerminal extends BaseInputTerminal {
|
||||
update(input) {
|
||||
this.multiple = false;
|
||||
this.collection = true;
|
||||
this.collection_type = input.collection_type;
|
||||
@@ -509,9 +502,9 @@ var InputCollectionTerminal = BaseInputTerminal.extend({
|
||||
collectionTypes.push(ANY_COLLECTION_TYPE_DESCRIPTION);
|
||||
}
|
||||
this.collectionTypes = collectionTypes;
|
||||
},
|
||||
connect: function(connector) {
|
||||
BaseInputTerminal.prototype.connect.call(this, connector);
|
||||
}
|
||||
connect(connector) {
|
||||
super.connect(connector);
|
||||
var other = connector.handle1;
|
||||
if (!other) {
|
||||
return;
|
||||
@@ -537,8 +530,8 @@ var InputCollectionTerminal = BaseInputTerminal.extend({
|
||||
|
||||
var effectiveMapOver = this._effectiveMapOver(other);
|
||||
this.setMapOver(effectiveMapOver);
|
||||
},
|
||||
_effectiveMapOver: function(other) {
|
||||
}
|
||||
_effectiveMapOver(other) {
|
||||
var collectionTypes = this.collectionTypes;
|
||||
var otherCollectionType = this._otherCollectionType(other);
|
||||
var canMatch = _.some(collectionTypes, collectionType => collectionType.canMatch(otherCollectionType));
|
||||
@@ -555,12 +548,12 @@ var InputCollectionTerminal = BaseInputTerminal.extend({
|
||||
}
|
||||
}
|
||||
return NULL_COLLECTION_TYPE_DESCRIPTION;
|
||||
},
|
||||
_effectiveCollectionTypes: function() {
|
||||
}
|
||||
_effectiveCollectionTypes() {
|
||||
var thisMapOver = this.mapOver();
|
||||
return _.map(this.collectionTypes, t => thisMapOver.append(t));
|
||||
},
|
||||
attachable: function(other) {
|
||||
}
|
||||
attachable(other) {
|
||||
var otherCollectionType = this._otherCollectionType(other);
|
||||
if (otherCollectionType.isCollection) {
|
||||
var effectiveCollectionTypes = this._effectiveCollectionTypes();
|
||||
@@ -615,11 +608,11 @@ var InputCollectionTerminal = BaseInputTerminal.extend({
|
||||
return new ConnectionAcceptable(false, "Cannot attach a data output to a collection input.");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var OutputCollectionTerminal = Terminal.extend({
|
||||
initialize: function(attr) {
|
||||
Terminal.prototype.initialize.call(this, attr);
|
||||
class OutputCollectionTerminal extends Terminal {
|
||||
constructor(attr) {
|
||||
super(attr);
|
||||
this.datatypes = attr.datatypes;
|
||||
this.optional = attr.optional;
|
||||
this.force_datatype = attr.force_datatype;
|
||||
@@ -633,8 +626,8 @@ var OutputCollectionTerminal = Terminal.extend({
|
||||
this.collectionType = ANY_COLLECTION_TYPE_DESCRIPTION;
|
||||
}
|
||||
this.isCollection = true;
|
||||
},
|
||||
update: function(output) {
|
||||
}
|
||||
update(output) {
|
||||
this.optional = output.optional;
|
||||
var newCollectionType;
|
||||
if (output.collection_type) {
|
||||
@@ -657,9 +650,9 @@ var OutputCollectionTerminal = Terminal.extend({
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var OutputParameterTerminal = Terminal.extend({});
|
||||
var OutputParameterTerminal = Terminal;
|
||||
|
||||
export default {
|
||||
InputTerminal: InputTerminal,
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import $ from "jquery";
|
||||
|
||||
// TODO; tie into Galaxy state?
|
||||
window.workflow_globals = window.workflow_globals || {};
|
||||
|
||||
export class DataInputView {
|
||||
constructor(options = {}) {
|
||||
this.input = options.input;
|
||||
@@ -31,7 +28,7 @@ export class DataInputView {
|
||||
}
|
||||
|
||||
export class DataOutputView {
|
||||
constructor(options = {}) {
|
||||
constructor(app, options = {}) {
|
||||
this.$el = $("<div class='form-row dataRow'/>");
|
||||
this.output = options.output;
|
||||
this.terminalElement = options.terminalElement;
|
||||
@@ -46,13 +43,13 @@ export class DataOutputView {
|
||||
this.$el.html(label);
|
||||
this.calloutView = null;
|
||||
if (["tool", "subworkflow"].indexOf(node.type) >= 0) {
|
||||
const calloutView = new OutputCalloutView({
|
||||
const calloutView = new OutputCalloutView(app, {
|
||||
label: label,
|
||||
output: output,
|
||||
node: node
|
||||
});
|
||||
this.calloutView = calloutView;
|
||||
this.$el.append(calloutView.el);
|
||||
this.$el.append(calloutView.$el);
|
||||
}
|
||||
this.$el.css({
|
||||
position: "absolute",
|
||||
@@ -79,7 +76,7 @@ export class DataOutputView {
|
||||
}
|
||||
|
||||
export class ParameterOutputView {
|
||||
constructor(options = {}) {
|
||||
constructor(app, options = {}) {
|
||||
this.$el = $("<div class='form-row dataRow'/>");
|
||||
this.output = options.output;
|
||||
this.terminalElement = options.terminalElement;
|
||||
@@ -90,13 +87,13 @@ export class ParameterOutputView {
|
||||
this.$el.html(label);
|
||||
this.calloutView = null;
|
||||
if (["tool", "subworkflow"].indexOf(node.type) >= 0) {
|
||||
const calloutView = new OutputCalloutView({
|
||||
const calloutView = new OutputCalloutView(app, {
|
||||
label: label,
|
||||
output: output,
|
||||
node: node
|
||||
});
|
||||
this.calloutView = calloutView;
|
||||
this.$el.append(calloutView.el);
|
||||
this.$el.append(calloutView.$el);
|
||||
}
|
||||
this.$el.css({
|
||||
position: "absolute",
|
||||
@@ -123,7 +120,7 @@ export class ParameterOutputView {
|
||||
}
|
||||
|
||||
export class OutputCalloutView {
|
||||
constructor(options = {}) {
|
||||
constructor(app, options = {}) {
|
||||
this.$el = $("<div/>");
|
||||
this.label = options.label;
|
||||
this.node = options.node;
|
||||
@@ -138,13 +135,13 @@ export class OutputCalloutView {
|
||||
const outputName = view.output.name;
|
||||
if (node.isWorkflowOutput(outputName)) {
|
||||
node.removeWorkflowOutput(outputName);
|
||||
view.$("icon").removeClass("mark-terminal-active");
|
||||
view.$el.find("icon").removeClass("mark-terminal-active");
|
||||
} else {
|
||||
node.addWorkflowOutput(outputName);
|
||||
view.$("icon").addClass("mark-terminal-active");
|
||||
view.$el.find("icon").addClass("mark-terminal-active");
|
||||
}
|
||||
window.workflow_globals.workflow.has_changes = true;
|
||||
window.workflow_globals.canvas_manager.draw_overview();
|
||||
app.has_changes = true;
|
||||
app.canvas_manager.draw_overview();
|
||||
})
|
||||
)
|
||||
.tooltip({
|
||||
|
||||
@@ -4,7 +4,8 @@ import TerminalViews from "mvc/workflow/workflow-view-terminals";
|
||||
import { DataInputView, DataOutputView, ParameterOutputView } from "mvc/workflow/workflow-view-data";
|
||||
|
||||
export class NodeView {
|
||||
constructor(options) {
|
||||
constructor(app, options) {
|
||||
this.app = app;
|
||||
this.$el = options.$el;
|
||||
this.node = options.node;
|
||||
this.output_width = Math.max(150, this.$el.width());
|
||||
@@ -22,7 +23,7 @@ export class NodeView {
|
||||
}
|
||||
|
||||
renderToolLabel() {
|
||||
this.$el.find(".nodeTitle").text(this.node.label || this.node.name);
|
||||
this.$el.find(".node-title").text(this.node.label || this.node.name);
|
||||
this.$el.attr("node-label", this.node.label);
|
||||
}
|
||||
|
||||
@@ -62,7 +63,7 @@ export class NodeView {
|
||||
terminalView = null;
|
||||
}
|
||||
if (!terminalView) {
|
||||
terminalView = new terminalViewClass({
|
||||
terminalView = new terminalViewClass(this.app, {
|
||||
node: this.node,
|
||||
input: input
|
||||
});
|
||||
@@ -91,7 +92,7 @@ export class NodeView {
|
||||
} else if (output.parameter) {
|
||||
terminalViewClass = TerminalViews.OutputParameterTerminalView;
|
||||
}
|
||||
return new terminalViewClass({
|
||||
return new terminalViewClass(this.app, {
|
||||
node: this.node,
|
||||
output: output
|
||||
});
|
||||
@@ -99,7 +100,7 @@ export class NodeView {
|
||||
|
||||
outputViewforOutput(output, terminalView) {
|
||||
const outputViewClass = output.parameter ? ParameterOutputView : DataOutputView;
|
||||
return new outputViewClass({
|
||||
return new outputViewClass(this.app, {
|
||||
output: output,
|
||||
terminalElement: terminalView.el,
|
||||
nodeView: this
|
||||
|
||||
@@ -1,86 +1,58 @@
|
||||
import $ from "jquery";
|
||||
import _ from "underscore";
|
||||
import Backbone from "backbone";
|
||||
import Terminals from "mvc/workflow/workflow-terminals";
|
||||
import Connector from "mvc/workflow/workflow-connector";
|
||||
import ariaAlert from "utils/ariaAlert";
|
||||
|
||||
// TODO; tie into Galaxy state?
|
||||
window.workflow_globals = window.workflow_globals || {};
|
||||
var NODEINDEX = 0;
|
||||
|
||||
var TerminalMappingView = Backbone.View.extend({
|
||||
tagName: "div",
|
||||
className: "fa-icon-button fa fa-folder-o",
|
||||
initialize: function(options) {
|
||||
class TerminalMappingView {
|
||||
constructor(options = {}) {
|
||||
this.$el = $("<div class='fa-icon-button fa fa-folder-o' />");
|
||||
var mapText = "Run tool in parallel over collection";
|
||||
this.$el.tooltip({ delay: 500, title: mapText });
|
||||
this.model.bind("change", _.bind(this.render, this));
|
||||
},
|
||||
render: function() {
|
||||
if (this.model.mapOver.isCollection) {
|
||||
this.model = options.model;
|
||||
this.model.on("change", this.render.bind(this));
|
||||
}
|
||||
render() {
|
||||
if (this.model.mapOver && this.model.mapOver.isCollection) {
|
||||
this.$el.show();
|
||||
} else {
|
||||
this.$el.hide();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var InputTerminalMappingView = TerminalMappingView.extend({
|
||||
events: {
|
||||
click: "onClick",
|
||||
mouseenter: "onMouseEnter",
|
||||
mouseleave: "onMouseLeave"
|
||||
},
|
||||
onMouseEnter: function(e) {
|
||||
var model = this.model;
|
||||
if (!model.terminal.connected() && model.mapOver.isCollection) {
|
||||
this.$el.css("color", "red");
|
||||
}
|
||||
},
|
||||
onMouseLeave: function(e) {
|
||||
this.$el.css("color", "black");
|
||||
},
|
||||
onClick: function(e) {
|
||||
var model = this.model;
|
||||
if (!model.terminal.connected() && model.mapOver.isCollection) {
|
||||
// TODO: Consider prompting...
|
||||
model.terminal.resetMapping();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var TerminalView = Backbone.View.extend({
|
||||
setupMappingView: function(terminal) {
|
||||
var terminalMapping = new this.terminalMappingClass({
|
||||
class TerminalView {
|
||||
setupMappingView(terminalMappingClass, terminal) {
|
||||
var terminalMapping = new terminalMappingClass({
|
||||
terminal: terminal
|
||||
});
|
||||
var terminalMappingView = new this.terminalMappingViewClass({
|
||||
var terminalMappingView = new TerminalMappingView({
|
||||
model: terminalMapping
|
||||
});
|
||||
terminalMappingView.render();
|
||||
terminal.terminalMappingView = terminalMappingView;
|
||||
this.terminalMappingView = terminalMappingView;
|
||||
},
|
||||
terminalElements: function() {
|
||||
if (this.terminalMappingView) {
|
||||
return [this.terminalMappingView.el, this.el];
|
||||
} else {
|
||||
return [this.el];
|
||||
}
|
||||
}
|
||||
});
|
||||
terminalElements() {
|
||||
return [this.terminalMappingView.$el, this.el];
|
||||
}
|
||||
}
|
||||
|
||||
var BaseInputTerminalView = TerminalView.extend({
|
||||
className: "terminal input-terminal",
|
||||
initialize: function(options) {
|
||||
class BaseInputTerminalView extends TerminalView {
|
||||
constructor(app, options = {}, terminalMappingClass = null) {
|
||||
super();
|
||||
this.app = app;
|
||||
this.el = document.createElement("div");
|
||||
this.el.className = "terminal input-terminal";
|
||||
this.$el = $(this.el);
|
||||
const node = options.node;
|
||||
const input = options.input;
|
||||
const name = input.name;
|
||||
node.cid = NODEINDEX++;
|
||||
const id = `node-${node.cid}-input-${name}`;
|
||||
const terminal = this.terminalForInput(input);
|
||||
this.setupMappingView(terminal);
|
||||
this.setupMappingView(terminalMappingClass, terminal);
|
||||
this.el.terminal = terminal;
|
||||
this.$el.attr("input-name", name);
|
||||
this.$el.attr("id", id);
|
||||
@@ -91,15 +63,15 @@ var BaseInputTerminalView = TerminalView.extend({
|
||||
terminal.name = name;
|
||||
terminal.label = input.label;
|
||||
node.input_terminals[name] = terminal;
|
||||
},
|
||||
events: {
|
||||
dropinit: "onDropInit",
|
||||
dropstart: "onDropStart",
|
||||
dropend: "onDropEnd",
|
||||
drop: "onDrop",
|
||||
hover: "onHover"
|
||||
},
|
||||
onDropInit: function(e, d) {
|
||||
|
||||
const self = this;
|
||||
this.$el.on("dropinit", (e, d) => self.onDropInit(e, d));
|
||||
this.$el.on("dropstart", (e, d) => self.onDropStart(e, d));
|
||||
this.$el.on("dropend", (e, d) => self.onDropEnd(e, d));
|
||||
this.$el.on("drop", (e, d) => self.onDrop(e, d));
|
||||
this.$el.on("hover", () => self.onHover());
|
||||
}
|
||||
onDropInit(e, d = {}) {
|
||||
var terminal = this.el.terminal;
|
||||
// Accept a dragable if it is an output terminal and has a
|
||||
// compatible type
|
||||
@@ -115,8 +87,8 @@ var BaseInputTerminalView = TerminalView.extend({
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
onDropStart: function(e, d) {
|
||||
}
|
||||
onDropStart(e, d = {}) {
|
||||
if (d.proxy.terminal) {
|
||||
if (this.$el.hasClass("can-accept")) {
|
||||
d.proxy.terminal.connectors[0].inner_color = "#BBFFBB";
|
||||
@@ -131,21 +103,21 @@ var BaseInputTerminalView = TerminalView.extend({
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
onDropEnd: function(e, d) {
|
||||
}
|
||||
onDropEnd(e, d = {}) {
|
||||
d.proxy.dropTooltip = "";
|
||||
if (d.proxy.terminal) {
|
||||
d.proxy.terminal.connectors[0].inner_color = "#FFFFFF";
|
||||
}
|
||||
},
|
||||
onDrop: function(e, d) {
|
||||
}
|
||||
onDrop(e, d = {}) {
|
||||
d.proxy.dropTooltip = "";
|
||||
if (this.$el.hasClass("can-accept")) {
|
||||
const terminal = this.el.terminal;
|
||||
new Connector(d.drag.terminal, terminal).redraw();
|
||||
new Connector(this.app.canvas_manager, d.drag.terminal, terminal).redraw();
|
||||
}
|
||||
},
|
||||
onHover: function() {
|
||||
}
|
||||
onHover() {
|
||||
const element = this.el;
|
||||
const terminal = element.terminal;
|
||||
// If connected, create a popup to allow disconnection
|
||||
@@ -168,51 +140,61 @@ var BaseInputTerminalView = TerminalView.extend({
|
||||
.append(t);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var InputTerminalView = BaseInputTerminalView.extend({
|
||||
terminalMappingClass: Terminals.TerminalMapping,
|
||||
terminalMappingViewClass: InputTerminalMappingView,
|
||||
terminalForInput: function(input) {
|
||||
export class InputTerminalView extends BaseInputTerminalView {
|
||||
constructor(app, options = {}) {
|
||||
super(app, options, Terminals.TerminalMapping);
|
||||
}
|
||||
terminalForInput(input) {
|
||||
return new Terminals.InputTerminal({
|
||||
app: this.app,
|
||||
element: this.el,
|
||||
input: input
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var InputParameterTerminalView = BaseInputTerminalView.extend({
|
||||
terminalMappingClass: Terminals.TerminalMapping,
|
||||
terminalMappingViewClass: InputTerminalMappingView,
|
||||
terminalForInput: function(input) {
|
||||
export class InputParameterTerminalView extends BaseInputTerminalView {
|
||||
constructor(app, options = {}) {
|
||||
super(app, options, Terminals.TerminalMapping);
|
||||
}
|
||||
terminalForInput(input) {
|
||||
return new Terminals.InputParameterTerminal({
|
||||
app: this.app,
|
||||
element: this.el,
|
||||
input: input
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var InputCollectionTerminalView = BaseInputTerminalView.extend({
|
||||
terminalMappingClass: Terminals.TerminalMapping,
|
||||
terminalMappingViewClass: InputTerminalMappingView,
|
||||
terminalForInput: function(input) {
|
||||
export class InputCollectionTerminalView extends BaseInputTerminalView {
|
||||
constructor(app, options = {}) {
|
||||
super(app, options, Terminals.TerminalMapping);
|
||||
}
|
||||
terminalForInput(input = {}) {
|
||||
return new Terminals.InputCollectionTerminal({
|
||||
app: this.app,
|
||||
element: this.el,
|
||||
input: input
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var BaseOutputTerminalView = TerminalView.extend({
|
||||
className: "terminal output-terminal",
|
||||
initialize: function(options) {
|
||||
export class BaseOutputTerminalView extends TerminalView {
|
||||
constructor(app, options, terminalMappingClass = null) {
|
||||
super();
|
||||
this.app = app;
|
||||
this.el = document.createElement("div");
|
||||
this.el.className = "terminal output-terminal";
|
||||
this.$el = $(this.el);
|
||||
const node = options.node;
|
||||
const output = options.output;
|
||||
const name = output.name;
|
||||
node.cid = NODEINDEX++;
|
||||
const id = `node-${node.cid}-output-${name}`;
|
||||
const terminal = this.terminalForOutput(output);
|
||||
this.setupMappingView(terminal);
|
||||
this.setupMappingView(terminalMappingClass, terminal);
|
||||
this.el.terminal = terminal;
|
||||
this.$el.attr(
|
||||
"aria-label",
|
||||
@@ -227,35 +209,31 @@ var BaseOutputTerminalView = TerminalView.extend({
|
||||
terminal.name = name;
|
||||
terminal.label = output.label;
|
||||
node.output_terminals[name] = terminal;
|
||||
},
|
||||
events: {
|
||||
drag: "onDrag",
|
||||
dragstart: "onDragStart",
|
||||
dragend: "onDragEnd",
|
||||
keydown: "screenReaderSelectOutputNode"
|
||||
},
|
||||
|
||||
screenReaderSelectOutputNode: function(e) {
|
||||
const self = this;
|
||||
this.$el.on("drag", (d, e) => self.onDrag(d, e));
|
||||
this.$el.on("dragstart", (d, e) => self.onDragStart(d, e));
|
||||
this.$el.on("dragend", (d, e) => self.onDragEnd(d, e));
|
||||
this.$el.on("keydown", e => self.screenReaderSelectOutputNode(e));
|
||||
}
|
||||
screenReaderSelectOutputNode(e) {
|
||||
const inputChoiceKeyDown = e => {
|
||||
e.stopPropagation();
|
||||
const currentItem = e.currentTarget;
|
||||
const previousItem = currentItem.previousSibling;
|
||||
const nextItem = currentItem.nextSibling;
|
||||
const inputTerminal = currentItem.input.context.terminal;
|
||||
|
||||
const switchActiveItem = (currentActive, newActive) => {
|
||||
newActive.classList.add("active");
|
||||
newActive.focus();
|
||||
currentActive.classList.remove("active");
|
||||
};
|
||||
|
||||
const removeMenu = () => {
|
||||
$(currentItem.parentNode).remove();
|
||||
this.$el.removeAttr("aria-owns");
|
||||
this.$el.attr("aria-grabbed", "false");
|
||||
this.$el.focus();
|
||||
};
|
||||
|
||||
switch (e.keyCode) {
|
||||
case 40: // Down arrow
|
||||
if (nextItem) {
|
||||
@@ -273,7 +251,7 @@ var BaseOutputTerminalView = TerminalView.extend({
|
||||
break;
|
||||
case 32: // Space
|
||||
removeMenu();
|
||||
new Connector(this.el.terminal, inputTerminal).redraw();
|
||||
new Connector(this.app.canvas_manager, this.el.terminal, inputTerminal).redraw();
|
||||
ariaAlert("Node connected");
|
||||
|
||||
if (inputTerminal.connectors.length > 0) {
|
||||
@@ -344,11 +322,10 @@ var BaseOutputTerminalView = TerminalView.extend({
|
||||
ariaAlert("Node selected");
|
||||
buildInputChoicesMenu();
|
||||
}
|
||||
},
|
||||
onDrag: function(e, d) {
|
||||
}
|
||||
onDrag(e, d = {}) {
|
||||
var onmove = () => {
|
||||
// FIXME: global
|
||||
var canvasZoom = window.workflow_globals.canvas_manager.canvasZoom;
|
||||
var canvasZoom = this.app.canvas_manager.canvasZoom;
|
||||
var po = $(d.proxy)
|
||||
.offsetParent()
|
||||
.offset();
|
||||
@@ -357,18 +334,15 @@ var BaseOutputTerminalView = TerminalView.extend({
|
||||
var y = d.offsetY - po.top;
|
||||
$(d.proxy).css({ left: x / canvasZoom, top: y / canvasZoom });
|
||||
d.proxy.terminal.redraw();
|
||||
// FIXME: global
|
||||
window.workflow_globals.canvas_manager.update_viewport_overlay();
|
||||
this.app.canvas_manager.update_viewport_overlay();
|
||||
};
|
||||
onmove();
|
||||
$("#canvas-container")
|
||||
.get(0)
|
||||
.scroll_panel.test(e, onmove);
|
||||
},
|
||||
onDragStart: function(e, d) {
|
||||
}
|
||||
onDragStart(e, d = {}) {
|
||||
$(d.available).addClass("input-terminal-active");
|
||||
// Save PJAs in the case of change datatype actions.
|
||||
window.workflow_globals.workflow.check_changes_in_active_form();
|
||||
// Drag proxy div
|
||||
var h = $("<div class='drag-terminal'/>")
|
||||
.appendTo("#canvas-container")
|
||||
@@ -383,17 +357,13 @@ var BaseOutputTerminalView = TerminalView.extend({
|
||||
}
|
||||
});
|
||||
h.terminal = new Terminals.OutputTerminal({ element: h });
|
||||
var c = new Connector();
|
||||
var c = new Connector(this.app.canvas_manager);
|
||||
c.dragging = true;
|
||||
c.connect(this.el.terminal, h.terminal);
|
||||
return h;
|
||||
},
|
||||
onDragEnd: function(e, d) {
|
||||
}
|
||||
onDragEnd(e, d = {}) {
|
||||
var connector = d.proxy.terminal.connectors[0];
|
||||
// check_changes_in_active_form may change the state and cause a
|
||||
// the connection to have already been destroyed. There must be better
|
||||
// ways to handle this but the following check fixes some serious GUI
|
||||
// bugs for now.
|
||||
if (connector) {
|
||||
connector.destroy();
|
||||
}
|
||||
@@ -404,12 +374,13 @@ var BaseOutputTerminalView = TerminalView.extend({
|
||||
.get(0)
|
||||
.scroll_panel.stop();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var OutputTerminalView = BaseOutputTerminalView.extend({
|
||||
terminalMappingClass: Terminals.TerminalMapping,
|
||||
terminalMappingViewClass: TerminalMappingView,
|
||||
terminalForOutput: function(output) {
|
||||
export class OutputTerminalView extends BaseOutputTerminalView {
|
||||
constructor(app, options = {}) {
|
||||
super(app, options, Terminals.TerminalMapping);
|
||||
}
|
||||
terminalForOutput(output) {
|
||||
var type = output.extensions;
|
||||
return new Terminals.OutputTerminal({
|
||||
element: this.el,
|
||||
@@ -418,12 +389,13 @@ var OutputTerminalView = BaseOutputTerminalView.extend({
|
||||
optional: output.optional
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var OutputCollectionTerminalView = BaseOutputTerminalView.extend({
|
||||
terminalMappingClass: Terminals.TerminalMapping,
|
||||
terminalMappingViewClass: TerminalMappingView,
|
||||
terminalForOutput: function(output) {
|
||||
export class OutputCollectionTerminalView extends BaseOutputTerminalView {
|
||||
constructor(app, options = {}) {
|
||||
super(app, options, Terminals.TerminalMapping);
|
||||
}
|
||||
terminalForOutput(output) {
|
||||
var collection_type = output.collection_type;
|
||||
var collection_type_source = output.collection_type_source;
|
||||
return new Terminals.OutputCollectionTerminal({
|
||||
@@ -435,25 +407,26 @@ var OutputCollectionTerminalView = BaseOutputTerminalView.extend({
|
||||
optional: output.optional
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var OutputParameterTerminalView = BaseOutputTerminalView.extend({
|
||||
terminalMappingClass: Terminals.TerminalMapping,
|
||||
terminalMappingViewClass: TerminalMappingView,
|
||||
terminalForOutput: function(output) {
|
||||
export class OutputParameterTerminalView extends BaseOutputTerminalView {
|
||||
constructor(app, options = {}) {
|
||||
super(app, options, Terminals.TerminalMapping);
|
||||
}
|
||||
terminalForOutput(output) {
|
||||
return new Terminals.OutputParameterTerminal({
|
||||
element: this.el,
|
||||
type: output.type,
|
||||
optional: output.optional
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default {
|
||||
InputTerminalView: InputTerminalView,
|
||||
InputParameterTerminalView: InputParameterTerminalView,
|
||||
InputCollectionTerminalView: InputCollectionTerminalView,
|
||||
OutputTerminalView: OutputTerminalView,
|
||||
OutputParameterTerminalView: OutputParameterTerminalView,
|
||||
InputCollectionTerminalView: InputCollectionTerminalView,
|
||||
OutputCollectionTerminalView: OutputCollectionTerminalView
|
||||
};
|
||||
|
||||
@@ -1,616 +0,0 @@
|
||||
import _ from "underscore";
|
||||
import $ from "jquery";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { getGalaxyInstance } from "app";
|
||||
import _l from "utils/localization";
|
||||
import Utils from "utils/utils";
|
||||
import Workflow from "mvc/workflow/workflow-manager";
|
||||
import WorkflowCanvas from "mvc/workflow/workflow-canvas";
|
||||
import { Node } from "mvc/workflow/workflow-node";
|
||||
import WorkflowIcons from "mvc/workflow/workflow-icons";
|
||||
import FormWrappers from "mvc/workflow/workflow-forms";
|
||||
import { mountWorkflowNode } from "components/Workflow/Editor/mount";
|
||||
import "ui/editable-text";
|
||||
|
||||
import { hide_modal, show_message, show_modal } from "layout/modal";
|
||||
|
||||
// TODO; tie into Galaxy state?
|
||||
window.workflow_globals = window.workflow_globals || {};
|
||||
|
||||
const DEFAULT_INVOCATION_REPORT = `
|
||||
# Workflow Execution Report
|
||||
|
||||
## Workflow Inputs
|
||||
\`\`\`galaxy
|
||||
invocation_inputs()
|
||||
\`\`\`
|
||||
|
||||
## Workflow Outputs
|
||||
\`\`\`galaxy
|
||||
invocation_outputs()
|
||||
\`\`\`
|
||||
|
||||
## Workflow
|
||||
\`\`\`galaxy
|
||||
workflow_display()
|
||||
\`\`\`
|
||||
`;
|
||||
|
||||
export class WorkflowView {
|
||||
constructor(options, reportsEditor = {}) {
|
||||
var self = (window.workflow_globals.app = this);
|
||||
this.options = options;
|
||||
this.reportsEditor = reportsEditor;
|
||||
|
||||
// Canvas overview management
|
||||
this.canvas_manager = window.workflow_globals.canvas_manager = new WorkflowCanvas(
|
||||
this,
|
||||
$("#canvas-viewport"),
|
||||
$("#overview")
|
||||
);
|
||||
|
||||
// Initialize workflow state
|
||||
this.reset();
|
||||
|
||||
// get available datatypes for post job action options
|
||||
this.datatypes = JSON.parse(
|
||||
$.ajax({
|
||||
url: `${getAppRoot()}api/datatypes`,
|
||||
async: false
|
||||
}).responseText
|
||||
);
|
||||
|
||||
// get datatype mapping options
|
||||
this.datatypes_mapping = JSON.parse(
|
||||
$.ajax({
|
||||
url: `${getAppRoot()}api/datatypes/mapping`,
|
||||
async: false
|
||||
}).responseText
|
||||
);
|
||||
|
||||
// set mapping sub lists
|
||||
this.ext_to_type = this.datatypes_mapping.ext_to_class_name;
|
||||
this.type_to_type = this.datatypes_mapping.class_to_classes;
|
||||
|
||||
this.get_workflow_versions = function() {
|
||||
const _workflow_version_dropdown = {};
|
||||
const workflow_versions = JSON.parse(
|
||||
$.ajax({
|
||||
url: `${getAppRoot()}api/workflows/${self.options.id}/versions`,
|
||||
async: false
|
||||
}).responseText
|
||||
);
|
||||
|
||||
for (let i = 0; i < workflow_versions.length; i++) {
|
||||
const current_wf = workflow_versions[i];
|
||||
let version_text = `Version ${current_wf.version}, ${current_wf.steps} steps`;
|
||||
let selected = false;
|
||||
if (i == self.workflow.workflow_version) {
|
||||
version_text = `${version_text} (active)`;
|
||||
selected = true;
|
||||
}
|
||||
_workflow_version_dropdown[version_text] = {
|
||||
version: i,
|
||||
selected: selected
|
||||
};
|
||||
}
|
||||
return _workflow_version_dropdown;
|
||||
};
|
||||
|
||||
this.build_version_select = function() {
|
||||
const versions = this.get_workflow_versions();
|
||||
$("#workflow-version-switch").empty();
|
||||
$.each(versions, function(k, v) {
|
||||
$("#workflow-version-switch").append(
|
||||
$("<option></option>")
|
||||
.html(k)
|
||||
.val(v.version)
|
||||
.selected(v.selected)
|
||||
);
|
||||
});
|
||||
$("#workflow-version-switch").on("change", function() {
|
||||
$("#workflow-version-switch").unbind("change");
|
||||
if (this.value != self.workflow.workflow_version) {
|
||||
if (self.workflow && self.workflow.has_changes) {
|
||||
const r = window.confirm(
|
||||
"There are unsaved changes to your workflow which will be lost. Continue ?"
|
||||
);
|
||||
if (r == false) {
|
||||
// We rebuild the version select list, to reset the selected version
|
||||
self.build_version_select();
|
||||
return;
|
||||
}
|
||||
}
|
||||
self.load_workflow(self.options.id, this.value);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
this.load_workflow = function load_workflow(id, version) {
|
||||
this._workflowLoadAjax(id, version, {
|
||||
success: function(data) {
|
||||
self.reset();
|
||||
self.workflow.from_simple(data, true);
|
||||
const report = data.report || {};
|
||||
const markdown = report.markdown || DEFAULT_INVOCATION_REPORT;
|
||||
self.reportsEditor.input = markdown;
|
||||
self.workflow.has_changes = false;
|
||||
self.workflow.fit_canvas_to_nodes();
|
||||
self.scroll_to_nodes();
|
||||
self.canvas_manager.draw_overview();
|
||||
self.build_version_select();
|
||||
|
||||
// Determine if any parameters were 'upgraded' and provide message
|
||||
var upgrade_message = "";
|
||||
_.each(data.steps, (step, step_id) => {
|
||||
var details = "";
|
||||
if (step.errors) {
|
||||
details += `<li>${step.errors}</li>`;
|
||||
}
|
||||
_.each(data.upgrade_messages[step_id], m => {
|
||||
details += `<li>${m}</li>`;
|
||||
});
|
||||
if (details) {
|
||||
upgrade_message += `<li>Step ${parseInt(step_id, 10) + 1}: ${
|
||||
self.workflow.nodes[step_id].name
|
||||
}<ul>${details}</ul></li>`;
|
||||
}
|
||||
});
|
||||
if (upgrade_message) {
|
||||
show_modal(
|
||||
"Issues loading this workflow",
|
||||
`Please review the following issues, possibly resulting from tool upgrades or changes.<p><ul>${upgrade_message}</ul></p>`,
|
||||
{ Continue: hide_modal }
|
||||
);
|
||||
} else {
|
||||
hide_modal();
|
||||
}
|
||||
self.showWorkflowParameters();
|
||||
},
|
||||
error: function(response) {
|
||||
show_modal("Loading workflow failed.", response.err_msg, {
|
||||
Ok: function(response) {
|
||||
window.onbeforeunload = undefined;
|
||||
window.document.location = `${getAppRoot()}workflows/list`;
|
||||
}
|
||||
});
|
||||
},
|
||||
beforeSubmit: function(data) {
|
||||
show_message("Loading workflow", "progress");
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Load workflow definition
|
||||
this.load_workflow(self.options.id, self.options.version);
|
||||
|
||||
// On load, set the size to the pref stored in local storage if it exists
|
||||
var overview_size = localStorage.getItem("overview-size");
|
||||
if (overview_size !== undefined) {
|
||||
$(".workflow-overview").css({
|
||||
width: overview_size,
|
||||
height: overview_size
|
||||
});
|
||||
}
|
||||
|
||||
// Stores the size of the overview into local storage when it's resized
|
||||
$(".workflow-overview").bind("dragend", function(e, d) {
|
||||
var op = $(this).offsetParent();
|
||||
var opo = op.offset();
|
||||
var new_size = Math.max(op.width() - (d.offsetX - opo.left), op.height() - (d.offsetY - opo.top));
|
||||
localStorage.setItem("overview-size", `${new_size}px`);
|
||||
});
|
||||
|
||||
// Unload handler
|
||||
window.onbeforeunload = () => {
|
||||
if (self.workflow && self.workflow.has_changes) {
|
||||
return "There are unsaved changes to your workflow which will be lost.";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
copy_into_workflow(id = null, stepCount = null) {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (stepCount < 2) {
|
||||
this._copy_into_workflow_ajax(id);
|
||||
} else {
|
||||
// don't ruin the workflow by adding 50 steps unprompted.
|
||||
var self = this;
|
||||
Galaxy.modal.show({
|
||||
title: _l("Warning"),
|
||||
body: `This will copy ${stepCount} new steps into your workflow.`,
|
||||
buttons: {
|
||||
Cancel: function() {
|
||||
Galaxy.modal.hide();
|
||||
},
|
||||
Copy: function() {
|
||||
Galaxy.modal.hide();
|
||||
self._copy_into_workflow_ajax(id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
_copy_into_workflow_ajax(workflowId) {
|
||||
// Load workflow definition
|
||||
var self = this;
|
||||
this._workflowLoadAjax(workflowId, null, {
|
||||
success: function(data) {
|
||||
self.workflow.from_simple(data, false);
|
||||
// Determine if any parameters were 'upgraded' and provide message
|
||||
var upgrade_message = "";
|
||||
$.each(data.upgrade_messages, (k, v) => {
|
||||
upgrade_message += `<li>Step ${parseInt(k, 10) + 1}: ${self.workflow.nodes[k].name}<ul>`;
|
||||
$.each(v, (i, vv) => {
|
||||
upgrade_message += `<li>${vv}</li>`;
|
||||
});
|
||||
upgrade_message += "</ul></li>";
|
||||
});
|
||||
if (upgrade_message) {
|
||||
show_modal(
|
||||
"Subworkflow embedded with changes",
|
||||
`Problems were encountered loading this workflow (possibly a result of tool upgrades). Please review the following parameters and then save.<ul>${upgrade_message}</ul>`,
|
||||
{ Continue: hide_modal }
|
||||
);
|
||||
} else {
|
||||
hide_modal();
|
||||
}
|
||||
},
|
||||
beforeSubmit: function() {
|
||||
show_message("Importing workflow", "progress");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Global state for the whole workflow
|
||||
reset() {
|
||||
if (this.workflow) {
|
||||
this.workflow.remove_all();
|
||||
}
|
||||
this.workflow = window.workflow_globals.workflow = new Workflow(this, $("#canvas-container"));
|
||||
}
|
||||
|
||||
scroll_to_nodes() {
|
||||
var cv = $("#canvas-viewport");
|
||||
var cc = $("#canvas-container");
|
||||
var top;
|
||||
var left;
|
||||
if (cc.width() < cv.width()) {
|
||||
left = (cv.width() - cc.width()) / 2;
|
||||
} else {
|
||||
left = 0;
|
||||
}
|
||||
if (cc.height() < cv.height()) {
|
||||
top = (cv.height() - cc.height()) / 2;
|
||||
} else {
|
||||
top = 0;
|
||||
}
|
||||
cc.css({ left: left, top: top });
|
||||
}
|
||||
|
||||
_workflowLoadAjax(workflowId, version, options) {
|
||||
$.ajax(
|
||||
Utils.merge(options, {
|
||||
url: `${getAppRoot()}workflow/load_workflow`,
|
||||
data: { id: workflowId, _: "true", version: version },
|
||||
dataType: "json",
|
||||
cache: false
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
_moduleInitAjax(node, request_data) {
|
||||
var self = this;
|
||||
Utils.request({
|
||||
type: "POST",
|
||||
url: `${getAppRoot()}api/workflows/build_module`,
|
||||
data: request_data,
|
||||
success: function(data) {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
node.init_field_data(data);
|
||||
node.update_field_data(data);
|
||||
// Post init/update, for new modules we want to default to
|
||||
// nodes being outputs
|
||||
// TODO: Overhaul the handling of all this when we modernize
|
||||
// the editor, replace callout image manipulation with a simple
|
||||
// class toggle, etc.
|
||||
$.each(node.output_terminals, (ot_id, ot) => {
|
||||
node.addWorkflowOutput(ot.name);
|
||||
var callout = $(node.element).find(`.callout.${ot.name.replace(/(?=[()])/g, "\\")}`);
|
||||
callout.find("img").attr("src", `${Galaxy.root}static/images/fugue/asterisk-small.png`);
|
||||
});
|
||||
self.workflow.activate_node(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Add a new step to the workflow by tool id
|
||||
add_node_for_tool(id, title) {
|
||||
var node = this.workflow.create_node("tool", title, id);
|
||||
this._moduleInitAjax(node, {
|
||||
type: "tool",
|
||||
tool_id: id,
|
||||
_: "true"
|
||||
});
|
||||
}
|
||||
|
||||
// Add a new step to the workflow by tool id
|
||||
add_node_for_subworkflow(id, title) {
|
||||
var node = this.workflow.create_node("subworkflow", title, id);
|
||||
this._moduleInitAjax(node, {
|
||||
type: "subworkflow",
|
||||
content_id: id,
|
||||
_: "true"
|
||||
});
|
||||
}
|
||||
|
||||
add_node_for_module(type, title) {
|
||||
var node = this.workflow.create_node(type, title);
|
||||
this._moduleInitAjax(node, { type: type, _: "true" });
|
||||
}
|
||||
|
||||
display_file_list(node) {
|
||||
var addlist = "<select id='node_data_list' name='node_data_list'>";
|
||||
for (var out_terminal in node.output_terminals) {
|
||||
addlist += `<option value='${out_terminal}'>${out_terminal}</option>`;
|
||||
}
|
||||
addlist += "</select>";
|
||||
return addlist;
|
||||
}
|
||||
|
||||
showWorkflowParameters() {
|
||||
var parameter_re = /\$\{.+?\}/g;
|
||||
var workflow_parameters = [];
|
||||
var wf_parm_container = $("#workflow-parameters-container");
|
||||
var wf_parm_box = $("#workflow-parameters-box");
|
||||
var new_parameter_content = "";
|
||||
var matches = [];
|
||||
$.each(this.workflow.nodes, (k, node) => {
|
||||
if (node.config_form && node.config_form.inputs) {
|
||||
Utils.deepeach(node.config_form.inputs, d => {
|
||||
if (typeof d.value == "string") {
|
||||
var form_matches = d.value.match(parameter_re);
|
||||
if (form_matches) {
|
||||
matches = matches.concat(form_matches);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (node.post_job_actions) {
|
||||
$.each(node.post_job_actions, (k, pja) => {
|
||||
if (pja.action_arguments) {
|
||||
$.each(pja.action_arguments, (k, action_argument) => {
|
||||
if (typeof action_argument === "string") {
|
||||
const arg_matches = action_argument.match(parameter_re);
|
||||
if (arg_matches) {
|
||||
matches = matches.concat(arg_matches);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
if (matches) {
|
||||
$.each(matches, (k, element) => {
|
||||
if ($.inArray(element, workflow_parameters) === -1) {
|
||||
workflow_parameters.push(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (workflow_parameters && workflow_parameters.length !== 0) {
|
||||
$.each(workflow_parameters, (k, element) => {
|
||||
new_parameter_content += `<div>${element.substring(2, element.length - 1)}</div>`;
|
||||
});
|
||||
wf_parm_container.html(new_parameter_content);
|
||||
wf_parm_box.show();
|
||||
} else {
|
||||
wf_parm_container.html(new_parameter_content);
|
||||
wf_parm_box.hide();
|
||||
}
|
||||
}
|
||||
|
||||
showAttributes() {
|
||||
$(".right-content").hide();
|
||||
$("#edit-attributes").show();
|
||||
}
|
||||
|
||||
showForm(content, node) {
|
||||
const cls = "right-content";
|
||||
var id = `${cls}-${node.id}`;
|
||||
var $container = $(`#${cls}`);
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (content && $container.find(`#${id}`).length === 0) {
|
||||
var $el = $(`<div id="${id}" class="${cls}"/>`);
|
||||
content.node = node;
|
||||
content.workflow = this.workflow;
|
||||
content.datatypes = this.datatypes;
|
||||
content.icon = WorkflowIcons[node.type];
|
||||
content.cls = "ui-portlet-section";
|
||||
if (node) {
|
||||
var form_type = node.type == "tool" ? "Tool" : "Default";
|
||||
$el.append(new FormWrappers[form_type](content).form.$el);
|
||||
$container.append($el);
|
||||
} else {
|
||||
Galaxy.emit.debug("workflow-view::initialize()", "Node not found in workflow.");
|
||||
}
|
||||
}
|
||||
$(`.${cls}`).hide();
|
||||
$container.find(`#${id}`).show();
|
||||
$container.show();
|
||||
$container.scrollTop();
|
||||
}
|
||||
|
||||
isSubType(child, parent) {
|
||||
child = this.ext_to_type[child];
|
||||
parent = this.ext_to_type[parent];
|
||||
return this.type_to_type[child] && parent in this.type_to_type[child];
|
||||
}
|
||||
|
||||
report_changed(report_markdown) {
|
||||
this.workflow.has_changes = true;
|
||||
this.workflow.report.markdown = report_markdown;
|
||||
}
|
||||
|
||||
save_current_workflow() {
|
||||
const self = this;
|
||||
show_message("Saving workflow", "progress");
|
||||
self.workflow.check_changes_in_active_form();
|
||||
if (!self.workflow.has_changes) {
|
||||
hide_modal();
|
||||
return;
|
||||
}
|
||||
self.workflow.rectify_workflow_outputs();
|
||||
Utils.request({
|
||||
url: `${getAppRoot()}api/workflows/${self.options.id}`,
|
||||
type: "PUT",
|
||||
data: { workflow: self.workflow.to_simple(), from_tool_form: true },
|
||||
success: function(data) {
|
||||
var body = $("<div/>").text(data.message);
|
||||
if (data.errors) {
|
||||
body.addClass("warningmark");
|
||||
var errlist = $("<ul/>");
|
||||
$.each(data.errors, (i, v) => {
|
||||
$("<li/>")
|
||||
.text(v)
|
||||
.appendTo(errlist);
|
||||
});
|
||||
body.append(errlist);
|
||||
} else {
|
||||
body.addClass("donemark");
|
||||
}
|
||||
self.workflow.name = data.name;
|
||||
self.workflow.has_changes = false;
|
||||
self.workflow.stored = true;
|
||||
self.workflow.workflow_version = data.version;
|
||||
self.showWorkflowParameters();
|
||||
self.build_version_select();
|
||||
if (data.errors) {
|
||||
show_modal("Saving workflow", body, {
|
||||
Ok: hide_modal
|
||||
});
|
||||
} else {
|
||||
hide_modal();
|
||||
}
|
||||
},
|
||||
error: function(response) {
|
||||
show_modal("Saving workflow failed.", response.err_msg, { Ok: hide_modal });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
workflow_save_as() {
|
||||
const self = this;
|
||||
var body = $(
|
||||
'<form><label style="display:inline-block; width: 100%;">Save as name: </label><input type="text" id="workflow_rename" style="width: 80%;" autofocus/>' +
|
||||
'<br><label style="display:inline-block; width: 100%;">Annotation: </label><input type="text" id="wf_annotation" style="width: 80%;" /></form>'
|
||||
);
|
||||
show_modal("Save As a New Workflow", body, {
|
||||
OK: function() {
|
||||
var rename_name =
|
||||
$("#workflow_rename").val().length > 0
|
||||
? $("#workflow_rename").val()
|
||||
: `SavedAs_${self.workflow.name}`;
|
||||
var rename_annotation = $("#wf_annotation").val().length > 0 ? $("#wf_annotation").val() : "";
|
||||
$.ajax({
|
||||
url: `${getAppRoot()}workflow/save_workflow_as`,
|
||||
type: "POST",
|
||||
data: {
|
||||
workflow_name: rename_name,
|
||||
workflow_annotation: rename_annotation,
|
||||
from_tool_form: true,
|
||||
workflow_data: function() {
|
||||
return JSON.stringify(self.workflow.to_simple());
|
||||
}
|
||||
}
|
||||
})
|
||||
.done(id => {
|
||||
window.onbeforeunload = undefined;
|
||||
window.location = `${getAppRoot()}workflow/editor?id=${id}`;
|
||||
hide_modal();
|
||||
})
|
||||
.fail(() => {
|
||||
hide_modal();
|
||||
alert("Saving this workflow failed. Please contact this site's administrator.");
|
||||
});
|
||||
},
|
||||
Cancel: hide_modal
|
||||
});
|
||||
}
|
||||
|
||||
layout_editor() {
|
||||
this.workflow.layout();
|
||||
this.workflow.fit_canvas_to_nodes();
|
||||
this.scroll_to_nodes();
|
||||
this.canvas_manager.draw_overview();
|
||||
}
|
||||
|
||||
prebuildNode(type, title_text, content_id) {
|
||||
var self = this;
|
||||
|
||||
// Create node wrapper
|
||||
const container = document.createElement("div");
|
||||
container.className = "toolForm toolFormInCanvas";
|
||||
document.getElementById("canvas-container").appendChild(container);
|
||||
var $f = $(container);
|
||||
|
||||
// Create backbone model and view
|
||||
var node = new Node(this, { element: $f });
|
||||
node.type = type;
|
||||
node.content_id = content_id;
|
||||
|
||||
// Mount node component as child dom to node wrapper
|
||||
const child = document.createElement("div");
|
||||
container.appendChild(child);
|
||||
mountWorkflowNode(child, {
|
||||
id: content_id,
|
||||
type: type,
|
||||
title: title_text,
|
||||
node: node
|
||||
});
|
||||
|
||||
// Set initial scroll position
|
||||
$f.css("left", $(window).scrollLeft() + 20);
|
||||
$f.css("top", $(window).scrollTop() + 20);
|
||||
|
||||
// Position in container
|
||||
var o = $("#canvas-container").position();
|
||||
var p = $("#canvas-container").parent();
|
||||
var width = $f.outerWidth() + 50;
|
||||
var height = $f.height();
|
||||
$f.css({
|
||||
left: -o.left + p.width() / 2 - width / 2,
|
||||
top: -o.top + p.height() / 2 - height / 2
|
||||
});
|
||||
$f.css("width", width);
|
||||
$f.bind("dragstart", () => {
|
||||
self.workflow.activate_node(node);
|
||||
})
|
||||
.bind("dragend", function() {
|
||||
self.workflow.node_changed(this);
|
||||
self.workflow.fit_canvas_to_nodes();
|
||||
self.canvas_manager.draw_overview();
|
||||
})
|
||||
.bind("dragclickonly", () => {
|
||||
self.workflow.activate_node(node);
|
||||
})
|
||||
.bind("drag", function(e, d) {
|
||||
// Move
|
||||
var po = $(this)
|
||||
.offsetParent()
|
||||
.offset();
|
||||
// Find relative offset and scale by zoom
|
||||
var x = (d.offsetX - po.left) / self.canvas_manager.canvasZoom;
|
||||
var y = (d.offsetY - po.top) / self.canvas_manager.canvasZoom;
|
||||
$(this).css({ left: x, top: y });
|
||||
// Redraw
|
||||
$(this)
|
||||
.find(".terminal")
|
||||
.each(function() {
|
||||
this.terminal.redraw();
|
||||
});
|
||||
});
|
||||
return node;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,14 @@
|
||||
/* global QUnit */
|
||||
import $ from "jquery";
|
||||
import testApp from "qunit/test-app";
|
||||
import DatatypesMapping from "qunit/test-data/json/datatypes.mapping.json";
|
||||
import sinon from "sinon";
|
||||
import Utils from "utils/utils";
|
||||
import { WorkflowView } from "mvc/workflow/workflow-view";
|
||||
import WorkflowManager from "mvc/workflow/workflow-manager";
|
||||
import { Node } from "mvc/workflow/workflow-node";
|
||||
import { NodeView } from "mvc/workflow/workflow-view-node";
|
||||
import Terminals from "mvc/workflow/workflow-terminals";
|
||||
import TerminalsView from "mvc/workflow/workflow-view-terminals";
|
||||
import Connector from "mvc/workflow/workflow-connector";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
|
||||
// window.show_modal = function(a, b, c) {};
|
||||
// window.hide_modal = function() {};
|
||||
|
||||
// create body and app
|
||||
var create_app = function() {
|
||||
@@ -28,23 +24,24 @@ var create_app = function() {
|
||||
);
|
||||
|
||||
// build app
|
||||
return new WorkflowView({
|
||||
id: null,
|
||||
urls: { get_datatypes: getAppRoot() + "api/datatypes/mapping" },
|
||||
workflows: []
|
||||
});
|
||||
return new WorkflowManager(
|
||||
{
|
||||
datatypes: [],
|
||||
datatypes_mapping: DatatypesMapping
|
||||
},
|
||||
$("#canvas-container")
|
||||
);
|
||||
};
|
||||
|
||||
QUnit.module("Input terminal model test", {
|
||||
beforeEach: function() {
|
||||
testApp.create();
|
||||
this.node = new Node(create_app(), {});
|
||||
this.app = create_app();
|
||||
this.node = new Node(this.app, {});
|
||||
this.input = { extensions: ["txt"], multiple: false, optional: false };
|
||||
this.input_terminal = new Terminals.InputTerminal({ input: this.input });
|
||||
this.input_terminal = new Terminals.InputTerminal({ app: this.app, input: this.input });
|
||||
this.input_terminal.node = this.node;
|
||||
},
|
||||
afterEach: function() {
|
||||
testApp.destroy();
|
||||
delete this.node;
|
||||
},
|
||||
multiple: function() {
|
||||
@@ -54,7 +51,7 @@ QUnit.module("Input terminal model test", {
|
||||
test_connector: function() {
|
||||
var outputTerminal = new Terminals.OutputTerminal({ datatypes: ["input"] });
|
||||
var inputTerminal = this.input_terminal;
|
||||
return new Connector(outputTerminal, inputTerminal);
|
||||
return new Connector({}, outputTerminal, inputTerminal);
|
||||
},
|
||||
with_test_connector: function(f) {
|
||||
this.test_connector();
|
||||
@@ -230,12 +227,12 @@ QUnit.module("Connector test", {});
|
||||
QUnit.test("connects only if both valid handles", function(assert) {
|
||||
const input = { connect: sinon.spy() };
|
||||
const output = { connect: sinon.spy() };
|
||||
new Connector(input, null);
|
||||
new Connector(null, output);
|
||||
new Connector({}, input, null);
|
||||
new Connector({}, null, output);
|
||||
// Not attempts to connect...
|
||||
assert.ok(!input.connect.called);
|
||||
assert.ok(!output.connect.called);
|
||||
new Connector(input, output);
|
||||
new Connector({}, input, output);
|
||||
assert.ok(input.connect.called);
|
||||
assert.ok(output.connect.called);
|
||||
});
|
||||
@@ -243,7 +240,7 @@ QUnit.test("connects only if both valid handles", function(assert) {
|
||||
QUnit.test("default attributes", function(assert) {
|
||||
const input = { connect: sinon.spy() };
|
||||
const output = { connect: sinon.spy() };
|
||||
const connector = new Connector(input, output);
|
||||
const connector = new Connector({}, input, output);
|
||||
assert.equal(connector.dragging, false);
|
||||
assert.equal(connector.canvas, null);
|
||||
assert.equal(connector.inner_color, "#FFFFFF");
|
||||
@@ -253,7 +250,7 @@ QUnit.test("default attributes", function(assert) {
|
||||
QUnit.test("destroy", function(assert) {
|
||||
const input = { connect: sinon.spy(), disconnect: sinon.spy() };
|
||||
const output = { connect: sinon.spy(), disconnect: sinon.spy() };
|
||||
const connector = new Connector(input, output);
|
||||
const connector = new Connector({}, input, output);
|
||||
connector.destroy();
|
||||
assert.ok(input.disconnect.called);
|
||||
assert.ok(output.disconnect.called);
|
||||
@@ -274,7 +271,7 @@ QUnit.test("initial redraw", function(assert) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const connector = new Connector(input, output);
|
||||
const connector = new Connector({}, input, output);
|
||||
const n = $("#canvas-container").find("canvas").length;
|
||||
connector.redraw();
|
||||
// Ensure canvas gets set
|
||||
@@ -285,14 +282,11 @@ QUnit.test("initial redraw", function(assert) {
|
||||
|
||||
QUnit.module("Input collection terminal model test", {
|
||||
beforeEach: function() {
|
||||
testApp.create();
|
||||
this.node = new Node(create_app(), {});
|
||||
this.app = create_app();
|
||||
this.node = new Node(this.app, {});
|
||||
this.input = { extensions: ["txt"], collection_types: ["list"] };
|
||||
this.input_terminal = new Terminals.InputCollectionTerminal({ input: this.input });
|
||||
this.input_terminal = new Terminals.InputCollectionTerminal({ app: this.app, input: this.input });
|
||||
this.input_terminal.node = this.node;
|
||||
},
|
||||
afterEach: function() {
|
||||
testApp.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -332,7 +326,6 @@ QUnit.test("Collection output cannot connect to different collection input type"
|
||||
|
||||
QUnit.module("Node unit test", {
|
||||
beforeEach: function() {
|
||||
testApp.create();
|
||||
this.input_terminal = { destroy: sinon.spy(), redraw: sinon.spy() };
|
||||
this.output_terminal = { destroy: sinon.spy(), redraw: sinon.spy() };
|
||||
this.app = create_app();
|
||||
@@ -341,15 +334,12 @@ QUnit.module("Node unit test", {
|
||||
this.node.input_terminals.i1 = this.input_terminal;
|
||||
this.node.output_terminals.o1 = this.output_terminal;
|
||||
},
|
||||
afterEach: function() {
|
||||
testApp.destroy();
|
||||
},
|
||||
$: function(selector) {
|
||||
return $(this.node.element.find(selector));
|
||||
},
|
||||
expect_workflow_node_changed: function(assert, f) {
|
||||
const node = this.node;
|
||||
const node_changed_spy = sinon.spy(this.app.workflow, "node_changed");
|
||||
const node_changed_spy = sinon.spy(this.app, "node_changed");
|
||||
f();
|
||||
assert.ok(node_changed_spy.calledWith(node));
|
||||
},
|
||||
@@ -382,7 +372,7 @@ QUnit.test("make active", function(assert) {
|
||||
});
|
||||
|
||||
QUnit.test("destroy", function(assert) {
|
||||
var remove_node_spy = sinon.spy(this.app.workflow, "remove_node");
|
||||
var remove_node_spy = sinon.spy(this.app, "remove_node");
|
||||
this.node.destroy();
|
||||
assert.ok(this.input_terminal.destroy.called);
|
||||
assert.ok(this.output_terminal.destroy.called);
|
||||
@@ -439,7 +429,7 @@ QUnit.test("init_field_data data", function(assert) {
|
||||
assert.equal(this.$(".input-terminal").length, 1);
|
||||
assert.equal(this.$(".rule").length, 1);
|
||||
assert.equal(this.$(".toolFormBody").children().length, 3);
|
||||
assert.equal(this.$(".nodeTitle").text(), "newnode");
|
||||
assert.equal(this.$(".node-title").text(), "newnode");
|
||||
assert.ok(
|
||||
this.$(".toolFormTitle")
|
||||
.find("i")
|
||||
@@ -451,14 +441,14 @@ QUnit.test("init_field_data data", function(assert) {
|
||||
QUnit.test("node title behavior", function(assert) {
|
||||
this.expect_workflow_node_changed(assert, () => {
|
||||
// Node created with name newnode
|
||||
assert.equal(this.$(".nodeTitle").text(), "newnode");
|
||||
assert.equal(this.$(".node-title").text(), "newnode");
|
||||
// init_field_data_simple doesn't change label, so it should
|
||||
// remain original name.
|
||||
this.init_field_data_simple();
|
||||
assert.equal(this.$(".nodeTitle").text(), "newnode");
|
||||
assert.equal(this.$(".node-title").text(), "newnode");
|
||||
// Despite awkward name, update does change the label...
|
||||
this.update_field_data_with_new_input();
|
||||
assert.equal(this.$(".nodeTitle").text(), "New Label");
|
||||
assert.equal(this.$(".node-title").text(), "New Label");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -482,7 +472,7 @@ QUnit.test("update_field_data preserves connectors", function(assert) {
|
||||
// Call init with one input and output.
|
||||
this.init_field_data_simple();
|
||||
|
||||
var connector = new Connector();
|
||||
var connector = new Connector({});
|
||||
var old_input_terminal = node.input_terminals.input1;
|
||||
old_input_terminal.connectors.push(connector);
|
||||
|
||||
@@ -519,23 +509,20 @@ QUnit.test("update_field_data destroys old terminals", function(assert) {
|
||||
|
||||
QUnit.module("create_node", {
|
||||
beforeEach: function() {
|
||||
testApp.create();
|
||||
this.app = create_app();
|
||||
},
|
||||
afterEach: function() {
|
||||
testApp.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("node added to workflow", function(assert) {
|
||||
var add_node_spy = sinon.spy(this.app.workflow, "add_node");
|
||||
var node = this.app.workflow.create_node("tool", "Cat Files", "cat1");
|
||||
var add_node_spy = sinon.spy(this.app, "add_node");
|
||||
var node = this.app.create_node("tool", "Cat Files", "cat1");
|
||||
assert.ok(add_node_spy.calledWith(node));
|
||||
});
|
||||
|
||||
// global NodeView
|
||||
QUnit.module("Node view ", {
|
||||
beforeEach: function() {
|
||||
this.app = create_app();
|
||||
this.set_for_node({
|
||||
input_terminals: {},
|
||||
output_terminals: {},
|
||||
@@ -550,7 +537,7 @@ QUnit.module("Node view ", {
|
||||
},
|
||||
set_for_node: function(node) {
|
||||
var element = $("<div><div class='toolFormBody'></div></div>");
|
||||
this.view = new NodeView({ node: node, $el: element });
|
||||
this.view = new NodeView(this.app, { node: node, $el: element });
|
||||
},
|
||||
connectAttachedTerminal: function(inputType, outputType) {
|
||||
this.view.addDataInput({ name: "TestName", extensions: [inputType] });
|
||||
@@ -571,7 +558,7 @@ QUnit.module("Node view ", {
|
||||
disableMapOver: function() {},
|
||||
mapOver: Terminals.NULL_COLLECTION_TYPE_DESCRIPTION
|
||||
};
|
||||
return new Connector(outputTerminal, terminal);
|
||||
return new Connector({}, outputTerminal, terminal);
|
||||
},
|
||||
connectAttachedMultiInputTerminal: function(inputType, outputType) {
|
||||
this.view.addDataInput({ name: "TestName", extensions: [inputType], multiple: true });
|
||||
@@ -592,7 +579,7 @@ QUnit.module("Node view ", {
|
||||
disableMapOver: function() {},
|
||||
mapOver: new Terminals.CollectionTypeDescription("list")
|
||||
};
|
||||
return new Connector(outputTerminal, terminal);
|
||||
return new Connector({}, outputTerminal, terminal);
|
||||
},
|
||||
connectAttachedMappedOutput: function() {
|
||||
this.view.addDataInput({ name: "TestName", extensions: ["txt"], input_type: "dataset_collection" });
|
||||
@@ -613,7 +600,7 @@ QUnit.module("Node view ", {
|
||||
disableMapOver: function() {},
|
||||
mapOver: new Terminals.CollectionTypeDescription("list")
|
||||
};
|
||||
return new Connector(outputTerminal, terminal);
|
||||
return new Connector({}, outputTerminal, terminal);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -672,7 +659,7 @@ QUnit.test("replacing terminal on data input destroys invalid connections", func
|
||||
var connector_destroy_spy = sinon.spy(connector, "destroy");
|
||||
// Replacing input with same name - but now of type bam should destroy connection.
|
||||
this.view.addDataInput({ name: "TestName", extensions: ["bam"] }, newElement);
|
||||
var terminal = newElement.find(".input-terminal")[0].terminal;
|
||||
newElement.find(".input-terminal")[0].terminal;
|
||||
assert.ok(connector_destroy_spy.called);
|
||||
});
|
||||
|
||||
@@ -683,7 +670,7 @@ QUnit.test("replacing terminal on data input with collection changes mapping vie
|
||||
this.view.addDataInput({ name: "TestName", extensions: ["txt"], input_type: "dataset_collection" }, newElement);
|
||||
// Input type changed to dataset_collection - old connections are reset.
|
||||
// Would be nice to preserve these connections and make them map over.
|
||||
var terminal = newElement.find(".input-terminal")[0].terminal;
|
||||
newElement.find(".input-terminal")[0].terminal;
|
||||
assert.ok(connector_destroy_spy.called);
|
||||
});
|
||||
|
||||
@@ -692,16 +679,17 @@ QUnit.test("replacing terminal on data collection input with simple input change
|
||||
var newElement = $("<div class='inputs'></div>");
|
||||
var connector_destroy_spy = sinon.spy(connector, "destroy");
|
||||
this.view.addDataInput({ name: "TestName", extensions: ["txt"], input_type: "dataset" }, newElement);
|
||||
var terminal = newElement.find(".input-terminal")[0].terminal;
|
||||
newElement.find(".input-terminal")[0].terminal;
|
||||
assert.ok(connector_destroy_spy.called);
|
||||
});
|
||||
|
||||
// global InputTerminalView
|
||||
QUnit.module("Input terminal view", {
|
||||
beforeEach: function() {
|
||||
this.app = create_app();
|
||||
this.node = { input_terminals: [] };
|
||||
this.input = { name: "i1", extensions: "txt", multiple: false };
|
||||
this.view = new TerminalsView.InputTerminalView({
|
||||
this.view = new TerminalsView.InputTerminalView(this.app, {
|
||||
node: this.node,
|
||||
input: this.input
|
||||
});
|
||||
@@ -722,9 +710,10 @@ QUnit.test("terminal element", function(assert) {
|
||||
|
||||
QUnit.module("Output terminal view", {
|
||||
beforeEach: function() {
|
||||
this.app = create_app();
|
||||
this.node = { output_terminals: [] };
|
||||
this.output = { name: "o1", extensions: "txt" };
|
||||
this.view = new TerminalsView.OutputTerminalView({
|
||||
this.view = new TerminalsView.OutputTerminalView(this.app, {
|
||||
node: this.node,
|
||||
output: this.output
|
||||
});
|
||||
@@ -812,15 +801,6 @@ QUnit.test("equal", function(assert) {
|
||||
assert.ok(!this.listType().equal(Terminals.NULL_COLLECTION_TYPE_DESCRIPTION));
|
||||
});
|
||||
|
||||
QUnit.module("TerminalMapping", {
|
||||
beforeEach: function() {
|
||||
testApp.create();
|
||||
},
|
||||
afterEach: function() {
|
||||
testApp.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
QUnit.test("default constructor", function(assert) {
|
||||
var terminal = {};
|
||||
var mapping = new Terminals.TerminalMapping({ terminal: terminal });
|
||||
@@ -844,22 +824,13 @@ QUnit.test("disableMapOver", function(assert) {
|
||||
mapOver: new Terminals.CollectionTypeDescription("list")
|
||||
});
|
||||
var changeSpy = sinon.spy();
|
||||
mapping.bind("change", changeSpy);
|
||||
mapping.on("change", changeSpy);
|
||||
mapping.disableMapOver();
|
||||
assert.ok(mapping.mapOver === Terminals.NULL_COLLECTION_TYPE_DESCRIPTION);
|
||||
assert.ok(changeSpy.called);
|
||||
});
|
||||
|
||||
QUnit.module("terminal mapping logic", {
|
||||
beforeEach: function() {
|
||||
testApp.create();
|
||||
},
|
||||
afterEach: function() {
|
||||
testApp.destroy();
|
||||
if (this.inputTerminal1) {
|
||||
this.inputTerminal1.element.remove();
|
||||
}
|
||||
},
|
||||
newInputTerminal: function(mapOver, input, node) {
|
||||
input = input || {};
|
||||
node = node || this.newNode();
|
||||
@@ -867,8 +838,9 @@ QUnit.module("terminal mapping logic", {
|
||||
input["extensions"] = ["data"];
|
||||
}
|
||||
var inputEl = $("<div>")[0];
|
||||
var inputTerminal = new Terminals.InputTerminal({ element: inputEl, input: input });
|
||||
var inputTerminalMapping = new Terminals.TerminalMapping({ terminal: inputTerminal });
|
||||
const app = create_app();
|
||||
var inputTerminal = new Terminals.InputTerminal({ app: app, element: inputEl, input: input });
|
||||
new Terminals.TerminalMapping({ terminal: inputTerminal });
|
||||
inputTerminal.node = node;
|
||||
if (mapOver) {
|
||||
inputTerminal.setMapOver(new Terminals.CollectionTypeDescription(mapOver));
|
||||
@@ -882,7 +854,12 @@ QUnit.module("terminal mapping logic", {
|
||||
input["extensions"] = ["data"];
|
||||
}
|
||||
const inputEl = $("<div>")[0];
|
||||
const inputTerminal = new Terminals.InputCollectionTerminal({ element: inputEl, input: input });
|
||||
const app = create_app();
|
||||
const inputTerminal = new Terminals.InputCollectionTerminal({
|
||||
app: app,
|
||||
element: inputEl,
|
||||
input: input
|
||||
});
|
||||
new Terminals.TerminalMapping({ terminal: inputTerminal });
|
||||
inputTerminal.node = node;
|
||||
return inputTerminal;
|
||||
@@ -915,7 +892,7 @@ QUnit.module("terminal mapping logic", {
|
||||
datatypes: output.extensions,
|
||||
collection_type: collectionType
|
||||
});
|
||||
var outputTerminalMapping = new Terminals.TerminalMapping({ terminal: outputTerminal });
|
||||
new Terminals.TerminalMapping({ terminal: outputTerminal });
|
||||
outputTerminal.node = node;
|
||||
if (mapOver) {
|
||||
outputTerminal.setMapOver(new Terminals.CollectionTypeDescription(mapOver));
|
||||
@@ -924,14 +901,15 @@ QUnit.module("terminal mapping logic", {
|
||||
},
|
||||
newNode: function() {
|
||||
var nodeEl = $("<div>")[0];
|
||||
return new Node(create_app(), { element: nodeEl });
|
||||
var app = create_app();
|
||||
return new Node(app, { element: nodeEl });
|
||||
},
|
||||
_addExistingOutput: function(terminal, output, connected) {
|
||||
var self = this;
|
||||
var node = terminal.node;
|
||||
if (connected) {
|
||||
var inputTerminal = self.newInputTerminal();
|
||||
new Connector(inputTerminal, output);
|
||||
new Connector({}, inputTerminal, output);
|
||||
}
|
||||
this._addTerminalTo(output, node.output_terminals);
|
||||
return output;
|
||||
@@ -956,7 +934,7 @@ QUnit.module("terminal mapping logic", {
|
||||
var connectedInput = this.newInputTerminal();
|
||||
var node = terminal.node;
|
||||
var outputTerminal = self.newOutputTerminal();
|
||||
new Connector(connectedInput, outputTerminal);
|
||||
new Connector({}, connectedInput, outputTerminal);
|
||||
this._addTerminalTo(connectedInput, node.input_terminals);
|
||||
return connectedInput;
|
||||
},
|
||||
@@ -1012,7 +990,7 @@ QUnit.test("unconstrained input can be mapped over", function(assert) {
|
||||
|
||||
QUnit.test("unmapped input can be mapped over if matching connected input terminals map type", function(assert) {
|
||||
this.inputTerminal1 = this.newInputTerminal();
|
||||
var connectedInput1 = this.addConnectedInput(this.inputTerminal1);
|
||||
this.addConnectedInput(this.inputTerminal1);
|
||||
var connectedInput2 = this.addConnectedInput(this.inputTerminal1);
|
||||
connectedInput2.setMapOver(new Terminals.CollectionTypeDescription("list"));
|
||||
this.verifyAttachable(assert, this.inputTerminal1, "list");
|
||||
@@ -1029,7 +1007,7 @@ QUnit.test(
|
||||
"unmapped input can be attached to by output collection if matching connected input terminals map type",
|
||||
function(assert) {
|
||||
this.inputTerminal1 = this.newInputTerminal();
|
||||
var connectedInput1 = this.addConnectedInput(this.inputTerminal1);
|
||||
this.addConnectedInput(this.inputTerminal1);
|
||||
var connectedInput2 = this.addConnectedInput(this.inputTerminal1);
|
||||
connectedInput2.setMapOver(new Terminals.CollectionTypeDescription("list"));
|
||||
var outputTerminal = this.newOutputCollectionTerminal("list");
|
||||
@@ -1041,7 +1019,7 @@ QUnit.test(
|
||||
"unmapped input cannot be attached to by output collection if matching connected input terminals don't match map type",
|
||||
function(assert) {
|
||||
this.inputTerminal1 = this.newInputTerminal();
|
||||
var connectedInput1 = this.addConnectedInput(this.inputTerminal1);
|
||||
this.addConnectedInput(this.inputTerminal1);
|
||||
var connectedInput2 = this.addConnectedInput(this.inputTerminal1);
|
||||
connectedInput2.setMapOver(new Terminals.CollectionTypeDescription("list"));
|
||||
var outputTerminal = this.newOutputCollectionTerminal("paired");
|
||||
@@ -1053,7 +1031,7 @@ QUnit.test(
|
||||
"unmapped input can be attached to by output collection if effective output type (output+mapover) is same as mapped over input",
|
||||
function(assert) {
|
||||
this.inputTerminal1 = this.newInputTerminal();
|
||||
var connectedInput1 = this.addConnectedInput(this.inputTerminal1);
|
||||
this.addConnectedInput(this.inputTerminal1);
|
||||
var connectedInput2 = this.addConnectedInput(this.inputTerminal1);
|
||||
connectedInput2.setMapOver(new Terminals.CollectionTypeDescription("list:paired"));
|
||||
var outputTerminal = this.newOutputCollectionTerminal("paired");
|
||||
@@ -1066,7 +1044,7 @@ QUnit.test(
|
||||
"unmapped input cannot be attached to by output collection if effective output type (output+mapover) is not same as mapped over input (1)",
|
||||
function(assert) {
|
||||
this.inputTerminal1 = this.newInputTerminal();
|
||||
var connectedInput1 = this.addConnectedInput(this.inputTerminal1);
|
||||
this.addConnectedInput(this.inputTerminal1);
|
||||
var connectedInput2 = this.addConnectedInput(this.inputTerminal1);
|
||||
connectedInput2.setMapOver(new Terminals.CollectionTypeDescription("list:paired"));
|
||||
var outputTerminal = this.newOutputCollectionTerminal("list");
|
||||
@@ -1079,7 +1057,7 @@ QUnit.test(
|
||||
"unmapped input cannot be attached to by output collection if effective output type (output+mapover) is not same as mapped over input (2)",
|
||||
function(assert) {
|
||||
this.inputTerminal1 = this.newInputTerminal();
|
||||
var connectedInput1 = this.addConnectedInput(this.inputTerminal1);
|
||||
this.addConnectedInput(this.inputTerminal1);
|
||||
var connectedInput2 = this.addConnectedInput(this.inputTerminal1);
|
||||
connectedInput2.setMapOver(new Terminals.CollectionTypeDescription("list:paired"));
|
||||
var outputTerminal = this.newOutputCollectionTerminal("list");
|
||||
|
||||
@@ -36,6 +36,7 @@ $fa-font-path: "../../../node_modules/@fortawesome/fontawesome-free/webfonts/";
|
||||
@import "charts.scss";
|
||||
@import "message.scss";
|
||||
@import "toolshed.scss";
|
||||
@import "workflow.scss";
|
||||
|
||||
// ==== Select2 ====
|
||||
/* fix for zero width select2 - remove when fixed there */
|
||||
@@ -516,7 +517,6 @@ div.metadataHelpBody {
|
||||
}
|
||||
|
||||
// Forms
|
||||
|
||||
div.toolFormBody div.toolFormTitle {
|
||||
background: transparent;
|
||||
border: none;
|
||||
@@ -547,43 +547,6 @@ div.toolFormRow {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.toolForm.toolFormInCanvas {
|
||||
@extend .card;
|
||||
z-index: 100;
|
||||
position: absolute;
|
||||
border-color: $brand-primary;
|
||||
&.toolForm-active {
|
||||
z-index: 1001;
|
||||
box-shadow: 0 0 0 2px $brand-primary;
|
||||
}
|
||||
.toolFormTitle {
|
||||
@extend .card-header;
|
||||
@extend .py-1;
|
||||
@extend .px-2;
|
||||
cursor: move;
|
||||
background: $brand-secondary;
|
||||
}
|
||||
.toolFormBody {
|
||||
@extend .card-body;
|
||||
@extend .p-0;
|
||||
@extend .mx-2;
|
||||
.rule {
|
||||
height: 0;
|
||||
border: none;
|
||||
border-bottom: dotted black 1px;
|
||||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.toolForm.toolFormInCanvas.tool-node-error {
|
||||
border-color: $state-danger-border;
|
||||
.toolFormTitle {
|
||||
background: $state-danger-bg;
|
||||
border-color: $state-danger-border;
|
||||
}
|
||||
}
|
||||
|
||||
div.form,
|
||||
div.toolForm {
|
||||
border: solid $form-border 1px;
|
||||
@@ -656,18 +619,6 @@ div.form-row-input label {
|
||||
@extend .mt-3;
|
||||
}
|
||||
|
||||
.workflow-right {
|
||||
background: $white;
|
||||
}
|
||||
|
||||
.workflow-right .right-content {
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
.workflow-right .right-content .section-row {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
select,
|
||||
textarea,
|
||||
@@ -1666,102 +1617,6 @@ div.toolSectionTitle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* TEMPORARY, REFACTOR THIS -- This is only image-related styles from the
|
||||
* workflow editor; everything should be moved out of the mako and into its own
|
||||
* file */
|
||||
|
||||
#canvas-viewport {
|
||||
background: $workflow-editor-bg url("../../images/light_gray_grid.gif");
|
||||
width: 100%;
|
||||
height: calc(100% - 2.5rem);
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.base-terminal {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
@extend .mt-1;
|
||||
color: $brand-light;
|
||||
position: absolute;
|
||||
z-index: 1500;
|
||||
> icon {
|
||||
@extend .fa;
|
||||
@extend .fa-chevron-circle-right;
|
||||
color: $brand-primary;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
.input-terminal {
|
||||
@extend .base-terminal;
|
||||
left: -9px;
|
||||
}
|
||||
.output-terminal {
|
||||
@extend .base-terminal;
|
||||
right: -9px;
|
||||
&:hover > icon {
|
||||
color: $brand-success;
|
||||
}
|
||||
}
|
||||
.input-terminal-active.can-accept > icon {
|
||||
color: $brand-success;
|
||||
}
|
||||
.input-terminal-active.cannot-accept > icon {
|
||||
color: $brand-warning;
|
||||
}
|
||||
.drag-terminal {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
color: $brand-success;
|
||||
position: absolute;
|
||||
z-index: 1500;
|
||||
}
|
||||
.mark-terminal {
|
||||
cursor: pointer;
|
||||
color: $text-muted;
|
||||
&.mark-terminal-active {
|
||||
color: $brand-success;
|
||||
}
|
||||
}
|
||||
.delete-terminal {
|
||||
@extend .btn;
|
||||
@extend .btn-sm;
|
||||
@extend .btn-primary;
|
||||
@extend .fa;
|
||||
@extend .fa-minus-circle;
|
||||
@extend .p-1;
|
||||
@extend .mt-1;
|
||||
position: absolute;
|
||||
z-index: 2500;
|
||||
top: 0px;
|
||||
left: -15px;
|
||||
}
|
||||
.callout-terminal {
|
||||
position: absolute;
|
||||
}
|
||||
.workflow-overview {
|
||||
position: absolute;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
border-top: solid gray 1px;
|
||||
border-left: solid grey 1px;
|
||||
padding: 7px 0 0 7px;
|
||||
background: $workflow-overview-bg no-repeat url("../../images/resizable.png");
|
||||
z-index: 20000;
|
||||
overflow: hidden;
|
||||
max-width: 300px;
|
||||
max-height: 300px;
|
||||
min-width: 50px;
|
||||
min-height: 50px;
|
||||
}
|
||||
#input-choices-menu {
|
||||
color: black;
|
||||
}
|
||||
|
||||
/* For Vue */
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
|
||||
@@ -156,7 +156,8 @@ $state-running-border: $border-color;
|
||||
|
||||
// Workflow editor
|
||||
$workflow-editor-bg: $white;
|
||||
$workflow-overview-bg: $gray-200;
|
||||
$workflow-overview-bg: $panel-bg-color;
|
||||
$worfklow-node-width: 10rem;
|
||||
|
||||
// Margins
|
||||
$spacer: 1rem;
|
||||
|
||||
@@ -0,0 +1,180 @@
|
||||
.workflow-client {
|
||||
.workflow-panel {
|
||||
background: $white;
|
||||
}
|
||||
.workflow-center {
|
||||
z-index: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
.toolForm.toolFormInCanvas {
|
||||
@extend .card;
|
||||
z-index: 100;
|
||||
width: $worfklow-node-width;
|
||||
position: absolute;
|
||||
border-color: $brand-primary;
|
||||
&.toolForm-active {
|
||||
z-index: 1001;
|
||||
box-shadow: 0 0 0 2px $brand-primary;
|
||||
}
|
||||
.toolFormTitle {
|
||||
@extend .card-header;
|
||||
@extend .py-1;
|
||||
@extend .px-2;
|
||||
cursor: move;
|
||||
background: $brand-secondary;
|
||||
}
|
||||
.toolFormBody {
|
||||
@extend .card-body;
|
||||
@extend .p-0;
|
||||
@extend .mx-2;
|
||||
.rule {
|
||||
height: 0;
|
||||
border: none;
|
||||
border-bottom: dotted black 1px;
|
||||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.toolForm.toolFormInCanvas.tool-node-error {
|
||||
border-color: $state-danger-border;
|
||||
.toolFormTitle {
|
||||
background: $state-danger-bg;
|
||||
border-color: $state-danger-border;
|
||||
}
|
||||
}
|
||||
.workflow-canvas {
|
||||
#canvas-viewport {
|
||||
background: $workflow-editor-bg url("../../images/light_gray_grid.gif");
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.base-terminal {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
@extend .mt-1;
|
||||
color: $brand-light;
|
||||
position: absolute;
|
||||
z-index: 1500;
|
||||
> icon {
|
||||
@extend .fa;
|
||||
@extend .fa-chevron-circle-right;
|
||||
color: $brand-primary;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 0px;
|
||||
}
|
||||
}
|
||||
.input-terminal {
|
||||
@extend .base-terminal;
|
||||
left: -9px;
|
||||
}
|
||||
.output-terminal {
|
||||
@extend .base-terminal;
|
||||
right: -9px;
|
||||
&:hover > icon {
|
||||
color: $brand-success;
|
||||
}
|
||||
}
|
||||
.input-terminal-active.can-accept > icon {
|
||||
color: $brand-success;
|
||||
}
|
||||
.input-terminal-active.cannot-accept > icon {
|
||||
color: $brand-warning;
|
||||
}
|
||||
.drag-terminal {
|
||||
@extend .fa;
|
||||
@extend .fa-circle;
|
||||
color: $brand-success;
|
||||
position: absolute;
|
||||
z-index: 1500;
|
||||
}
|
||||
.mark-terminal {
|
||||
cursor: pointer;
|
||||
color: $text-muted;
|
||||
&.mark-terminal-active {
|
||||
color: $brand-success;
|
||||
}
|
||||
}
|
||||
.delete-terminal {
|
||||
@extend .btn;
|
||||
@extend .btn-sm;
|
||||
@extend .btn-primary;
|
||||
@extend .fa;
|
||||
@extend .fa-minus-circle;
|
||||
@extend .p-1;
|
||||
@extend .mt-1;
|
||||
position: absolute;
|
||||
z-index: 2500;
|
||||
top: 0px;
|
||||
left: -15px;
|
||||
}
|
||||
.callout-terminal {
|
||||
position: absolute;
|
||||
}
|
||||
.workflow-overview {
|
||||
border-top-left-radius: 0.3rem;
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
border-top: solid $border-color 1px;
|
||||
border-left: solid $border-color 1px;
|
||||
padding: 7px 0 0 7px;
|
||||
background: $workflow-overview-bg no-repeat url("../../images/resizable.png");
|
||||
z-index: 20000;
|
||||
overflow: hidden;
|
||||
max-width: 300px;
|
||||
max-height: 300px;
|
||||
min-width: 50px;
|
||||
min-height: 50px;
|
||||
.workflow-overview-body {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
#input-choices-menu {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
.workflow-report-body {
|
||||
display: flex;
|
||||
}
|
||||
#canvas-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#overview-container {
|
||||
position: absolute;
|
||||
}
|
||||
#overview-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
#overview-viewport {
|
||||
@extend .rounded;
|
||||
position: absolute;
|
||||
width: 0px;
|
||||
height: 0px;
|
||||
border: solid $brand-info 3px;
|
||||
background: $white;
|
||||
opacity: 0.8;
|
||||
z-index: 10;
|
||||
}
|
||||
canvas {
|
||||
position: absolute;
|
||||
z-index: 10;
|
||||
}
|
||||
canvas.dragging {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -270,7 +270,7 @@ workflow_editor:
|
||||
selectors:
|
||||
_: "[node-label='${label}']"
|
||||
|
||||
title: '${_} .nodeTitle'
|
||||
title: '${_} .node-title'
|
||||
destroy: '${_} .node-destroy'
|
||||
clone: '${_} .node-clone'
|
||||
|
||||
@@ -280,8 +280,7 @@ workflow_editor:
|
||||
input_mapping_icon: "${_} [name='${name}'] .fa-folder-o"
|
||||
|
||||
selectors:
|
||||
canvas_body: '#workflow-canvas-body'
|
||||
canvas_title: '#workflow-canvas-title'
|
||||
canvas_body: '#workflow-canvas'
|
||||
edit_annotation: '#workflow-annotation'
|
||||
edit_name: '#workflow-name'
|
||||
|
||||
|
||||
@@ -15,12 +15,9 @@
|
||||
${parent.javascript_app()}
|
||||
<script type="text/javascript">
|
||||
var editorConfig = ${ h.dumps( editor_config ) };
|
||||
editorConfig.workflow_globals = workflow_globals;
|
||||
config.addInitialization(function(galaxy, config) {
|
||||
console.log("workflow/editor.mako, editorConfig", editorConfig);
|
||||
window.bundleEntries.mountWorkflowEditor(editorConfig);
|
||||
window.bundleEntries.mountToolBoxWorkflow(editorConfig);
|
||||
window.bundleEntries.mountWorkflowPanel(editorConfig);
|
||||
});
|
||||
</script>
|
||||
</%def>
|
||||
@@ -34,7 +31,7 @@
|
||||
</%def>
|
||||
|
||||
<%def name="overlay(visible=False)">
|
||||
${parent.overlay( "Loading workflow editor...",
|
||||
${parent.overlay( "Loading workflow...",
|
||||
"<div class='progress progress-striped progress-info active'><div class='progress-bar' style='width: 100%;'></div></div>", self.overlay_visible )}
|
||||
</%def>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user