Skip to content

Commit

Permalink
WIP: sample sheets...
Browse files Browse the repository at this point in the history
  • Loading branch information
jmchilton committed Dec 11, 2024
1 parent 27e6f31 commit a461ab7
Show file tree
Hide file tree
Showing 30 changed files with 1,069 additions and 17 deletions.
4 changes: 4 additions & 0 deletions client/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,4 +312,8 @@ export type ObjectExportTaskResponse = components["schemas"]["ObjectExportTaskRe
export type ExportObjectRequestMetadata = components["schemas"]["ExportObjectRequestMetadata"];
export type ExportObjectResultMetadata = components["schemas"]["ExportObjectResultMetadata"];

export type SampleSheetColumnDefinition = components["schemas"]["SampleSheetColumnDefinition"];
export type SampleSheetColumnDefinitionType = SampleSheetColumnDefinition["type"];
export type SampleSheetColumnDefinitions = SampleSheetColumnDefinition[] | null;

export type AsyncTaskResultSummary = components["schemas"]["AsyncTaskResultSummary"];
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const collectionTypeOptions = [
{ value: "list", label: "List of Datasets" },
{ value: "paired", label: "Dataset Pair" },
{ value: "list:paired", label: "List of Dataset Pairs" },
{ value: "sample_sheet", label: "Sample Sheet of Datasets" },
];
function updateValue(newValue: string | undefined) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<script setup lang="ts">
import { type SampleSheetColumnDefinition, type SampleSheetColumnDefinitionType } from "@/api";
import FormColumnDefinitionType from "./FormColumnDefinitionType.vue";
import FormElement from "@/components/Form/FormElement.vue";
interface Props {
value: SampleSheetColumnDefinition;
index: number;
prefix: string; // prefix for ID objects
}
const props = defineProps<Props>();
const emit = defineEmits(["onChange"]);
function stateCopy(): SampleSheetColumnDefinition {
return JSON.parse(JSON.stringify(props.value));
}
function onName(name: string) {
const state = stateCopy();
state.name = name;
emit("onChange", state, props.index);
}
function onType(newType: SampleSheetColumnDefinitionType) {
const state = stateCopy();
state.type = newType;
emit("onChange", state, props.index);
}
</script>

<template>
<div>
<FormElement
:id="prefix + '_name'"
:value="value.name"
title="Name"
type="text"
help="Provide a short, unique name to describe this column."
@input="onName" />
<FormColumnDefinitionType :value="value.type" :prefix="prefix" @onChange="onType" />
<FormElement
:id="prefix + '_description'"
:value="value.description"
title="Description"
type="text"
help="Provide a longer description to help people running this workflow under what is expected to be entered in this column."
@input="onName" />
TODO: There are more fields to enter here including restrictions and validations that vary based on the type
chosen. There will be a lot of overlap with the same validation options for workflow parameters so it might be
best to wait until those components can be developed in parallel.
</div>
</template>
148 changes: 148 additions & 0 deletions client/src/components/Workflow/Editor/Forms/FormColumnDefinitions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faCaretDown, faCaretUp, faPlus, faTrashAlt } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BLink } from "bootstrap-vue";
import { computed } from "vue";
import { type SampleSheetColumnDefinition, type SampleSheetColumnDefinitions } from "@/api";
import localize from "@/utils/localization";
import FormColumnDefinition from "./FormColumnDefinition.vue";
import FormCard from "@/components/Form/FormCard.vue";
library.add(faPlus, faTrashAlt, faCaretUp, faCaretDown);
interface Props {
value: SampleSheetColumnDefinitions;
}
const props = defineProps<Props>();
function addColumn() {
const state = stateCopy();
state.push({ type: "int", name: "column" });
emit("onChange", state);
}
function stateCopy(): SampleSheetColumnDefinition[] {
return JSON.parse(JSON.stringify(props.value || []));
}
function onRemove(index: number) {
const state = stateCopy();
state.splice(index, 1);
emit("onChange", state);
}
function titleForColumnDefinition(index: number) {
return `Column ${index + 1}`;
}
function getPrefix(index: number) {
const name = `column_definition_${index}`;
return name;
}
function getButtonId(index: number, direction: "up" | "down") {
const prefix = getPrefix(index);
return `${prefix}_${direction}`;
}
function swap(index: number, swapWith: number, direction: "up" | "down") {
// the FormRepeat version does cool highlighting - probably worth implementing
// on next pass
const state = stateCopy();
if (swapWith >= 0 && swapWith < state.length && index >= 0 && index < state.length) {
const wasSwapped = state[swapWith] as SampleSheetColumnDefinition;
state[swapWith] = state[index] as SampleSheetColumnDefinition;
state[index] = wasSwapped;
}
emit("onChange", state);
}
function onChildUpdate(childState: SampleSheetColumnDefinition, index: number) {
const state = stateCopy();
state[index] = childState;
emit("onChange", state);
}
const deleteTooltip = computed(() => {
return localize(`Click to delete column definition`);
});
const emit = defineEmits(["onChange"]);
</script>

