Split up workflow output control and output visible controls

This commit is contained in:
mvdbeek
2023-01-09 17:22:38 +01:00
parent a8016b5a49
commit 32540d294e
10 changed files with 120 additions and 278 deletions
@@ -18,7 +18,7 @@ describe("FormOutputLabel", () => {
];
beforeEach(() => {
const stepOne = { id: 0, workflow_outputs: outputs };
const stepOne = { id: 0, outputs: [{ name: "output-name" }], workflow_outputs: outputs };
const pinia = createPinia();
setActivePinia(pinia);
wrapper = mount(FormOutputLabel, {
@@ -29,7 +29,7 @@ describe("FormOutputLabel", () => {
localVue,
pinia,
});
const stepTwo = { id: 1, workflow_outputs: outputs };
const stepTwo = { id: 1, outputs: [{ name: "other-name" }], workflow_outputs: outputs };
wrapperOther = mount(FormOutputLabel, {
propsData: {
name: "other-name",
@@ -75,7 +75,7 @@
</template>
<script>
import Vue, { computed } from "vue";
import Vue from "vue";
import BootstrapVue from "bootstrap-vue";
import { UntypedParameters } from "components/Workflow/Editor/modules/parameters";
import LintSection from "components/Workflow/Editor/LintSection";
@@ -94,6 +94,7 @@ import { library } from "@fortawesome/fontawesome-svg-core";
import { faMagic, faExclamationTriangle } from "@fortawesome/free-solid-svg-icons";
import { useWorkflowStepStore } from "@/stores/workflowStepStore";
import { DatatypesMapperModel } from "@/components/Datatypes/model";
import { storeToRefs } from "pinia";
Vue.use(BootstrapVue);
@@ -131,9 +132,8 @@ export default {
required: true,
},
},
setup(props) {
const stepStore = useWorkflowStepStore();
const hasActiveOutputs = computed(() => Object.keys(stepStore.workflowOutputs).length > 0);
setup() {
const { hasActiveOutputs } = storeToRefs(useWorkflowStepStore());
return { hasActiveOutputs };
},
computed: {
@@ -92,7 +92,6 @@
:datatypes-mapper="datatypesMapper"
v-on="$listeners"
@stopDragging="onStopDragging"
@onToggle="onToggleOutput"
@onChange="onChange" />
</div>
</draggable-wrapper>
@@ -109,7 +108,6 @@ import Recommendations from "@/components/Workflow/Editor/Recommendations.vue";
import NodeInput from "@/components/Workflow/Editor/NodeInput.vue";
import NodeOutput from "@/components/Workflow/Editor/NodeOutput.vue";
import DraggableWrapper from "@/components/Workflow/Editor/DraggablePan.vue";
import { ActiveOutputs } from "./modules/outputs";
import { computed, ref } from "vue";
import { useNodePosition } from "@/components/Workflow/Editor/composables/useNodePosition";
import { useWorkflowStateStore, type XYPosition } from "@/stores/workflowEditorStateStore";
@@ -163,7 +161,7 @@ const isLoading = computed(() => Boolean(stateStore.getStepLoadingState(props.id
const position = useNodePosition(el, props.id, stateStore);
const title = computed(() => props.step.label || props.step.name);
const idString = computed(() => `wf-node-step-${props.id}`);
const showRule = computed(() => props.step.inputs.length > 0 && props.step.outputs.length > 0);
const showRule = computed(() => props.step.inputs?.length > 0 && props.step.outputs?.length > 0);
const iconClass = computed(() => `icon fa fa-fw ${WorkflowIcons[props.step.type]}`);
const canClone = computed(() => props.step.type !== "subworkflow"); // Why ?
const isEnabled = getGalaxyInstance().config.enable_tool_recommendations; // getGalaxyInstance is not reactive
@@ -180,7 +178,6 @@ const classes = computed(() => {
const style = computed(() => {
return { top: props.step.position!.top + "px", left: props.step.position!.left + "px" };
});
const activeOutputs = new ActiveOutputs();
const errors = computed(() => props.step.errors || stateStore.getStepLoadingState(props.id)?.error);
const inputs = computed(() => props.step.inputs);
const outputs = computed(() => props.step.outputs);
@@ -213,10 +210,6 @@ function onClone() {
emit("onClone", props.id);
}
function onToggleOutput(name: string) {
activeOutputs.toggle(name);
}
function makeActive() {
emit("onActivate", props.id);
}
@@ -1,14 +1,23 @@
<template>
<div class="form-row dataRow output-data-row">
<div
v-if="showCallout"
v-if="showCalloutActiveOutput"
v-b-tooltip
:class="['callout-terminal', output.name]"
title="Unchecked outputs will be hidden and are not available as subworkflow outputs."
@keyup="onToggle"
@click="onToggle">
@keyup="onToggleActive"
@click="onToggleActive">
<i :class="['mark-terminal', activeClass]" />
</div>
<div
v-if="showCalloutVisible"
v-b-tooltip
:class="['callout-terminal', output.name]"
:title="visibleHint"
@keyup="onToggleVisible"
@click="onToggleVisible">
<i :class="['mark-terminal', visibleClass]" />
</div>
{{ label }}
<draggable-wrapper
:id="id"
@@ -49,6 +58,7 @@ import { ref, computed, watch, nextTick, toRefs } from "vue";
import { DatatypesMapperModel } from "@/components/Datatypes/model";
import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore";
import ConnectionMenu from "@/components/Workflow/Editor/ConnectionMenu";
import { useWorkflowStepStore } from "@/stores/workflowStepStore";
export default {
components: {
@@ -94,6 +104,8 @@ export default {
},
},
setup(props) {
const stateStore = useWorkflowStateStore();
const stepStore = useWorkflowStepStore();
const el = ref(null);
const { rootOffset, parentOffset, stepPosition, output, stepId, datatypesMapper } = toRefs(props);
const position = useCoordinatePosition(el, rootOffset, parentOffset, stepPosition);
@@ -117,11 +129,24 @@ export default {
});
const { terminal, isMappedOver: isMultiple } = useTerminal(stepId, effectiveOutput, datatypesMapper);
const workflowOutput = computed(() =>
props.workflowOutputs.find((workflowOutput) => workflowOutput.output_name == props.output.name)
);
const activeClass = computed(() => workflowOutput.value && "mark-terminal-active");
const isVisible = computed(() => {
const isHidden = `HideDatasetAction${props.output.name}` in props.postJobActions;
return !isHidden;
});
const visibleClass = computed(() => (isVisible.value ? "mark-terminal-visible" : "mark-terminal-hidden"));
const visibleHint = computed(() => {
if (isVisible.value) {
return `Output will be visible in history. Click to hide output.`;
} else {
return `Output will be hidden in history. Click to make output visible.`;
}
});
const label = computed(() => {
const workflowOutput = props.workflowOutputs.find(
(workflowOutput) => workflowOutput.output_name == props.output.name
);
const activeLabel = workflowOutput?.label || props.output.name;
const activeLabel = workflowOutput.value?.label || props.output.name;
return `${activeLabel} (${extensions.value.join(", ")})`;
});
@@ -140,10 +165,45 @@ export default {
}
}
const stateStore = useWorkflowStateStore();
function onToggleActive() {
const step = stepStore.getStep(stepId.value);
if (workflowOutput.value) {
console.log("wfo", workflowOutput.value);
step.workflow_outputs = step.workflow_outputs.filter(
(workflowOutput) => workflowOutput.output_name !== output.value.name
);
} else {
step.workflow_outputs.push({ output_name: output.value.name });
}
stepStore.updateStep(step);
}
function onToggleVisible() {
const actionKey = `HideDatasetAction${props.output.name}`;
const step = stepStore.getStep(stepId.value);
if (isVisible.value) {
step.post_job_actions = {
...step.post_job_actions,
[actionKey]: {
action_type: "HideDatasetAction",
output_name: props.output.name,
action_arguments: {},
},
};
} else {
const { [actionKey]: ignoreUnused, ...newPostJobActions } = step.post_job_actions;
step.post_job_actions = newPostJobActions;
}
stepStore.updateStep(step);
}
return {
el,
position,
activeClass,
visibleClass,
visibleHint,
isVisible,
terminal,
isMultiple,
label,
@@ -153,6 +213,8 @@ export default {
toggleChildComponent,
closeMenu,
effectiveOutput,
onToggleActive,
onToggleVisible,
};
},
data() {
@@ -189,12 +251,11 @@ export default {
id() {
return `node-${this.stepId}-output-${this.output.name}`;
},
activeClass() {
return this.output.activeOutput && "mark-terminal-active";
showCalloutActiveOutput() {
return this.stepType === "tool" || this.stepType === "subworkflow";
},
showCallout() {
return this.stepType == "tool";
showCalloutVisible() {
return this.stepType === "tool";
},
terminalClass() {
const cls = "terminal output-terminal";
@@ -234,9 +295,6 @@ export default {
this.dragY = 0;
this.$emit("stopDragging");
},
onToggle() {
this.$emit("onToggle", this.output.name);
},
},
};
</script>
@@ -1,136 +0,0 @@
import Vue from "vue";
export const allLabels = {};
export class ActiveOutputs {
constructor() {
this.entries = {};
}
/** Initialize list of active outputs from server response */
initialize(outputs, incoming) {
this.outputs = outputs;
this._refreshIndex();
incoming &&
incoming.forEach((entry) => {
if (entry.label && allLabels[entry.label]) {
entry.label = null;
}
this.add(entry.output_name, entry.label);
if (entry.label) {
allLabels[entry.label] = true;
}
});
}
/** Adds a new record to the value stack **/
add(name, label) {
if (!this.exists(name)) {
this.update(name, label);
return true;
}
return false;
}
/** Toggle an entry */
toggle(name) {
if (this.exists(name)) {
this.remove(name);
} else {
this.add(name);
}
}
/** Change label for an output */
labelOutput(outputName, newLabel) {
const activeOutput = this.get(outputName);
const oldLabel = activeOutput && activeOutput.label;
if (newLabel == oldLabel) {
return true;
}
if (this.outputsIndex[outputName] && !allLabels[newLabel]) {
const oldLabel = this.update(outputName, newLabel);
if (oldLabel && allLabels[oldLabel]) {
delete allLabels[oldLabel];
}
if (newLabel) {
allLabels[newLabel] = true;
}
return true;
}
return false;
}
/** Returns the number of added records **/
count() {
return Object.keys(this.entries).length;
}
/** Return label */
get(name) {
return this.entries[name];
}
/** Returns true if a record is available for a given key **/
exists(name) {
return !!this.entries[name];
}
/** Remove an entry given its name */
remove(name) {
const activeOutput = this.get(name);
const activeLabel = activeOutput && activeOutput.label;
if (activeLabel && allLabels[activeLabel]) {
delete allLabels[activeLabel];
}
delete this.entries[name];
this._updateOutput(name);
}
/** Returns list of all values */
getAll() {
return Object.values(this.entries);
}
/** Update an active outputs label */
update(name, label) {
const activeOutput = this.get(name);
const oldLabel = activeOutput && activeOutput.label;
this.entries[name] = {
output_name: name,
label: label || null,
};
this._updateOutput(name);
return oldLabel;
}
/** Removes all entries which are not in the parsed dictionary of names */
filterOutputs(names) {
this.getAll().forEach((wf_output) => {
if (!names.includes(wf_output.output_name)) {
this.remove(wf_output.output_name);
}
});
this._refreshIndex();
}
/** Update an output */
_updateOutput(name) {
const output = this.outputsIndex[name];
if (output) {
const activeOutput = this.get(output.name);
Vue.set(output, "activeOutput", !!activeOutput);
Vue.set(output, "activeLabel", activeOutput && activeOutput.label);
}
}
/** Refreshes dictionary of outputs */
_refreshIndex() {
this.outputsIndex = {};
this.outputs &&
this.outputs.forEach((o) => {
this.outputsIndex[o.name] = o;
this._updateOutput(o.name);
});
}
}
@@ -1,69 +0,0 @@
import { allLabels, ActiveOutputs } from "./outputs";
describe("Workflow Outputs", () => {
it("test output label handling", () => {
// Test adding initial node
const activeOutputs = new ActiveOutputs();
const outputs = [{ name: "output_0" }, { name: "output_1" }, { name: "output_2" }];
const incoming = [
{ output_name: "output_0", label: "label_0" },
{ output_name: "output_1", label: "label_0" },
{ output_name: "output_2", label: "label_1" },
];
activeOutputs.initialize(outputs, incoming);
expect(Object.keys(allLabels).length).toBe(2);
expect(allLabels["label_0"]).toBe(true);
expect(allLabels["label_1"]).toBe(true);
expect(activeOutputs.count()).toBe(3);
expect(activeOutputs.entries["output_0"].label).toBe("label_0");
expect(activeOutputs.entries["output_1"].label).toBe(null);
expect(activeOutputs.entries["output_2"].label).toBe("label_1");
// Test adding additional node
const activeOutputs_1 = new ActiveOutputs();
const outputs_1 = [{ name: "output_0" }, { name: "output_1" }, { name: "output_2" }];
const incoming_1 = [
{ output_name: "output_0", label: "label_0" },
{ output_name: "output_1", label: "label_0" },
{ output_name: "output_2", label: "label_2" },
];
activeOutputs_1.initialize(outputs_1, incoming_1);
expect(Object.keys(allLabels).length).toBe(3);
expect(allLabels["label_0"]).toBe(true);
expect(allLabels["label_1"]).toBe(true);
expect(allLabels["label_2"]).toBe(true);
expect(activeOutputs_1.count()).toBe(3);
expect(activeOutputs_1.entries["output_0"].label).toBe(null);
expect(activeOutputs_1.entries["output_1"].label).toBe(null);
expect(activeOutputs_1.entries["output_2"].label).toBe("label_2");
// Test toggle / removal
activeOutputs_1.toggle("output_0");
expect(activeOutputs_1.count()).toBe(2);
activeOutputs_1.toggle("output_1");
expect(activeOutputs_1.count()).toBe(1);
activeOutputs_1.toggle("output_0");
expect(activeOutputs_1.count()).toBe(2);
activeOutputs_1.toggle("output_1");
expect(activeOutputs_1.count()).toBe(3);
// Test output filtering
activeOutputs.filterOutputs(["output_0", "output_2"]);
expect(activeOutputs.count()).toBe(2);
expect(activeOutputs.entries["output_0"].label).toBe("label_0");
expect(activeOutputs.entries["output_1"]).toBe(undefined);
expect(activeOutputs.entries["output_2"].label).toBe("label_1");
// Update output label
const response = activeOutputs.labelOutput("output_0", "label_3");
expect(response).toBe(true);
expect(activeOutputs.entries["output_0"].label).toBe("label_3");
const response_1 = activeOutputs.labelOutput("output_0", "label_1");
expect(response_1).toBe(false);
expect(activeOutputs.entries["output_0"].label).toBe("label_3");
// Test output removal
activeOutputs.filterOutputs([]);
expect(Object.keys(allLabels).length).toBe(1);
});
});
@@ -19,8 +19,7 @@
"extensions": [
"input"
],
"optional": false,
"activeOutput": false
"optional": false
}
],
"annotation": "",
@@ -66,8 +65,7 @@
"tabular"
],
"type": "data",
"optional": false,
"activeOutput": false
"optional": false
}
],
"annotation": "",
@@ -109,8 +107,7 @@
],
"collection": true,
"collection_type": "list",
"optional": false,
"activeOutput": false
"optional": false
}
],
"annotation": "",
@@ -146,8 +143,7 @@
],
"collection": true,
"collection_type": "list:list",
"optional": false,
"activeOutput": false
"optional": false
}
],
"annotation": "",
@@ -183,8 +179,7 @@
],
"collection": true,
"collection_type": "paired",
"optional": false,
"activeOutput": false
"optional": false
}
],
"annotation": "",
@@ -216,8 +211,7 @@
"label": "integer parameter input",
"type": "integer",
"optional": false,
"parameter": true,
"activeOutput": false
"parameter": true
}
],
"annotation": "",
@@ -263,8 +257,7 @@
"tabular"
],
"type": "data",
"optional": false,
"activeOutput": false
"optional": false
}
],
"annotation": "",
@@ -319,8 +312,7 @@
"optional": false,
"collection": true,
"collection_type": null,
"collection_type_source": null,
"activeOutput": false
"collection_type_source": null
}
],
"annotation": "",
@@ -376,8 +368,7 @@
],
"type": "data",
"optional": false,
"label": "input dataset(s) (extracted element)",
"activeOutput": false
"label": "input dataset(s) (extracted element)"
}
],
"annotation": "",
@@ -416,8 +407,7 @@
"extensions": [
"input"
],
"optional": true,
"activeOutput": false
"optional": true
}
],
"annotation": "",
@@ -449,8 +439,7 @@
"label": "text parameter input",
"type": "text",
"optional": false,
"parameter": true,
"activeOutput": false
"parameter": true
}
],
"annotation": "",
@@ -497,8 +486,7 @@
"txt"
],
"type": "data",
"optional": false,
"activeOutput": false
"optional": false
}
],
"annotation": "",
@@ -564,8 +552,7 @@
"txt"
],
"type": "data",
"optional": false,
"activeOutput": false
"optional": false
}
],
"annotation": "",
@@ -618,8 +605,7 @@
"txt"
],
"type": "data",
"optional": false,
"activeOutput": false
"optional": false
}
],
"annotation": "",
@@ -680,8 +666,7 @@
"input"
],
"type": "data",
"optional": false,
"activeOutput": false
"optional": false
}
],
"annotation": "",
@@ -761,8 +746,7 @@
"txt"
],
"type": "data",
"optional": false,
"activeOutput": false
"optional": false
},
{
"name": "out2",
@@ -770,8 +754,7 @@
"txt"
],
"type": "data",
"optional": false,
"activeOutput": false
"optional": false
}
],
"annotation": "",
@@ -818,8 +801,7 @@
],
"collection": true,
"collection_type": "list:list:list",
"optional": false,
"activeOutput": false
"optional": false
}
],
"annotation": "",
@@ -19,8 +19,7 @@
"extensions": [
"input"
],
"optional": false,
"activeOutput": false
"optional": false
}
],
"annotation": "",
+12 -5
View File
@@ -41,7 +41,6 @@ export interface DataOutput {
extensions: string[];
name: string;
optional: boolean;
activeOutput?: boolean;
}
export interface CollectionOutput extends Omit<DataOutput, "type"> {
@@ -148,15 +147,20 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
getStepIndex(state: State) {
return Math.max(...Object.values(state.steps).map((step) => step.id), state.stepIndex);
},
hasActiveOutputs(state: State) {
return Boolean(Object.values(state.steps).find((step) => step.workflow_outputs?.length));
},
workflowOutputs(state: State) {
const workflowOutputs: WorkflowOutputs = {};
Object.values(state.steps).forEach((step) => {
if (step.workflow_outputs?.length) {
step.workflow_outputs.forEach((workflowOutput) => {
workflowOutputs[workflowOutput.label || workflowOutput.output_name] = {
outputName: workflowOutput.output_name,
stepId: step.id,
};
if (workflowOutput.label) {
workflowOutputs[workflowOutput.label] = {
outputName: workflowOutput.output_name,
stepId: step.id,
};
}
});
}
});
@@ -174,6 +178,9 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
return step;
},
updateStep(this: State, step: Step) {
step.workflow_outputs = step.workflow_outputs?.filter((workflowOutput) =>
step.outputs.find((output) => workflowOutput.output_name == output.name)
);
this.steps[step.id.toString()] = step;
},
changeStepMapOver(stepId: number, mapOver: CollectionTypeDescriptor) {
+8
View File
@@ -163,6 +163,14 @@
@extend .fa;
@extend .fa-check-square;
}
&.mark-terminal-visible {
@extend .fa;
@extend .fa-eye;
}
&.mark-terminal-hidden {
@extend .fa;
@extend .fa-eye-slash;
}
}
.delete-terminal {
@extend .btn;