diff --git a/client/src/components/Form/Elements/FormData/FormData.vue b/client/src/components/Form/Elements/FormData/FormData.vue
index f6054f28cd00..d537016f6cb4 100644
--- a/client/src/components/Form/Elements/FormData/FormData.vue
+++ b/client/src/components/Form/Elements/FormData/FormData.vue
@@ -17,6 +17,7 @@ import { BATCH, SOURCE, VARIANTS } from "./variants";
import FormSelection from "../FormSelection.vue";
import FormSelect from "@/components/Form/Elements/FormSelect.vue";
+import HelpText from "@/components/Help/HelpText.vue";
library.add(faCopy, faFile, faFolder, faCaretDown, faCaretUp, faExclamation, faLink, faUnlink);
@@ -650,7 +651,11 @@ const noOptionsWarningMessage = computed(() => {
-
+
+ The supplied input will be this
+ tool.
+
+
This is a batch mode input field. Individual jobs will be triggered for each dataset.
diff --git a/client/src/components/Help/HelpPopover.vue b/client/src/components/Help/HelpPopover.vue
new file mode 100644
index 000000000000..11ded1f423de
--- /dev/null
+++ b/client/src/components/Help/HelpPopover.vue
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/client/src/components/Help/HelpTerm.vue b/client/src/components/Help/HelpTerm.vue
new file mode 100644
index 000000000000..92db6966591d
--- /dev/null
+++ b/client/src/components/Help/HelpTerm.vue
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+ Something went wrong, no Galaxy help found for term or URI {{ term }}.
+
+
+
diff --git a/client/src/components/Help/HelpText.vue b/client/src/components/Help/HelpText.vue
index f3f1b4740af1..f5f6a7006295 100644
--- a/client/src/components/Help/HelpText.vue
+++ b/client/src/components/Help/HelpText.vue
@@ -1,41 +1,24 @@
-
-
-
- {{ text }}
- {{ text }}
+ :term="uri" />
+ {{ text }}
diff --git a/client/src/components/Help/terms.yml b/client/src/components/Help/terms.yml
index a6df4b852a63..6391c7979e18 100644
--- a/client/src/components/Help/terms.yml
+++ b/client/src/components/Help/terms.yml
@@ -29,6 +29,31 @@ unix:
More information on stack traces can be found on [Wikipedia](https://en.wikipedia.org/wiki/Stack_trace).
galaxy:
+ collections:
+ flatList: |
+ A flat list is just a simple dataset collection of type ``list`` that contains only datasets and not
+ other collections.
+ mapOver: |
+ When a tool consumes a dataset but is run with a collection, the collection *maps over* the collection.
+ This means instead of just running the tool once - the tool will be run once for each element of the
+ provided collection. Additionally, the outputs of the tool will be collected into a collection that
+ matches the structure of the provided collection. This matching structure means the output collections
+ will have the same element identifiers as the provided collection and they will appear in the same order.
+
+ It is easiest to visualize "mapping over" a collection is in the context of a tool that consumes a dataset
+ and produces a dataset, but the semantics apply rather naturally to tools that consume collections or
+ produce collections as well.
+
+ For instance, consider a tool that consumes a ``paired`` collection and produces an output dataset.
+ If a list of paired collections (collection type ``list:paired``) is passed to the tool - it will
+ will produce a flat list (collection type ``list``) of output datasets with the same number of elements
+ in the same order as the provided list of ``paired`` collections.
+
+ In the case of outputs, consider a tool that takes in a dataset and produces a flat list. If this tool
+ is run over a flat list of datasets - that list will be "mapped over" and each element will produce a list.
+ These lists will be gathered together in a nested list structured (collection type ``list:list``) where
+ the outer element count and structure matches that of the input and the inner list for each of those
+ is just the outputs of the tool for the corresponding element of the input.
jobs:
states:
# upload, waiting, failed, paused, deleting, deleted, stop, stopped, skipped.
diff --git a/client/src/components/Tool/ToolCard.vue b/client/src/components/Tool/ToolCard.vue
index d28e4d46ad96..6c0876d3ac8c 100644
--- a/client/src/components/Tool/ToolCard.vue
+++ b/client/src/components/Tool/ToolCard.vue
@@ -186,7 +186,7 @@ const showHelpForum = computed(() => isConfigLoaded.value && config.value.enable
Help
-
+
-import { useFormattedToolHelp } from "composables/formattedToolHelp";
-
-const props = defineProps({
- content: {
- type: String,
- required: true,
- },
-});
-
-const { formattedContent } = useFormattedToolHelp(props.content);
+
-
-
+
+
+
+
+ {{ content }}
+
+
-
-
diff --git a/client/src/components/Tool/ToolHelpMarkdown.vue b/client/src/components/Tool/ToolHelpMarkdown.vue
new file mode 100644
index 000000000000..a96aebe73232
--- /dev/null
+++ b/client/src/components/Tool/ToolHelpMarkdown.vue
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/client/src/components/Tool/ToolHelp.test.js b/client/src/components/Tool/ToolHelpRst.test.js
similarity index 87%
rename from client/src/components/Tool/ToolHelp.test.js
rename to client/src/components/Tool/ToolHelpRst.test.js
index afbd19d4ba92..40f8e95f2d1e 100644
--- a/client/src/components/Tool/ToolHelp.test.js
+++ b/client/src/components/Tool/ToolHelpRst.test.js
@@ -1,7 +1,7 @@
import { mount } from "@vue/test-utils";
import { getLocalVue } from "tests/jest/helpers";
-import ToolHelp from "./ToolHelp";
+import ToolHelpRst from "./ToolHelpRst";
const localVue = getLocalVue();
@@ -25,9 +25,9 @@ const expectedHelpText = `
h4 Heading
empty link`;
-describe("ToolHelp", () => {
+describe("ToolHelp RST", () => {
it("modifies help text", () => {
- const wrapper = mount(ToolHelp, {
+ const wrapper = mount(ToolHelpRst, {
propsData: {
content: inputHelpText,
},
diff --git a/client/src/components/Tool/ToolHelpRst.vue b/client/src/components/Tool/ToolHelpRst.vue
new file mode 100644
index 000000000000..88420b8b2ee6
--- /dev/null
+++ b/client/src/components/Tool/ToolHelpRst.vue
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
diff --git a/client/src/entry/analysis/router.js b/client/src/entry/analysis/router.js
index e3bf947b2ce6..0128829043b9 100644
--- a/client/src/entry/analysis/router.js
+++ b/client/src/entry/analysis/router.js
@@ -7,6 +7,7 @@ import DatasetAttributes from "components/DatasetInformation/DatasetAttributes";
import DatasetDetails from "components/DatasetInformation/DatasetDetails";
import DatasetError from "components/DatasetInformation/DatasetError";
import FormGeneric from "components/Form/FormGeneric";
+import HelpTerm from "components/Help/HelpTerm";
import HistoryExportTasks from "components/History/Export/HistoryExport";
import HistoryPublished from "components/History/HistoryPublished";
import HistoryView from "components/History/HistoryView";
@@ -195,6 +196,11 @@ export function getRouter(Galaxy) {
path: "about",
component: AboutGalaxy,
},
+ {
+ path: "help/terms/:term",
+ component: HelpTerm,
+ props: true,
+ },
{
path: "carbon_emissions_calculations",
component: CarbonEmissionsCalculations,
diff --git a/client/src/stores/helpTermsStore.ts b/client/src/stores/helpTermsStore.ts
new file mode 100644
index 000000000000..349ca4879802
--- /dev/null
+++ b/client/src/stores/helpTermsStore.ts
@@ -0,0 +1,129 @@
+import { defineStore } from "pinia";
+import { computed, type Ref, ref } from "vue";
+
+import { hasHelp as hasHelpTextFromYaml, help as helpTextFromYaml } from "@/components/Help/terms";
+
+import { useDatatypeStore } from "./datatypeStore";
+
+interface DatatypeDescription {
+ ext: string;
+ description: string | null;
+ descriptionUrl: string | null;
+}
+
+interface RawDatatypeDescription {
+ id: string;
+ text: string;
+ description: string | unknown | null;
+ description_url: string | unknown | null;
+ upload_warning: string | unknown | null;
+}
+
+export const useHelpTermsStore = defineStore("helpTermsStore", () => {
+ const initialized = ref(false);
+ const datatypeDescriptions = ref(null as DatatypeDescription[] | null);
+ const datatypeStore = useDatatypeStore();
+
+ function datatypeDescriptionForExtension(extension: string): DatatypeDescription | null {
+ if (!datatypeDescriptions.value) {
+ return null;
+ }
+ for (const description of datatypeDescriptions.value) {
+ if (description.ext == extension) {
+ return description;
+ }
+ }
+ return null;
+ }
+
+ function moreInformationFromUrlMarkdown(ext: string, url: string) {
+ return `More information on the datatype ${ext} can be found at [${url}](${url}).`;
+ }
+
+ function datatypeDescriptionToMarkdown(datatypeDescription: DatatypeDescription): string {
+ const ext = datatypeDescription.ext;
+ let description = datatypeDescription.description?.trimEnd();
+ const url = datatypeDescription.descriptionUrl;
+ if (!description && !url) {
+ return `${ext} is a registered Galaxy datatype.`;
+ } else if (!description) {
+ return moreInformationFromUrlMarkdown(ext, url as string);
+ } else if (!url) {
+ return description;
+ } else {
+ if (description.charAt(description.length - 1) != ".") {
+ description += ".";
+ }
+ return `${description}\n\n${moreInformationFromUrlMarkdown(ext, url)}`;
+ }
+ }
+
+ async function ensureInitialized() {
+ if (!initialized.value) {
+ await datatypeStore.fetchUploadDatatypes();
+ const rawDatatypes = datatypeStore.getUploadDatatypes as RawDatatypeDescription[];
+ datatypeDescriptions.value = rawDatatypes.map((datatype: RawDatatypeDescription) => {
+ return {
+ ext: datatype.id,
+ description: datatype.description || null,
+ descriptionUrl: datatype.description_url || null,
+ } as DatatypeDescription;
+ });
+ initialized.value = true;
+ }
+ }
+
+ const loading = computed(() => {
+ return !initialized.value;
+ });
+
+ function hasHelpText(term: string): boolean {
+ if (term.startsWith("galaxy.datatypes.extensions.")) {
+ const extension = term.substring("galaxy.datatypes.extensions.".length);
+ return datatypeDescriptionForExtension(extension) != null;
+ } else {
+ return hasHelpTextFromYaml(term);
+ }
+ }
+
+ function helpText(term: string): string | null {
+ if (term.startsWith("galaxy.datatypes.extensions.")) {
+ const extension = term.substring("galaxy.datatypes.extensions.".length);
+ const description = datatypeDescriptionForExtension(extension);
+ if (!description) {
+ return null;
+ }
+ return datatypeDescriptionToMarkdown(description);
+ } else {
+ return helpTextFromYaml(term);
+ }
+ }
+
+ return {
+ ensureInitialized,
+ hasHelpText,
+ helpText,
+ loading,
+ };
+});
+
+export function useHelpForTerm(uri: Ref) {
+ const termsStore = useHelpTermsStore();
+ termsStore.ensureInitialized();
+
+ const loading = computed(() => {
+ return termsStore.loading;
+ });
+ const hasHelp = computed(() => {
+ return termsStore.hasHelpText(uri.value);
+ });
+ const help = computed(() => {
+ return termsStore.helpText(uri.value);
+ });
+
+ return {
+ loading,
+ hasHelp,
+ help,
+ };
+}
diff --git a/lib/galaxy/tool_util/linters/cwl.py b/lib/galaxy/tool_util/linters/cwl.py
index f4e7f367ba56..c72bd433f0aa 100644
--- a/lib/galaxy/tool_util/linters/cwl.py
+++ b/lib/galaxy/tool_util/linters/cwl.py
@@ -89,5 +89,5 @@ class CWLHelpTODO(Linter):
@classmethod
def lint(cls, tool_source: "ToolSource", lint_ctx: "LintContext"):
help = tool_source.parse_help()
- if help and "TODO" in help:
+ if help and "TODO" in help.content:
lint_ctx.warn("Help contains TODO text.")
diff --git a/lib/galaxy/tool_util/parser/cwl.py b/lib/galaxy/tool_util/parser/cwl.py
index 571a7faa4172..6891dd9590c6 100644
--- a/lib/galaxy/tool_util/parser/cwl.py
+++ b/lib/galaxy/tool_util/parser/cwl.py
@@ -11,6 +11,7 @@
)
from galaxy.tool_util.deps import requirements
from .interface import (
+ HelpContent,
PageSource,
PagesSource,
ToolSource,
@@ -80,7 +81,11 @@ def parse_edam_topics(self):
return []
def parse_help(self):
- return self.tool_proxy.doc()
+ doc = self.tool_proxy.doc()
+ if doc:
+ return HelpContent(type="plain_text", content=doc)
+ else:
+ return None
def parse_sanitize(self):
return False
diff --git a/lib/galaxy/tool_util/parser/interface.py b/lib/galaxy/tool_util/parser/interface.py
index a7b3896c289f..f0f06103e80d 100644
--- a/lib/galaxy/tool_util/parser/interface.py
+++ b/lib/galaxy/tool_util/parser/interface.py
@@ -121,6 +121,11 @@ class Citation(BaseModel):
content: str
+class HelpContent(BaseModel):
+ type: Literal["restructuredtext", "plain_text", "markdown"]
+ content: str
+
+
class ToolSource(metaclass=ABCMeta):
"""This interface represents an abstract source to parse tool
information from.
@@ -333,9 +338,11 @@ def parse_stdio(self):
return [], []
@abstractmethod
- def parse_help(self) -> Optional[str]:
- """Return RST definition of help text for tool or None if the tool
- doesn't define help text.
+ def parse_help(self) -> Optional[HelpContent]:
+ """Return help text for tool or None if the tool doesn't define help text.
+
+ The returned object contains the help text and an indication if it ``rst``
+ or ``markdown``.
"""
@abstractmethod
diff --git a/lib/galaxy/tool_util/parser/xml.py b/lib/galaxy/tool_util/parser/xml.py
index 9ab4a30f65b6..611351380013 100644
--- a/lib/galaxy/tool_util/parser/xml.py
+++ b/lib/galaxy/tool_util/parser/xml.py
@@ -41,6 +41,7 @@
DrillDownDynamicOptions,
DrillDownOptionsDict,
DynamicOptions,
+ HelpContent,
InputSource,
PageSource,
PagesSource,
@@ -649,9 +650,14 @@ def parse_strict_shell(self):
else:
return string_as_bool(default)
- def parse_help(self):
+ def parse_help(self) -> Optional[HelpContent]:
help_elem = self.root.find("help")
- return help_elem.text if help_elem is not None else None
+ if help_elem is None:
+ return None
+
+ help_type = help_elem.get("type", "restructuredtext")
+ content = help_elem.text or ''
+ return HelpContent(type=help_type, content=content)
@property
def macro_paths(self):
diff --git a/lib/galaxy/tool_util/parser/yaml.py b/lib/galaxy/tool_util/parser/yaml.py
index 6813db0f5211..d3ac11775c55 100644
--- a/lib/galaxy/tool_util/parser/yaml.py
+++ b/lib/galaxy/tool_util/parser/yaml.py
@@ -19,6 +19,7 @@
from .interface import (
AssertionDict,
AssertionList,
+ HelpContent,
InputSource,
PageSource,
PagesSource,
@@ -124,8 +125,12 @@ def parse_strict_shell(self):
def parse_stdio(self):
return error_on_exit_code()
- def parse_help(self):
- return self.root_dict.get("help", None)
+ def parse_help(self) -> Optional[HelpContent]:
+ content = self.root_dict.get("help", None)
+ if content:
+ return HelpContent(type="markdown", content=content)
+ else:
+ return None
def parse_outputs(self, tool):
outputs = self.root_dict.get("outputs", {})
diff --git a/lib/galaxy/tool_util/xsd/galaxy.xsd b/lib/galaxy/tool_util/xsd/galaxy.xsd
index 7c17312c4f0b..9c7225d79e2a 100644
--- a/lib/galaxy/tool_util/xsd/galaxy.xsd
+++ b/lib/galaxy/tool_util/xsd/galaxy.xsd
@@ -783,6 +783,69 @@ Read more about configuring Galaxy to run Docker jobs
+
+
+ Document type of tool help
+
+
+
+
+
+
+
+
+
+
+.. class:: warningmark
+
+'''TIP''' This tool requires *fasta* format.
+
+----
+
+'''Example'''
+
+Query sequence::
+ >seq1
+ ATCG...
+
+.. image:: my_figure.png
+ :height: 500
+ :width: 600
+
+
+```
+
+]]>
+
+
+
+
+
+ Valid values are ``restructuredtext`` and ``markdown``
+
+
+
+
+
diff --git a/lib/galaxy/tools/__init__.py b/lib/galaxy/tools/__init__.py
index 7193fd6ea618..a3aff85c31dc 100644
--- a/lib/galaxy/tools/__init__.py
+++ b/lib/galaxy/tools/__init__.py
@@ -78,6 +78,7 @@
ToolOutputCollectionPart,
)
from galaxy.tool_util.parser.interface import (
+ HelpContent,
InputSource,
PageSource,
ToolSource,
@@ -1678,9 +1679,12 @@ def populate_tool_shed_info(self, tool_shed_repository):
@property
def help(self) -> Template:
+ help_content = self.raw_help
+ assert help_content
+ assert help_content.type == "restructuredtext"
try:
return Template(
- rst_to_html(self.raw_help),
+ rst_to_html(help_content.content),
input_encoding="utf-8",
default_filters=["decode.utf8"],
encoding_errors="replace",
@@ -1697,23 +1701,25 @@ def biotools_reference(self) -> Optional[str]:
"""
return biotools_reference(self.xrefs)
- def __get_help_with_images(self, raw_help: Optional[str]):
- help_text = raw_help or ""
- try:
- if help_text.find(".. image:: ") >= 0 and (self.tool_shed_repository or self.repository_id):
- return set_image_paths(
- self.app,
- help_text,
- encoded_repository_id=self.repository_id,
- tool_shed_repository=self.tool_shed_repository,
- tool_id=self.old_id,
- tool_version=self.version,
+ def __get_help_with_images(self, help_content: Optional[HelpContent]) -> Optional[HelpContent]:
+ if help_content and help_content.type == "restructuredtext":
+ help_text = help_content.content or ""
+ try:
+ if help_text.find(".. image:: ") >= 0 and (self.tool_shed_repository or self.repository_id):
+ help_text = set_image_paths(
+ self.app,
+ help_text,
+ encoded_repository_id=self.repository_id,
+ tool_shed_repository=self.tool_shed_repository,
+ tool_id=self.old_id,
+ tool_version=self.version,
+ )
+ except Exception:
+ log.exception(
+ "Exception in parse_help, so images may not be properly displayed for tool with id '%s'", self.id
)
- except Exception:
- log.exception(
- "Exception in parse_help, so images may not be properly displayed for tool with id '%s'", self.id
- )
- return help_text
+ help_content = HelpContent(type="restructuredtext", content=help_text)
+ return help_content
def find_output_def(self, name):
# name is JobToOutputDatasetAssociation name.
@@ -2505,12 +2511,18 @@ def to_dict(self, trans, link_details=False, io_details=False, tool_help=False):
if tool_help:
# create tool help
help_txt = ""
- if self.help:
- help_txt = self.help.render(
- static_path=self.app.url_for("/static"), host_url=self.app.url_for("/", qualified=True)
- )
- help_txt = unicodify(help_txt)
+ help_type = "restructuredtext"
+ help_content = self.raw_help
+ if help_content:
+ help_type = help_content.type
+ if help_content.type == "restructuredtext":
+ help_txt = self.help.render(
+ static_path=self.app.url_for("/static"), host_url=self.app.url_for("/", qualified=True)
+ )
+ help_txt = unicodify(help_txt)
+
tool_dict["help"] = help_txt
+ tool_dict["help_type"] = help_type
return tool_dict
@@ -2570,11 +2582,15 @@ def to_json(self, trans, kwd=None, job=None, workflow_building_mode=False, histo
# create tool help
tool_help = ""
- if self.help:
+ tool_help_type = "restructuredtext"
+ if self.raw_help and self.raw_help.type == "restructuredtext":
tool_help = self.help.render(
static_path=self.app.url_for("/static"), host_url=self.app.url_for("/", qualified=True)
)
tool_help = unicodify(tool_help, "utf-8")
+ elif self.raw_help:
+ tool_help = self.raw_help.content
+ tool_help_type = self.raw_help.type
if isinstance(self.action, tuple):
action = self.action[0] + self.app.url_for(self.action[1])
@@ -2586,6 +2602,7 @@ def to_json(self, trans, kwd=None, job=None, workflow_building_mode=False, histo
{
"id": self.id,
"help": tool_help,
+ "help_type": tool_help_type,
"citations": bool(self.citations),
"sharable_url": self.sharable_url,
"message": tool_message,
diff --git a/lib/galaxy/webapps/galaxy/buildapp.py b/lib/galaxy/webapps/galaxy/buildapp.py
index 8b13885b45d6..1e40c414f740 100644
--- a/lib/galaxy/webapps/galaxy/buildapp.py
+++ b/lib/galaxy/webapps/galaxy/buildapp.py
@@ -222,6 +222,7 @@ def app_pair(global_conf, load_app_kwds=None, wsgi_preflight=True, **kwargs):
webapp.add_client_route("/admin/form/{form_id}")
webapp.add_client_route("/admin/api_keys")
webapp.add_client_route("/carbon_emissions_calculations")
+ webapp.add_client_route("/help/terms/{term_id}")
webapp.add_client_route("/datatypes")
webapp.add_client_route("/login/start")
webapp.add_client_route("/tools/list")
diff --git a/lib/galaxy/workflow/modules.py b/lib/galaxy/workflow/modules.py
index eeece34fe554..a74a59923114 100644
--- a/lib/galaxy/workflow/modules.py
+++ b/lib/galaxy/workflow/modules.py
@@ -1854,7 +1854,7 @@ def get_version(self):
return self.tool.version if self.tool else self.tool_version
def get_tooltip(self, static_path=None):
- if self.tool and self.tool.help:
+ if self.tool and self.tool.raw_help and self.tool.raw_help.type == "restructuredtext":
host_url = self.trans.url_builder("/")
static_path = self.trans.url_builder(static_path) if static_path else ""
return self.tool.help.render(host_url=host_url, static_path=static_path)
diff --git a/test/unit/tool_util/test_parsing.py b/test/unit/tool_util/test_parsing.py
index af630961287c..21ec92387694 100644
--- a/test/unit/tool_util/test_parsing.py
+++ b/test/unit/tool_util/test_parsing.py
@@ -372,7 +372,7 @@ def test_stdio(self):
assert isinf(exit[0].range_end)
def test_help(self):
- help_text = self._tool_source.parse_help()
+ help_text = self._tool_source.parse_help().content
assert help_text.strip() == "This is HELP TEXT1!!!"
def test_tests(self):
@@ -579,7 +579,7 @@ def test_stdio(self):
assert isinf(exit[1].range_end)
def test_help(self):
- help_text = self._tool_source.parse_help()
+ help_text = self._tool_source.parse_help().content
assert help_text.strip() == "This is HELP TEXT2!!!"
def test_inputs(self):