<template>
<div class="ui-form-element section-row">
<div class="ui-form-title">
<span class="ui-form-title-text">Column definitions</span>
</div>
<FormCard
v-for="(columnDefinition, index) in value"
v-bind:key="index"
data-description="column definition block"
class="card"
:title="titleForColumnDefinition(index)">
<template v-slot:operations>
<!-- code modelled after FormRepeat -->
<span class="float-right">
<b-button-group>
<b-button
:id="getButtonId(index, 'up')"
v-b-tooltip.hover.bottom
title="move up"
role="button"
variant="link"
size="sm"
class="ml-0"
@click="() => swap(index, index - 1, 'up')">
<FontAwesomeIcon icon="caret-up" />
</b-button>
<b-button
:id="getButtonId(index, 'down')"
v-b-tooltip.hover.bottom
title="move down"
role="button"
variant="link"
size="sm"
class="ml-0"
@click="() => swap(index, index + 1, 'down')">
<FontAwesomeIcon icon="caret-down" />
</b-button>
</b-button-group>

<span v-b-tooltip.hover.bottom :title="deleteTooltip">
<b-button
title="delete"
role="button"
variant="link"
size="sm"
class="ml-0"
@click="() => onRemove(index)">
<FontAwesomeIcon icon="trash-alt" />
</b-button>
</span>
</span>
</template>
<template v-slot:body>
<FormColumnDefinition
:index="index"
:value="columnDefinition"
:prefix="getPrefix(index)"
@onChange="onChildUpdate" />
</template>
</FormCard>
<BLink @click="addColumn">Add column.</BLink>
</div>
</template>

