diff --git a/client/src/components/Markdown/Markdown.vue b/client/src/components/Markdown/Markdown.vue
index edcd3abf4a01..4d15b58b7e42 100644
--- a/client/src/components/Markdown/Markdown.vue
+++ b/client/src/components/Markdown/Markdown.vue
@@ -74,15 +74,12 @@ import Vue from "vue";
import { useWorkflowStore } from "@/stores/workflowStore";
+import { splitMarkdown as splitMarkdownUnrendered } from "./parse";
+
import MarkdownContainer from "./MarkdownContainer.vue";
import LoadingSpan from "components/LoadingSpan.vue";
import StsDownloadButton from "components/StsDownloadButton.vue";
-const FUNCTION_VALUE_REGEX = `\\s*(?:[\\w_\\-]+|\\"[^\\"]+\\"|\\'[^\\']+\\')\\s*`;
-const FUNCTION_CALL = `\\s*[\\w\\|]+\\s*=` + FUNCTION_VALUE_REGEX;
-const FUNCTION_CALL_LINE = `\\s*(\\w+)\\s*\\(\\s*(?:(${FUNCTION_CALL})(,${FUNCTION_CALL})*)?\\s*\\)\\s*`;
-const FUNCTION_CALL_LINE_TEMPLATE = new RegExp(FUNCTION_CALL_LINE, "m");
-
const mdNewline = markdownItRegexp(/
/, () => {
return "
";
});
@@ -195,70 +192,15 @@ export default {
}
},
splitMarkdown(markdown) {
- const sections = [];
- let digest = markdown;
- while (digest.length > 0) {
- const galaxyStart = digest.indexOf("```galaxy");
- if (galaxyStart != -1) {
- const galaxyEnd = digest.substr(galaxyStart + 1).indexOf("```");
- if (galaxyEnd != -1) {
- if (galaxyStart > 0) {
- const defaultContent = digest.substr(0, galaxyStart).trim();
- if (defaultContent) {
- sections.push({
- name: "default",
- content: md.render(defaultContent),
- });
- }
- }
- const galaxyEndIndex = galaxyEnd + 4;
- const galaxySection = digest.substr(galaxyStart, galaxyEndIndex);
- let args = null;
- try {
- args = this.getArgs(galaxySection);
- sections.push(args);
- } catch (e) {
- this.markdownErrors.push({
- error: "Found an unresolved tag.",
- line: galaxySection,
- });
- }
- digest = digest.substr(galaxyStart + galaxyEndIndex);
- } else {
- digest = digest.substr(galaxyStart + 1);
- }
- } else {
- sections.push({
- name: "default",
- content: md.render(digest),
- });
- break;
+ const { sections, markdownErrors } = splitMarkdownUnrendered(markdown);
+ markdownErrors.forEach((error) => markdownErrors.push(error));
+ sections.forEach((section) => {
+ if (section.name == "default") {
+ section.content = md.render(section.content);
}
- }
+ });
return sections;
},
- getArgs(content) {
- const galaxy_function = FUNCTION_CALL_LINE_TEMPLATE.exec(content);
- const args = {};
- const function_name = galaxy_function[1];
- // we need [... ] to return empty string, if regex doesn't match
- const function_arguments = [...content.matchAll(new RegExp(FUNCTION_CALL, "g"))];
- for (let i = 0; i < function_arguments.length; i++) {
- if (function_arguments[i] === undefined) {
- continue;
- }
- const arguments_str = function_arguments[i].toString().replace(/,/g, "").trim();
- if (arguments_str) {
- const [key, val] = arguments_str.split("=");
- args[key.trim()] = val.replace(/['"]+/g, "").trim();
- }
- }
- return {
- name: function_name,
- args: args,
- content: content,
- };
- },
},
};
diff --git a/client/src/components/Markdown/parse.test.js b/client/src/components/Markdown/parse.test.js
new file mode 100644
index 000000000000..50b1ae99397d
--- /dev/null
+++ b/client/src/components/Markdown/parse.test.js
@@ -0,0 +1,175 @@
+import { getArgs, replaceLabel, splitMarkdown } from "./parse";
+
+describe("parse.ts", () => {
+ describe("getArgs", () => {
+ it("parses simple directive expression", () => {
+ const args = getArgs("job_metrics(job_id=THISFAKEID)");
+ expect(args.name).toBe("job_metrics");
+ });
+
+ it("parses labels spaces at the end with single quotes", () => {
+ const args = getArgs("job_metrics(step=' fakestepname ')");
+ expect(args.name).toBe("job_metrics");
+ expect(args.args.step).toBe(" fakestepname ");
+ });
+
+ it("parses labels spaces at the end with double quotes", () => {
+ const args = getArgs('job_metrics(step=" fakestepname ")');
+ expect(args.name).toBe("job_metrics");
+ expect(args.args.step).toBe(" fakestepname ");
+ });
+ });
+
+ describe("splitMarkdown", () => {
+ it("strip leading whitespace by default", () => {
+ const { sections } = splitMarkdown("\n```galaxy\njob_metrics(job_id=THISFAKEID)\n```");
+ expect(sections.length).toBe(1);
+ });
+
+ it("should not strip leading whitespace if disabled", () => {
+ const { sections } = splitMarkdown("\n```galaxy\njob_metrics(job_id=THISFAKEID)\n```", true);
+ expect(sections.length).toBe(2);
+ expect(sections[0].content).toBe("\n");
+ });
+
+ it("should parse labels with leading spaces", () => {
+ const { sections } = splitMarkdown("\n```galaxy\njob_metrics(step='THISFAKEID ')\n```", true);
+ expect(sections.length).toBe(2);
+ expect(sections[0].content).toBe("\n");
+ });
+ });
+
+ describe("replaceLabel", () => {
+ it("should leave unaffected markdown alone", () => {
+ const input = "some random\n`markdown content`\n";
+ const result = replaceLabel(input, "output", "from", "to");
+ expect(result).toBe(result);
+ });
+
+ it("should leave unaffected galaxy directives alone", () => {
+ const input = "some random\n`markdown content`\n```galaxy\ncurrent_time()\n```\n";
+ const result = replaceLabel(input, "output", "from", "to");
+ expect(result).toBe(result);
+ });
+
+ it("should leave galaxy directives of same type with other labels alone", () => {
+ const input = "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(output=moo)\n```\n";
+ const result = replaceLabel(input, "output", "from", "to");
+ expect(result).toBe(result);
+ });
+
+ it("should leave galaxy directives of other types with same labels alone", () => {
+ const input = "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(input=from)\n```\n";
+ const result = replaceLabel(input, "output", "from", "to");
+ expect(result).toBe(result);
+ });
+
+ it("should swap simple directives of specified type", () => {
+ const input = "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(output=from)\n```\n";
+ const output = "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(output=to)\n```\n";
+ const result = replaceLabel(input, "output", "from", "to");
+ expect(result).toBe(output);
+ });
+
+ it("should swap single quoted directives of specified type", () => {
+ const input = "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(output='from')\n```\n";
+ const output = "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(output=to)\n```\n";
+ const result = replaceLabel(input, "output", "from", "to");
+ expect(result).toBe(output);
+ });
+
+ it("should swap single quoted directives of specified type with extra args", () => {
+ const input =
+ "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output='from', title=dog)\n```\n";
+ const output =
+ "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output=to, title=dog)\n```\n";
+ const result = replaceLabel(input, "output", "from", "to");
+ expect(result).toBe(output);
+ });
+
+ it("should swap double quoted directives of specified type", () => {
+ const input = 'some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(output="from")\n```\n';
+ const output = "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(output=to)\n```\n";
+ const result = replaceLabel(input, "output", "from", "to");
+ expect(result).toBe(output);
+ });
+
+ it("should swap double quoted directives of specified type with extra args", () => {
+ const input =
+ "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output=\"from\", title=dog)\n```\n";
+ const output =
+ "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output=to, title=dog)\n```\n";
+ const result = replaceLabel(input, "output", "from", "to");
+ expect(result).toBe(output);
+ });
+
+ it("should work with double quotes for labels and spaces in the quotes", () => {
+ const input =
+ "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output=\"from this\", title=dog)\n```\n";
+ const output =
+ "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output=\"to that\", title=dog)\n```\n";
+ const result = replaceLabel(input, "output", "from this", "to that");
+ expect(result).toBe(output);
+ });
+
+ it("should work with single quotes for labels and spaces in the quotes", () => {
+ const input =
+ "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output='from this', title=dog)\n```\n";
+ const output =
+ "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output='to that', title=dog)\n```\n";
+ const result = replaceLabel(input, "output", "from this", "to that");
+ expect(result).toBe(output);
+ });
+
+ it("should work with single quotes for labels and spaces in the quotes including end", () => {
+ const input =
+ "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output='from this ', title=dog)\n```\n";
+ const output =
+ "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output='to thatx ', title=dog)\n```\n";
+ const result = replaceLabel(input, "output", "from this ", "to thatx ");
+ expect(result).toBe(output);
+ });
+
+ it("should add quotes when refactoring to labels with spaces", () => {
+ const input =
+ "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output=fromthis, title=dog)\n```\n";
+ const output =
+ "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output='to that', title=dog)\n```\n";
+ const result = replaceLabel(input, "output", "fromthis", "to that");
+ expect(result).toBe(output);
+ });
+
+ it("should add quotes when refactoring to labels with spaces including end space", () => {
+ const input =
+ "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output=fromthis, title=dog)\n```\n";
+ const output =
+ "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(footer='cow', output='to that ', title=dog)\n```\n";
+ const result = replaceLabel(input, "output", "fromthis", "to that ");
+ expect(result).toBe(output);
+ });
+
+ it("should leave non-arguments alone", () => {
+ const input =
+ "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(title='cow from farm', output=from)\n```\n";
+ const output =
+ "some random\n`markdown content`\n```galaxy\nhistory_dataset_embedded(title='cow from farm', output=to)\n```\n";
+ const result = replaceLabel(input, "output", "from", "to");
+ expect(result).toBe(output);
+ });
+
+ // not a valid workflow label per se but make sure we're escaping the regex to be safe
+ it("should not be messed up by labels containing regexp content", () => {
+ const input = "```galaxy\nhistory_dataset_embedded(output='from(')\n```\n";
+ const output = "```galaxy\nhistory_dataset_embedded(output=to$1)\n```\n";
+ const result = replaceLabel(input, "output", "from(", "to$1");
+ expect(result).toBe(output);
+ });
+
+ it("should not swallow leading newlines", () => {
+ const input = "\n```galaxy\nhistory_dataset_embedded(output='from')\n```\n";
+ const output = "\n```galaxy\nhistory_dataset_embedded(output=to)\n```\n";
+ const result = replaceLabel(input, "output", "from", "to");
+ expect(result).toBe(output);
+ });
+ });
+});
diff --git a/client/src/components/Markdown/parse.ts b/client/src/components/Markdown/parse.ts
new file mode 100644
index 000000000000..f7d5b17cd562
--- /dev/null
+++ b/client/src/components/Markdown/parse.ts
@@ -0,0 +1,170 @@
+const FUNCTION_ARGUMENT_VALUE_REGEX = `\\s*(?:[\\w_\\-]+|\\"[^\\"]+\\"|\\'[^\\']+\\')\\s*`;
+const FUNCTION_ARGUMENT_VALUE_TO_VALUE_REGEX = `\\s*(?:\\"(?[^\\"]+)\\"|\\'(?[^\\']+)\\'|(?[\\w_\\-]+))\\s*`;
+const FUNCTION_ARGUMENT_REGEX = `\\s*[\\w\\|]+\\s*=` + FUNCTION_ARGUMENT_VALUE_REGEX;
+const FUNCTION_CALL_LINE = `\\s*(\\w+)\\s*\\(\\s*(?:(${FUNCTION_ARGUMENT_REGEX})(,${FUNCTION_ARGUMENT_REGEX})*)?\\s*\\)\\s*`;
+const FUNCTION_CALL_LINE_TEMPLATE = new RegExp(FUNCTION_CALL_LINE, "m");
+
+type DefaultSection = { name: "default"; content: string };
+type GalaxyDirectiveSection = { name: string; content: string; args: { [key: string]: string } };
+type Section = DefaultSection | GalaxyDirectiveSection;
+
+type WorkflowLabelKind = "input" | "output" | "step";
+
+const SINGLE_QUOTE = "'";
+const DOUBLE_QUOTE = '"';
+
+export function splitMarkdown(markdown: string, preserveWhitespace: boolean = false) {
+ const sections: Section[] = [];
+ const markdownErrors = [];
+ let digest = markdown;
+ while (digest.length > 0) {
+ const galaxyStart = digest.indexOf("```galaxy");
+ if (galaxyStart != -1) {
+ const galaxyEnd = digest.substr(galaxyStart + 1).indexOf("```");
+ if (galaxyEnd != -1) {
+ if (galaxyStart > 0) {
+ const rawContent = digest.substr(0, galaxyStart);
+ const defaultContent = rawContent.trim();
+ if (preserveWhitespace || defaultContent) {
+ sections.push({
+ name: "default",
+ content: preserveWhitespace ? rawContent : defaultContent,
+ });
+ }
+ }
+ const galaxyEndIndex = galaxyEnd + 4;
+ const galaxySection = digest.substr(galaxyStart, galaxyEndIndex);
+ let args = null;
+ try {
+ args = getArgs(galaxySection);
+ sections.push(args);
+ } catch (e) {
+ markdownErrors.push({
+ error: "Found an unresolved tag.",
+ line: galaxySection,
+ });
+ }
+ digest = digest.substr(galaxyStart + galaxyEndIndex);
+ } else {
+ digest = digest.substr(galaxyStart + 1);
+ }
+ } else {
+ sections.push({
+ name: "default",
+ content: digest,
+ });
+ break;
+ }
+ }
+ return { sections, markdownErrors };
+}
+
+export function replaceLabel(
+ markdown: string,
+ labelType: WorkflowLabelKind,
+ fromLabel: string,
+ toLabel: string
+): string {
+ const { sections } = splitMarkdown(markdown, true);
+
+ function rewriteSection(section: Section) {
+ if ("args" in section) {
+ const directiveSection = section as GalaxyDirectiveSection;
+ const args = directiveSection.args;
+ if (!(labelType in args)) {
+ return section;
+ }
+ const labelValue = args[labelType];
+ if (labelValue != fromLabel) {
+ return section;
+ }
+ // we've got a section with a matching label and type...
+ const newArgs = { ...args };
+ newArgs[labelType] = toLabel;
+ const argRexExp = namedArgumentRegex(labelType);
+ let escapedToLabel = escapeRegExpReplacement(toLabel);
+ const incomingContent = directiveSection.content;
+ let content: string;
+ const match = incomingContent.match(argRexExp);
+ if (match) {
+ const firstMatch = match[0];
+ // TODO: handle whitespace more broadly here...
+ if (escapedToLabel.indexOf(" ") >= 0) {
+ const quoteChar = getQuoteChar(firstMatch);
+ escapedToLabel = `${quoteChar}${escapedToLabel}${quoteChar}`;
+ }
+ content = incomingContent.replace(argRexExp, `$1${escapedToLabel}`);
+ } else {
+ content = incomingContent;
+ }
+ return {
+ name: directiveSection.name,
+ args: newArgs,
+ content: content,
+ };
+ } else {
+ return section;
+ }
+ }
+
+ const rewrittenSections = sections.map(rewriteSection);
+ const rewrittenMarkdown = rewrittenSections.map((section) => section.content).join("");
+ return rewrittenMarkdown;
+}
+
+function getQuoteChar(argMatch: string): string {
+ // this could be a lot stronger, handling escaping and such...
+ let quoteChar = SINGLE_QUOTE;
+ if (argMatch.indexOf(DOUBLE_QUOTE) >= 0) {
+ quoteChar = DOUBLE_QUOTE;
+ }
+ return quoteChar;
+}
+
+export function getArgs(content: string): GalaxyDirectiveSection {
+ const galaxy_function = FUNCTION_CALL_LINE_TEMPLATE.exec(content);
+ if (galaxy_function == null) {
+ throw Error("Failed to parse galaxy directive");
+ }
+ type ArgsType = { [key: string]: string };
+ const args: ArgsType = {};
+ const function_name = galaxy_function[1] as string;
+ // we need [... ] to return empty string, if regex doesn't match
+ const function_arguments = [...content.matchAll(new RegExp(FUNCTION_ARGUMENT_REGEX, "g"))];
+ for (let i = 0; i < function_arguments.length; i++) {
+ if (function_arguments[i] === undefined) {
+ continue;
+ }
+ const arguments_str = function_arguments[i]?.toString().replace(/,/g, "").trim();
+ if (arguments_str) {
+ const [keyStr, valStr] = arguments_str.split("=");
+ if (keyStr == undefined || valStr == undefined) {
+ throw Error("Failed to parse galaxy directive");
+ }
+ const key = keyStr.trim();
+ let val: string = valStr?.trim() ?? "";
+ if (val) {
+ const strippedValueMatch = val.match(FUNCTION_ARGUMENT_VALUE_TO_VALUE_REGEX);
+ const groups = strippedValueMatch?.groups;
+ if (groups) {
+ val = groups.unquoted ?? groups.squoted ?? groups.dquoted ?? val;
+ }
+ }
+ args[key] = val;
+ }
+ }
+ return {
+ name: function_name,
+ args: args,
+ content: content,
+ };
+}
+
+function namedArgumentRegex(argument: string): RegExp {
+ return new RegExp(`(\\s*${argument}\\s*=)` + FUNCTION_ARGUMENT_VALUE_REGEX);
+}
+
+// https://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex
+function escapeRegExpReplacement(value: string): string {
+ return value.replace(/\$/g, "$$$$");
+}
diff --git a/client/src/components/Workflow/Editor/Forms/FormDefault.vue b/client/src/components/Workflow/Editor/Forms/FormDefault.vue
index 8ec5d71f984f..72e786d3b6d3 100644
--- a/client/src/components/Workflow/Editor/Forms/FormDefault.vue
+++ b/client/src/components/Workflow/Editor/Forms/FormDefault.vue
@@ -51,7 +51,8 @@
:key="index"
:name="output.name"
:step="step"
- :show-details="true" />
+ :show-details="true"
+ @onOutputLabel="onOutputLabel" />
@@ -78,7 +79,14 @@ const props = defineProps<{
step: Step;
datatypes: DatatypesMapperModel["datatypes"];
}>();
-const emit = defineEmits(["onAnnotation", "onLabel", "onAttemptRefactor", "onEditSubworkflow", "onSetData"]);
+const emit = defineEmits([
+ "onAnnotation",
+ "onLabel",
+ "onAttemptRefactor",
+ "onEditSubworkflow",
+ "onSetData",
+ "onOutputLabel",
+]);
const stepRef = toRef(props, "step");
const { stepId, contentId, annotation, label, name, type, configForm } = useStepProps(stepRef);
const { stepStore } = useWorkflowStores();
@@ -117,4 +125,7 @@ function onChange(values: any) {
inputs: values,
});
}
+function onOutputLabel(oldValue: string | null, newValue: string | null) {
+ emit("onOutputLabel", oldValue, newValue);
+}
diff --git a/client/src/components/Workflow/Editor/Forms/FormOutput.vue b/client/src/components/Workflow/Editor/Forms/FormOutput.vue
index a0e41c37eb40..554b218df6ef 100644
--- a/client/src/components/Workflow/Editor/Forms/FormOutput.vue
+++ b/client/src/components/Workflow/Editor/Forms/FormOutput.vue
@@ -1,7 +1,7 @@
-
+
();
+
const { stepStore } = useWorkflowStores();
const error: Ref = ref(undefined);
@@ -54,12 +58,14 @@ function onInput(newLabel: string | undefined | null) {
}
if (newLabel === "") {
+ const oldLabel = label.value || null;
// user deleted existing label, we inactivate this as output
const strippedWorkflowOutputs = (props.step.workflow_outputs || []).filter(
(workflowOutput) => workflowOutput.output_name !== props.name
);
stepStore.updateStep({ ...props.step, workflow_outputs: strippedWorkflowOutputs });
error.value = undefined;
+ emit("onOutputLabel", oldLabel, null);
return;
}
@@ -74,6 +80,8 @@ function onInput(newLabel: string | undefined | null) {
output_name: props.name,
});
stepStore.updateStep({ ...props.step, workflow_outputs: newWorkflowOutputs });
+ const oldLabel = label.value || null;
+ emit("onOutputLabel", oldLabel, newLabel || null);
error.value = undefined;
} else if (existingWorkflowOutput.stepId !== props.step.id) {
error.value = `Duplicate output label '${newLabel}' will be ignored.`;
diff --git a/client/src/components/Workflow/Editor/Forms/FormSection.vue b/client/src/components/Workflow/Editor/Forms/FormSection.vue
index 8c6837f82f71..d7b96dfc9591 100644
--- a/client/src/components/Workflow/Editor/Forms/FormSection.vue
+++ b/client/src/components/Workflow/Editor/Forms/FormSection.vue
@@ -23,6 +23,7 @@
:datatypes="datatypes"
:form-data="formData"
@onInput="onInput"
+ @onOutputLabel="onOutputLabel"
@onDatatype="onDatatype" />
@@ -148,6 +149,9 @@ export default {
delete pjas[this.emailPayloadKey];
}
},
+ onOutputLabel(oldValue, newValue) {
+ this.$emit("onOutputLabel", oldValue, newValue);
+ },
onInput(value, pjaKey) {
let changed = false;
const exists = pjaKey in this.formData;
diff --git a/client/src/components/Workflow/Editor/Forms/FormTool.vue b/client/src/components/Workflow/Editor/Forms/FormTool.vue
index 0a10578d2e8d..919cbad7a7b0 100644
--- a/client/src/components/Workflow/Editor/Forms/FormTool.vue
+++ b/client/src/components/Workflow/Editor/Forms/FormTool.vue
@@ -46,6 +46,7 @@
:step="step"
:datatypes="datatypes"
:post-job-actions="postJobActions"
+ @onOutputLabel="onOutputLabel"
@onChange="onChangePostJobActions" />
@@ -88,7 +89,7 @@ export default {
required: true,
},
},
- emits: ["onSetData", "onUpdateStep", "onChangePostJobActions", "onAnnotation", "onLabel"],
+ emits: ["onSetData", "onUpdateStep", "onChangePostJobActions", "onAnnotation", "onLabel", "onOutputLabel"],
setup(props, { emit }) {
const { stepId, annotation, label, stepInputs, stepOutputs, configForm, postJobActions } = useStepProps(
toRef(props, "step")
@@ -163,6 +164,9 @@ export default {
onLabel(newLabel) {
this.$emit("onLabel", this.stepId, newLabel);
},
+ onOutputLabel(oldValue, newValue) {
+ this.$emit("onOutputLabel", oldValue, newValue);
+ },
/**
* Change event is triggered on component creation and input changes.
* @param { Object } values contains flat key-value pairs `prefixed-name=value`
diff --git a/client/src/components/Workflow/Editor/Index.vue b/client/src/components/Workflow/Editor/Index.vue
index a283e5b863b2..b045017acc9c 100644
--- a/client/src/components/Workflow/Editor/Index.vue
+++ b/client/src/components/Workflow/Editor/Index.vue
@@ -95,6 +95,7 @@
@onChangePostJobActions="onChangePostJobActions"
@onAnnotation="onAnnotation"
@onLabel="onLabel"
+ @onOutputLabel="onOutputLabel"
@onUpdateStep="onUpdateStep"
@onSetData="onSetData" />
= 0;
+ const labelType = isInput ? "input" : "step";
+ const labelTypeTitle = isInput ? "Input" : "Step";
+ const newMarkdown = replaceLabel(this.markdownText, labelType, oldLabel, newLabel);
+ if (newMarkdown !== this.markdownText) {
+ this.debouncedToast(`${labelTypeTitle} label updated in workflow report.`, 1500);
+ }
+ this.onReportUpdate(newMarkdown);
+ },
+ debouncedToast(message, delay) {
+ clearTimeout(this.debounceTimer);
+ this.debounceTimer = setTimeout(() => Toast.success(message), delay);
},
onScrollTo(stepId) {
this.scrollToId = stepId;