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 12, 2024
1 parent a048d47 commit 1eec06e
Show file tree
Hide file tree
Showing 33 changed files with 1,208 additions and 16 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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import { type SampleSheetColumnDefinitionType } from "@/api";
import FormElement from "@/components/Form/FormElement.vue";
interface Props {
value: SampleSheetColumnDefinitionType;
prefix: string;
}
const props = defineProps<Props>();
const currentValue = ref<SampleSheetColumnDefinitionType>(props.value);
function onInput(newType: SampleSheetColumnDefinitionType) {
emit("onChange", newType);
}
function updateValue(newValue: SampleSheetColumnDefinitionType) {
currentValue.value = newValue;
}
// TODO: Sync this language with workflow parameter type dropdown...
const columnTypes = [
{ value: "int", label: "Integer" },
{ value: "float", label: "Any Number" },
{ value: "string", label: "String of text" },
{ value: "boolean", label: "Boolean (true or false value)" },
];
watch(() => props.value, updateValue, { immediate: true });
const emit = defineEmits(["onChange"]);
</script>

<template>
<div>
<FormElement
:id="prefix + '_type'"
:value="currentValue"
:attributes="{ data: columnTypes }"
title="Column type"
:optional="false"
type="select"
@input="onInput" />
</div>
</template>
176 changes: 176 additions & 0 deletions client/src/components/Workflow/Editor/Forms/FormColumnDefinitions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faCaretDown, faCaretUp, faFileExcel, 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 { withPrefix } from "@/utils/redirect";
import FormColumnDefinition from "./FormColumnDefinition.vue";
import FormCard from "@/components/Form/FormCard.vue";
library.add(faPlus, faFileExcel, 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 saveTooltip = computed(() => {
return localize(`Click to download an example workbook (xlsx file) for these columns`);
});
function getDownloadWorkbookUrl() {
const columnDefinitionsJson = JSON.stringify(props.value);
const columnDefinitionsJsonBase64 = Buffer.from(columnDefinitionsJson).toString("base64");
const url = withPrefix(`/api/sample_sheet_workbook/generate?column_definitions=${columnDefinitionsJsonBase64}`);
return url;
}
function downloadWorkbook() {
const url = getDownloadWorkbookUrl();
window.location.assign(url);
}
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>
<span v-b-tooltip.hover.bottom :title="saveTooltip">
<b-button
title="download example workbook"
role="button"
variant="link"
size="sm"
class="ml-0"
@click="downloadWorkbook">
<FontAwesomeIcon icon="file-excel" />
</b-button>
</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,6 +68,13 @@ 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") {
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
Loading

0 comments on commit 1eec06e

Please sign in to comment.