<style lang="scss" scoped>
@import "../../../Form/_form-elements.scss";
.column-definition-list {
padding: 0px;
list-style-type: none;
}
</style>
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
<script setup lang="ts">
import { computed, toRef } from "vue";
import { type SampleSheetColumnDefinitions } from "@/api";
import type { DatatypesMapperModel } from "@/components/Datatypes/model";
import type { Step } from "@/stores/workflowStepStore";
import { useToolState } from "../composables/useToolState";
import FormElement from "@/components/Form/FormElement.vue";
import FormCollectionType from "@/components/Workflow/Editor/Forms/FormCollectionType.vue";
import FormColumnDefinitions from "@/components/Workflow/Editor/Forms/FormColumnDefinitions.vue";
import FormDatatype from "@/components/Workflow/Editor/Forms/FormDatatype.vue";
interface ToolState {
collection_type: string | null;
optional: boolean;
format: string | null;
tag: string | null;
column_definitions: SampleSheetColumnDefinitions;
}
const props = defineProps<{
Expand All @@ -34,6 +37,7 @@ function cleanToolState(): ToolState {
optional: false,
tag: null,
format: null,
column_definitions: null,
};
}
}
Expand Down Expand Up @@ -64,9 +68,16 @@ function onCollectionType(newCollectionType: string | null) {
emit("onChange", state);
}
function onColumnDefinitions(newColumnDefinitions: SampleSheetColumnDefinitions) {
const state = cleanToolState();
console.log(newColumnDefinitions);
state.column_definitions = newColumnDefinitions;
emit("onChange", state);
}
const formatsAsList = computed(() => {
const formatStr = toolState.value?.format as (string | string[] | null);
if (formatStr && (typeof formatStr === "string")) {
const formatStr = toolState.value?.format as string | string[] | null;
if (formatStr && typeof formatStr === "string") {
return formatStr.split(/\s*,\s*/);
} else if (formatStr) {
return formatStr;
Expand Down Expand Up @@ -100,5 +111,9 @@ emit("onChange", cleanToolState());
type="text"
help="Tags to automatically filter inputs"
@input="onTags" />
<FormColumnDefinitions
v-if="toolState?.collection_type == 'sample_sheet'"
:value="toolState?.column_definitions"
@onChange="onColumnDefinitions" />
</div>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ export class CollectionTypeDescription implements CollectionTypeDescriptor {
}
}

const collectionTypeRegex = /^(list|paired)(:(list|paired))*$/;
const collectionTypeRegex = /^((list|paired)(:(list|paired))*|sample_sheet)$/;

export function isValidCollectionTypeStr(collectionType: string | undefined) {
if (collectionType) {
Expand Down
2 changes: 2 additions & 0 deletions client/src/stores/workflowStepStore.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { computed, del, ref, set } from "vue";

import { type SampleSheetColumnDefinitions } from "@/api";
import { type CollectionTypeDescriptor } from "@/components/Workflow/Editor/modules/collectionTypeDescription";
import { type Connection, getConnectionId, useConnectionStore } from "@/stores/workflowConnectionStore";
import { assertDefined } from "@/utils/assertions";
Expand Down Expand Up @@ -71,6 +72,7 @@ export interface DataStepInput extends BaseStepInput {
export interface DataCollectionStepInput extends BaseStepInput {
input_type: "dataset_collection";
collection_types: string[];
column_definitions: SampleSheetColumnDefinitions;
}

export interface ParameterStepInput extends Omit<BaseStepInput, "input_type"> {
Expand Down
17 changes: 16 additions & 1 deletion lib/galaxy/managers/collections.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ def create(
completed_job=None,
output_name=None,
fields=None,
column_definitions=None,
rows=None,
):
"""
PRECONDITION: security checks on ability to add to parent
Expand All @@ -201,6 +203,8 @@ def create(
copy_elements=copy_elements,
history=history,
fields=fields,
column_definitions=column_definitions,
rows=rows,
)

implicit_inputs = []
Expand Down Expand Up @@ -288,6 +292,8 @@ def create_dataset_collection(
copy_elements=False,
history=None,
fields=None,
column_definitions=None,
rows=None,
):
# Make sure at least one of these is None.
assert element_identifiers is None or elements is None
Expand Down Expand Up @@ -324,9 +330,12 @@ def create_dataset_collection(

if elements is not self.ELEMENTS_UNINITIALIZED:
type_plugin = collection_type_description.rank_type_plugin()
dataset_collection = builder.build_collection(type_plugin, elements, fields=fields)
dataset_collection = builder.build_collection(
type_plugin, elements, fields=fields, column_definitions=column_definitions, rows=rows
)
else:
# TODO: Pass fields here - need test case first.
# TODO: same with column definitions I think.
dataset_collection = model.DatasetCollection(populated=False)
dataset_collection.collection_type = collection_type
return dataset_collection
Expand Down Expand Up @@ -783,10 +792,16 @@ def __init_rule_data(self, elements, collection_type_description, parent_identif
identifiers = parent_identifiers + [element.element_identifier]
if not element.is_collection:
data.append([])
columns = None
collection_type_str = collection_type_description.collection_type
if collection_type_str == "sample_sheet":
columns = element.columns
assert isinstance(columns, list)
source = {
"identifiers": identifiers,
"dataset": element_object,
"tags": element_object.make_tag_string_list(),
"columns": columns,
}
sources.append(source)
else:
Expand Down
6 changes: 6 additions & 0 deletions lib/galaxy/managers/collections_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
exceptions,
model,
)
from galaxy.model.dataset_collections.types.sample_sheet_util import validate_column_definitions
from galaxy.util import string_as_bool

log = logging.getLogger(__name__)
Expand All @@ -33,13 +34,18 @@ def api_payload_to_create_params(payload):
message = f"Missing required parameters {missing_parameters}"
raise exceptions.ObjectAttributeMissingException(message)

column_definitions = payload.get("column_definitions", None)
validate_column_definitions(column_definitions)

params = dict(
collection_type=payload.get("collection_type"),
element_identifiers=payload.get("element_identifiers"),
name=payload.get("name", None),
hide_source_items=string_as_bool(payload.get("hide_source_items", False)),
copy_elements=string_as_bool(payload.get("copy_elements", False)),
fields=payload.get("fields", None),
column_definitions=column_definitions,
rows=payload.get("rows", None),
)
return params

Expand Down
Loading

0 comments on commit a461ab7

Please sign in to comment.