mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #15311 from ElectronicBlueberry/ts-config-tweaks
Stricter typescript config
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
# Handling Possibly undefined Values in Typescript
|
||||
|
||||
You may notice some more typescript errors than the usual, even compared to strict typescript.
|
||||
This is due to the rule `noUncheckedIndexedAccess` being activated in this project.
|
||||
|
||||
Here's an example of what this rule enforces:
|
||||
|
||||
`noUncheckedIndexedAccess` disabled
|
||||
|
||||
```ts
|
||||
type NumberDictionary = {
|
||||
[key: string]: number;
|
||||
};
|
||||
|
||||
const object: NumberDictionary = {
|
||||
keyA: 1,
|
||||
};
|
||||
|
||||
// type of a is number, but it is clearly undefined here
|
||||
const a = object.keyB;
|
||||
```
|
||||
|
||||
`noUncheckedIndexedAccess` enabled
|
||||
|
||||
```ts
|
||||
type NumberDictionary = {
|
||||
[key: string]: number;
|
||||
};
|
||||
|
||||
const object: NumberDictionary = {
|
||||
keyA: 1,
|
||||
};
|
||||
|
||||
// type of a is number | undefined, improving type safety
|
||||
const a = object.keyB;
|
||||
```
|
||||
|
||||
`noUncheckedIndexAccess` causes typescript to evaluate types as possibly undefined in
|
||||
any scenario where they may be undefined. E.g. when accessing a key of an object with unknown keys,
|
||||
or a value of an array with unknown length.
|
||||
|
||||
This leads to extra type safety during development, but needs some extra care to handle.
|
||||
|
||||
Here are some more realistic scenarios and how to handle them.
|
||||
|
||||
---
|
||||
|
||||
## Unspecific Types
|
||||
|
||||
Sometimes this error can be resolved, by simply being stricter about your types. The two main use-cases for this are `tuples` and `readonly objects`. These can also be defined as `arrays` and `records`, however, unlike `tuples` and `readonly objects`, these types have no guarantee that a value exists behind a specific index, which is where the typescript error message comes from.
|
||||
|
||||
### Readonly Objects
|
||||
|
||||
```ts
|
||||
const operatorForAlias: Record<string, string> = {
|
||||
lt: "<",
|
||||
le: "<=",
|
||||
ge: ">=",
|
||||
gt: ">",
|
||||
eq: ":",
|
||||
};
|
||||
|
||||
export function getOperatorForAlias(alias: string): string {
|
||||
return operatorForAlias[alias];
|
||||
}
|
||||
```
|
||||
|
||||
The keys and values of constant object never change. Typescript does not know this, and throws a possible undefined error in the function. This can be solved with more specific types:
|
||||
|
||||
```ts
|
||||
const operatorForAlias = {
|
||||
lt: "<",
|
||||
le: "<=",
|
||||
ge: ">=",
|
||||
gt: ">",
|
||||
eq: ":",
|
||||
} as const;
|
||||
|
||||
type OperatorForAlias = typeof operatorForAlias;
|
||||
type Alias = keyof OperatorForAlias;
|
||||
type Operator = OperatorForAlias[Alias];
|
||||
|
||||
export function getOperatorForAlias(alias: Alias): Operator {
|
||||
return operatorForAlias[alias];
|
||||
}
|
||||
```
|
||||
|
||||
`as const` tells typescript the values will never change. The other types restrict the possible inputs and outputs of the function to the objects keys and values respectively, giving us much more detailed type hints, and resolving the error.
|
||||
|
||||
To ensure future code changes keep the object as a flat key-value string pair, we can further add `satisfies Record<string, string>` to it, as follows:
|
||||
|
||||
```ts
|
||||
const operatorForAlias = {
|
||||
lt: "<",
|
||||
le: "<=",
|
||||
ge: ">=",
|
||||
gt: ">",
|
||||
eq: ":",
|
||||
} as const satisfies Record<string, string>;
|
||||
```
|
||||
|
||||
### Tuples
|
||||
|
||||
Consider an array of arrays as follows:
|
||||
|
||||
```ts
|
||||
const validAliases = [
|
||||
[">", "_gt"],
|
||||
["<", "_lt"],
|
||||
];
|
||||
|
||||
for (const [alias, substitute] of validAliases) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
The types of `alias` and `substitute` will be `string | undefined`. This is because typescript resolved the type of `validAliases` to `string[][]`: an array of string arrays. The length of the inner string array is unknown here. It could contain 0, 2, or 20 elements, as far as typescript knows, so this type is correct. We however know, that the inner array will always be exactly two strings long.
|
||||
|
||||
This is known as a tuple. `[">", "_gt"]` can be restricted to a tuple by setting it's type to `[string, string]`: An array with exactly two elements, the first of type string, the second one also of type string. Tuples do not need to be two elements long, and do not need the same type on every index. E.g a tuple of `[string, string, number]` is perfectly valid.
|
||||
|
||||
To make the types of `alias` and `substitute` resolve to `string`, we can change the code as follows:
|
||||
|
||||
```ts
|
||||
const defaultValidAliases: Array<[string, string]> = [
|
||||
[">", "_gt"],
|
||||
["<", "_lt"],
|
||||
];
|
||||
```
|
||||
|
||||
This also restricts what elements can be inserted into this array, further improving type safety.
|
||||
|
||||
---
|
||||
|
||||
## Potential Errors
|
||||
|
||||
Typescript also catches potential errors, which have not been thrown / asserted. These are valid potential error sources, which can not be worked around as in the examples above. Properly asserting them, gives us much more helpful error messages, to track down the source of an issue.
|
||||
|
||||
```ts
|
||||
const outputStep = this.stepStore.getStep(connection.output.stepId);
|
||||
let terminalSource = outputStep.outputs.find((output) => output.name === connection.output.name);
|
||||
```
|
||||
|
||||
This code will compile with errors, because `outputStep` is potentially undefined. While we may know that this state is not possible, a future error or mistake may still make it possible and properly asserting can be helpful. Here I'm using the new `assertDefined` utility.
|
||||
|
||||
```ts
|
||||
const outputStep = this.stepStore.getStep(connection.output.stepId);
|
||||
assertDefined(outputStep, `No such step with id ${connection.output.stepId}`);
|
||||
|
||||
let terminalSource = outputStep.outputs.find((output) => output.name === connection.output.name);
|
||||
```
|
||||
|
||||
The code will now compile fine.
|
||||
|
||||
---
|
||||
|
||||
## Defaulting and Optional Chaining
|
||||
|
||||
Setting sensible default values, or using optional chaining can also be a way of dealing with possible undefined values in some situations.
|
||||
|
||||
Example
|
||||
|
||||
```ts
|
||||
// if otherVar is undefined or null, myVar will be set to 4
|
||||
const myVar = otherVar ?? 4;
|
||||
|
||||
// someFunction will only be called if someObject is not undefined or null
|
||||
someObject?.someFunction();
|
||||
|
||||
// combining both concepts
|
||||
const myOtherVar = someObject?.getValue() ?? "default";
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Incorrect Warning
|
||||
|
||||
Typescript isn't always right when it comes to these warnings. Sometimes the surrounding code makes it obvious that an undefined value is not possible here. For example:
|
||||
|
||||
(This code is modified for this example)
|
||||
|
||||
```ts
|
||||
if (row.length > 0) {
|
||||
// Try to split by comma first
|
||||
let rowDataSplit = row[0].split(","); // possible undefined error
|
||||
if (rowDataSplit.length === num_columns) {
|
||||
return rowDataSplit;
|
||||
}
|
||||
// Try to split by tab
|
||||
rowDataSplit = row[0].split("\t"); // possible undefined error
|
||||
if (rowDataSplit.length === num_columns) {
|
||||
return rowDataSplit;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Solution 1: Reordering code. To tell typescript that we are sure this value exists, we can simply reorder the code.
|
||||
This is the preferable solution, as it leads to cleaner code.
|
||||
|
||||
```ts
|
||||
const firstElement = row[0];
|
||||
|
||||
if (firstElement) {
|
||||
// Try to split by comma first
|
||||
let rowDataSplit = firstElement.split(",");
|
||||
if (rowDataSplit.length === num_columns) {
|
||||
return rowDataSplit;
|
||||
}
|
||||
// Try to split by tab
|
||||
rowDataSplit = firstElement.split("\t");
|
||||
if (rowDataSplit.length === num_columns) {
|
||||
return rowDataSplit;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Solution 2: Tell typescript you know better. We can override typescripts assumption using `!`. **Warning:** Only use this if the immediately surrounding code makes it obvious this value exists!
|
||||
|
||||
```ts
|
||||
if (row.length > 0) {
|
||||
// Try to split by comma first
|
||||
let rowDataSplit = row[0]!.split(",");
|
||||
if (rowDataSplit.length === num_columns) {
|
||||
return rowDataSplit;
|
||||
}
|
||||
// Try to split by tab
|
||||
rowDataSplit = row[0]!.split("\t");
|
||||
if (rowDataSplit.length === num_columns) {
|
||||
return rowDataSplit;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notice the `!`, behind the element which typescript thought may be undefined.
|
||||
|
||||
Sometimes the first solution is not possible, like when we are checking for a specific length. This was the case in the code this example was inspired by. In this case, we must use the second solution:
|
||||
|
||||
```ts
|
||||
[...]
|
||||
} else if (row.length === 1) {
|
||||
// Try to split by comma first
|
||||
let rowDataSplit = row[0]!.split(",");
|
||||
if (rowDataSplit.length === num_columns) {
|
||||
return rowDataSplit;
|
||||
}
|
||||
// Try to split by tab
|
||||
rowDataSplit = row[0]!.split("\t");
|
||||
if (rowDataSplit.length === num_columns) {
|
||||
return rowDataSplit;
|
||||
}
|
||||
[...]
|
||||
```
|
||||
@@ -3,7 +3,7 @@ import { computed, ref } from "vue";
|
||||
|
||||
export interface FormCheckProps {
|
||||
value?: string | string[];
|
||||
options: string[];
|
||||
options: Array<[string, string]>;
|
||||
}
|
||||
|
||||
const props = defineProps<FormCheckProps>();
|
||||
|
||||
@@ -44,8 +44,8 @@ function removeHistoryFromList(history: History) {
|
||||
historyStore.unpinHistory(history.id);
|
||||
}
|
||||
|
||||
if (!selectedHistories.value.length) {
|
||||
historyStore.pinHistory(props.histories[0].id);
|
||||
if (!selectedHistories.value.length ?? props.histories.length > 0) {
|
||||
historyStore.pinHistory(props.histories[0]!.id);
|
||||
}
|
||||
|
||||
function addHistoriesToList(histories: History[]) {
|
||||
|
||||
@@ -125,19 +125,28 @@ export async function cleanupDiscardedDatasets(items: CleanableItem[]): Promise<
|
||||
(acc: { [key: string]: CleanableItem }, item: CleanableItem) => ((acc[item.id] = item), acc),
|
||||
{}
|
||||
);
|
||||
|
||||
const datasetSourceIds: DatasetSourceId[] = items.map((item: CleanableItem) => {
|
||||
const dataset = item as DiscardedDataset;
|
||||
return { id: dataset.id, src: dataset.hda_ldda };
|
||||
});
|
||||
|
||||
const requestResult = await purgeDatasets(datasetSourceIds);
|
||||
|
||||
resultResponse.totalItemCount = items.length;
|
||||
|
||||
if (requestResult.errors) {
|
||||
resultResponse.errors = mapErrors(datasetsTable, requestResult.errors);
|
||||
|
||||
const erroredIds = requestResult.errors?.reduce((acc: string[], error) => [...acc, error.dataset.id], []);
|
||||
resultResponse.totalFreeBytes = datasetSourceIds.reduce(
|
||||
(partial_sum, item) => partial_sum + (erroredIds?.includes(item.id) ? 0 : datasetsTable[item.id].size),
|
||||
0
|
||||
);
|
||||
|
||||
resultResponse.totalFreeBytes = datasetSourceIds.reduce((partial_sum, item) => {
|
||||
if (erroredIds?.includes(item.id)) {
|
||||
return partial_sum;
|
||||
} else {
|
||||
return partial_sum + (datasetsTable[item.id]?.size ?? 0);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
} catch (error) {
|
||||
resultResponse.errorMessage = error as string;
|
||||
@@ -207,12 +216,13 @@ export async function cleanupDiscardedHistories(histories: DiscardedHistory[]) {
|
||||
try {
|
||||
for (const history of histories) {
|
||||
await purgeHistory(history.id);
|
||||
resultResponse.totalFreeBytes += historiesTable[history.id].size;
|
||||
resultResponse.totalFreeBytes += historiesTable[history.id]?.size ?? 0;
|
||||
resultResponse.totalItemCount += 1;
|
||||
}
|
||||
} catch (error) {
|
||||
resultResponse.errorMessage = error as string;
|
||||
}
|
||||
|
||||
return new CleanupResult(resultResponse);
|
||||
}
|
||||
|
||||
@@ -224,7 +234,7 @@ export async function cleanupDiscardedHistories(histories: DiscardedHistory[]) {
|
||||
*/
|
||||
function mapErrors(datasetsTable: { [key: string]: CleanableItem }, errors: DatasetErrorMessage[]): ItemError[] {
|
||||
return errors.map((error) => {
|
||||
const name = datasetsTable[error.dataset.id].name;
|
||||
const name = datasetsTable[error.dataset.id]?.name ?? "Unknown Dataset";
|
||||
return { name: name, reason: error.error_message };
|
||||
});
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ const columnStyle = computed(() => {
|
||||
const columnStyle = Array(props.options.dataset_config.metadata_columns);
|
||||
if (props.options.dataset_config.metadata_column_types?.length > 0) {
|
||||
props.options.dataset_config.metadata_column_types.forEach((column_type, index) => {
|
||||
columnStyle[index] = column_type === "str" || column_type === "list" ? "stringalign" : "numberalign";
|
||||
columnStyle[index] = column_type === "str" || column_type === "list" ? "string-align" : "number-align";
|
||||
});
|
||||
}
|
||||
return columnStyle;
|
||||
@@ -116,17 +116,17 @@ function processRow(row: string[]) {
|
||||
return row.slice(0, num_columns - 1).concat([row.slice(num_columns - 1).join("\t")]);
|
||||
} else if (row.length === 1) {
|
||||
// Try to split by comma first
|
||||
let rowDataSplit = row[0].split(",");
|
||||
let rowDataSplit = row[0]!.split(",");
|
||||
if (rowDataSplit.length === num_columns) {
|
||||
return rowDataSplit;
|
||||
}
|
||||
// Try to split by tab
|
||||
rowDataSplit = row[0].split("\t");
|
||||
rowDataSplit = row[0]!.split("\t");
|
||||
if (rowDataSplit.length === num_columns) {
|
||||
return rowDataSplit;
|
||||
}
|
||||
// Try to split by space
|
||||
rowDataSplit = row[0].split(" ");
|
||||
rowDataSplit = row[0]!.split(" ");
|
||||
if (rowDataSplit.length === num_columns) {
|
||||
return rowDataSplit;
|
||||
}
|
||||
@@ -190,10 +190,10 @@ onMounted(() => {
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.stringalign {
|
||||
.string-align {
|
||||
text-align: left;
|
||||
}
|
||||
.numberalign {
|
||||
.number-align {
|
||||
text-align: right;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -24,8 +24,14 @@
|
||||
<script lang="ts" setup>
|
||||
import { useWorkflowStepStore } from "@/stores/workflowStepStore";
|
||||
import { computed, onMounted, ref, watch, type ComputedRef } from "vue";
|
||||
import { type OutputTerminals, type InputTerminalsAndInvalid, terminalFactory } from "./modules/terminals";
|
||||
import {
|
||||
type OutputTerminals,
|
||||
type InputTerminalsAndInvalid,
|
||||
terminalFactory,
|
||||
type InputTerminals,
|
||||
} from "./modules/terminals";
|
||||
import { useFocusWithin } from "@/composables/useActiveElement";
|
||||
import { assertDefined } from "@/utils/assertions";
|
||||
|
||||
const props = defineProps<{
|
||||
terminal: OutputTerminals;
|
||||
@@ -41,7 +47,7 @@ const emit = defineEmits<{ (e: "closeMenu", value: boolean): void }>();
|
||||
onMounted(() => {
|
||||
if (menuItem.value) {
|
||||
if ("length" in menuItem.value) {
|
||||
menuItem.value[0].focus();
|
||||
menuItem.value[0]?.focus();
|
||||
} else {
|
||||
menuItem.value.focus();
|
||||
}
|
||||
@@ -67,29 +73,28 @@ function increment() {
|
||||
if (menuItem.value && "length" in menuItem.value) {
|
||||
activeElement.value += 1;
|
||||
activeElement.value = Math.min(activeElement.value, menuItem.value!.length - 1);
|
||||
menuItem.value![activeElement.value].focus();
|
||||
menuItem.value![activeElement.value]?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function decrement() {
|
||||
if (menuItem.value && "length" in menuItem.value) {
|
||||
activeElement.value = Math.max(activeElement.value - 1, 0);
|
||||
menuItem.value![activeElement.value].focus();
|
||||
menuItem.value![activeElement.value]?.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function terminalToInputObject(terminal: InputTerminalsAndInvalid, connected: boolean): InputObject {
|
||||
const step = stepStore.getStep(terminal.stepId);
|
||||
assertDefined(step);
|
||||
const inputLabel = `${terminal.name} in step ${step.id + 1}: ${step.label}`;
|
||||
return { stepId: step.id, inputName: terminal.name, inputLabel, connected };
|
||||
}
|
||||
|
||||
function inputObjectToTerminal(inputObject: InputObject): InputTerminalsAndInvalid {
|
||||
// TODO: this isn't ideal, we may not actually have a step .. except we do.
|
||||
// Generalize connection.<input | output> to terminalSource ?
|
||||
const inputSource = stepStore
|
||||
.getStep(inputObject.stepId)
|
||||
.inputs.find((input) => input.name == inputObject.inputName)!;
|
||||
function inputObjectToTerminal(inputObject: InputObject): InputTerminals {
|
||||
const step = stepStore.getStep(inputObject.stepId);
|
||||
assertDefined(step);
|
||||
const inputSource = step.inputs.find((input) => input.name == inputObject.inputName)!;
|
||||
return terminalFactory(inputObject.stepId, inputSource, props.terminal.datatypesMapper);
|
||||
}
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
step: {
|
||||
// type Step from "@/stores/workflowStepStore";
|
||||
// type Step from @/stores/workflowStepStore
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -50,7 +50,7 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
step: {
|
||||
// type Step from "@/stores/workflowStepStore";
|
||||
// type Step from @/stores/workflowStepStore
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -81,6 +81,7 @@ export default {
|
||||
},
|
||||
props: {
|
||||
step: {
|
||||
// type Step from @/stores/workflowStepStore
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
:input="input"
|
||||
:step-id="id"
|
||||
:datatypes-mapper="datatypesMapper"
|
||||
:step-position="step.position"
|
||||
:step-position="step.position ?? { top: 0, left: 0 }"
|
||||
:root-offset="rootOffset"
|
||||
:scroll="scroll"
|
||||
:scale="scale"
|
||||
@@ -92,7 +92,7 @@
|
||||
:post-job-actions="postJobActions"
|
||||
:step-id="id"
|
||||
:step-type="step.type"
|
||||
:step-position="step.position"
|
||||
:step-position="step.position ?? { top: 0, left: 0 }"
|
||||
:root-offset="rootOffset"
|
||||
:scroll="scroll"
|
||||
:scale="scale"
|
||||
|
||||
@@ -61,8 +61,8 @@ describe("NodeOutput", () => {
|
||||
it("displays multiple icon if not mapped over", async () => {
|
||||
const simpleDataStep = stepForLabel("simple data", stepStore.steps);
|
||||
const listInputStep = stepForLabel("list input", stepStore.steps);
|
||||
const inputTerminal = terminalFactory(simpleDataStep.id, simpleDataStep.inputs[0], testDatatypesMapper);
|
||||
const outputTerminal = terminalFactory(listInputStep.id, listInputStep.outputs[0], testDatatypesMapper);
|
||||
const inputTerminal = terminalFactory(simpleDataStep.id, simpleDataStep.inputs[0]!, testDatatypesMapper);
|
||||
const outputTerminal = terminalFactory(listInputStep.id, listInputStep.outputs[0]!, testDatatypesMapper);
|
||||
const propsData = propsForStep(simpleDataStep);
|
||||
const wrapper = shallowMount(NodeOutput, {
|
||||
propsData: propsData,
|
||||
|
||||
@@ -23,10 +23,8 @@ const props = defineProps<{
|
||||
const stepStore = useWorkflowStepStore();
|
||||
const connectionStore = useConnectionStore();
|
||||
const outputIsMappedOver = computed(() => stepStore.stepMapOver[props.connection.output.stepId]?.isCollection);
|
||||
const inputIsMappedOver = computed(
|
||||
() =>
|
||||
stepStore.stepInputMapOver[props.connection.input.stepId] &&
|
||||
stepStore.stepInputMapOver[props.connection.input.stepId][props.connection.input.name]?.isCollection
|
||||
const inputIsMappedOver = computed(() =>
|
||||
Boolean(stepStore.stepInputMapOver[props.connection.input.stepId]?.[props.connection.input.name]?.isCollection)
|
||||
);
|
||||
const outputIsOptional = computed(() => {
|
||||
return Boolean(
|
||||
|
||||
@@ -51,6 +51,7 @@ import { useWorkflowStepStore, type Step } from "@/stores/workflowStepStore";
|
||||
import { useD3Zoom } from "./composables/d3Zoom";
|
||||
import type { XYPosition } from "@/stores/workflowEditorStateStore";
|
||||
import type { OutputTerminals } from "./modules/terminals";
|
||||
import { assertDefined } from "@/utils/assertions";
|
||||
import { minZoom, maxZoom } from "./modules/zoomLevels";
|
||||
|
||||
const emit = defineEmits(["transform", "graph-offset", "onRemove", "scrollTo"]);
|
||||
@@ -78,8 +79,14 @@ watch(
|
||||
() => props.scrollToId,
|
||||
() => {
|
||||
if (props.scrollToId !== null) {
|
||||
const { width: stepWidth, height: stepHeight } = stateStore.stepPosition[props.scrollToId];
|
||||
const { position: stepPosition } = stepStore.getStep(props.scrollToId);
|
||||
const scrollToPosition = stateStore.stepPosition[props.scrollToId];
|
||||
const step = stepStore.getStep(props.scrollToId);
|
||||
|
||||
assertDefined(scrollToPosition);
|
||||
assertDefined(step);
|
||||
|
||||
const { width: stepWidth, height: stepHeight } = scrollToPosition;
|
||||
const { position: stepPosition } = step;
|
||||
if (stepPosition) {
|
||||
const { width, height } = reactive(elementBounding);
|
||||
const centerScreenX = width / 2;
|
||||
|
||||
@@ -49,12 +49,15 @@ function recalculateAABB() {
|
||||
|
||||
Object.values(props.steps).forEach((step) => {
|
||||
const rect = stateStore.stepPosition[step.id];
|
||||
aabb.fitRectangle({
|
||||
x: step.position!.left,
|
||||
y: step.position!.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
|
||||
if (rect) {
|
||||
aabb.fitRectangle({
|
||||
x: step.position!.left,
|
||||
y: step.position!.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
aabb.squareCenter();
|
||||
@@ -161,7 +164,10 @@ function renderMinimap() {
|
||||
ctx.fillStyle = colors.node;
|
||||
okSteps.forEach((step) => {
|
||||
const rect = stateStore.stepPosition[step.id];
|
||||
ctx.rect(step.position!.left, step.position!.top, rect.width, rect.height);
|
||||
|
||||
if (rect) {
|
||||
ctx.rect(step.position!.left, step.position!.top, rect.width, rect.height);
|
||||
}
|
||||
});
|
||||
ctx.fill();
|
||||
|
||||
@@ -169,7 +175,10 @@ function renderMinimap() {
|
||||
ctx.fillStyle = colors.error;
|
||||
errorSteps.forEach((step) => {
|
||||
const rect = stateStore.stepPosition[step.id];
|
||||
ctx.rect(step.position!.left, step.position!.top, rect.width, rect.height);
|
||||
|
||||
if (rect) {
|
||||
ctx.rect(step.position!.left, step.position!.top, rect.width, rect.height);
|
||||
}
|
||||
});
|
||||
ctx.fill();
|
||||
|
||||
@@ -181,12 +190,16 @@ function renderMinimap() {
|
||||
ctx.strokeStyle = colors.selectedOutline;
|
||||
ctx.lineWidth = edge;
|
||||
const rect = stateStore.stepPosition[selectedStep.id];
|
||||
ctx.rect(
|
||||
selectedStep.position!.left - edge,
|
||||
selectedStep.position!.top - edge,
|
||||
rect.width + edge * 2,
|
||||
rect.height + edge * 2
|
||||
);
|
||||
|
||||
if (rect) {
|
||||
ctx.rect(
|
||||
selectedStep.position!.left - edge,
|
||||
selectedStep.position!.top - edge,
|
||||
rect.width + edge * 2,
|
||||
rect.height + edge * 2
|
||||
);
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ import ELK from "elkjs/lib/elk.bundled.js";
|
||||
import { useWorkflowStateStore } from "@/stores/workflowEditorStateStore";
|
||||
import type { Step } from "@/stores/workflowStepStore";
|
||||
import { useConnectionStore } from "@/stores/workflowConnectionStore";
|
||||
import { assertDefined } from "@/utils/assertions";
|
||||
|
||||
const elk = new ELK();
|
||||
|
||||
@@ -67,7 +68,10 @@ export async function autoLayout(steps: { [index: string]: Step }) {
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const position = stateStore.stepPosition[step.id];
|
||||
assertDefined(position, `No StepPosition with step id ${step.id} found in workflowStateStore`);
|
||||
|
||||
return {
|
||||
id: stepId,
|
||||
height: position.height + 20,
|
||||
|
||||
@@ -2,6 +2,7 @@ import { terminalFactory } from "./terminals";
|
||||
import type { Step, Steps } from "@/stores/workflowStepStore";
|
||||
import type { DatatypesMapperModel } from "@/components/Datatypes/model";
|
||||
import type { UntypedParameters } from "@/components/Workflow/Editor/modules/parameters";
|
||||
import { assertDefined } from "@/utils/assertions";
|
||||
|
||||
interface LintState {
|
||||
stepId: number;
|
||||
@@ -90,16 +91,23 @@ export function getUntypedParameters(untypedParameters: UntypedParameters) {
|
||||
const items: LintState[] = [];
|
||||
if (untypedParameters) {
|
||||
untypedParameters.parameters.forEach((parameter) => {
|
||||
const parameterReference = parameter.references[0];
|
||||
// TODO: Not sure this is right, but I think this may have been broken previously?
|
||||
const stepLabel = "toolInput" in parameterReference ? parameterReference.toolInput.label : parameter.name;
|
||||
items.push({
|
||||
stepId: parameterReference.stepId,
|
||||
stepLabel: stepLabel,
|
||||
warningLabel: parameter.name,
|
||||
name: parameter.name,
|
||||
autofix: parameter.canExtract(),
|
||||
});
|
||||
try {
|
||||
const parameterReference = parameter.references[0];
|
||||
assertDefined(parameterReference, `Parameter references for ${parameter.name} are empty.`);
|
||||
|
||||
// TODO: Not sure this is right, but I think this may have been broken previously?
|
||||
const stepLabel =
|
||||
"toolInput" in parameterReference ? parameterReference.toolInput.label : parameter.name;
|
||||
items.push({
|
||||
stepId: parameterReference.stepId,
|
||||
stepLabel: stepLabel,
|
||||
warningLabel: parameter.name,
|
||||
name: parameter.name,
|
||||
autofix: parameter.canExtract(),
|
||||
});
|
||||
} catch (errorMessage) {
|
||||
console.error(errorMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
return items;
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "./terminals";
|
||||
import { testDatatypesMapper } from "@/components/Datatypes/test_fixtures";
|
||||
import { useConnectionStore } from "@/stores/workflowConnectionStore";
|
||||
import type { DataOutput, Steps } from "@/stores/workflowStepStore";
|
||||
import type { DataOutput, Steps, Step } from "@/stores/workflowStepStore";
|
||||
import {
|
||||
ANY_COLLECTION_TYPE_DESCRIPTION,
|
||||
CollectionTypeDescription,
|
||||
@@ -29,10 +29,10 @@ function setupAdvanced() {
|
||||
if (stepLabel) {
|
||||
terminals[stepLabel] = {};
|
||||
step.inputs?.map((input) => {
|
||||
terminals[stepLabel][input.name] = terminalFactory(step.id, input, testDatatypesMapper);
|
||||
terminals[stepLabel]![input.name] = terminalFactory(step.id, input, testDatatypesMapper);
|
||||
});
|
||||
step.outputs?.map((output) => {
|
||||
terminals[stepLabel][output.name] = terminalFactory(step.id, output, testDatatypesMapper);
|
||||
terminals[stepLabel]![output.name] = terminalFactory(step.id, output, testDatatypesMapper);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -43,9 +43,9 @@ function rebuildTerminal<T extends ReturnType<typeof terminalFactory>>(terminal:
|
||||
let terminalSource: TerminalSource;
|
||||
const step = terminal.stepStore.getStep(terminal.stepId);
|
||||
if (terminal.terminalType === "input") {
|
||||
terminalSource = step.inputs.find((input) => input.name == terminal.name)!;
|
||||
terminalSource = step!.inputs.find((input) => input.name == terminal.name)!;
|
||||
} else {
|
||||
terminalSource = step.outputs.find((output) => output.name == terminal.name)!;
|
||||
terminalSource = step!.outputs.find((output) => output.name == terminal.name)!;
|
||||
}
|
||||
return terminalFactory(terminal.stepId, terminalSource, testDatatypesMapper) as T;
|
||||
}
|
||||
@@ -58,29 +58,29 @@ describe("terminalFactory", () => {
|
||||
});
|
||||
|
||||
it("constructs correct class instances", () => {
|
||||
expect(terminals["data input"]["output"]).toBeInstanceOf(OutputTerminal);
|
||||
expect(terminals["simple data"]["input"]).toBeInstanceOf(InputTerminal);
|
||||
expect(terminals["simple data"]["out_file1"]).toBeInstanceOf(OutputTerminal);
|
||||
expect(terminals["simple data 2"]["input"]).toBeInstanceOf(InputTerminal);
|
||||
expect(terminals["simple data 2"]["out_file1"]).toBeInstanceOf(OutputTerminal);
|
||||
expect(terminals["multiple simple data"]["input1"]).toBeInstanceOf(InputTerminal);
|
||||
expect(terminals["multiple simple data"]["queries_0|input2"]).toBeInstanceOf(InputTerminal);
|
||||
expect(terminals["multiple simple data"]["out_file1"]).toBeInstanceOf(OutputTerminal);
|
||||
expect(terminals["optional data input"]["output"]).toBeInstanceOf(OutputTerminal);
|
||||
expect(terminals["list input"]["output"]).toBeInstanceOf(OutputCollectionTerminal);
|
||||
expect(terminals["list:list input"]["output"]).toBeInstanceOf(OutputCollectionTerminal);
|
||||
expect(terminals["paired input"]["output"]).toBeInstanceOf(OutputCollectionTerminal);
|
||||
expect(terminals["multi data"]["f1"]).toBeInstanceOf(InputTerminal);
|
||||
expect(terminals["multi data"]["f2"]).toBeInstanceOf(InputTerminal);
|
||||
expect(terminals["multi data"]["out1"]).toBeInstanceOf(OutputTerminal);
|
||||
expect(terminals["multi data"]["out2"]).toBeInstanceOf(OutputTerminal);
|
||||
expect(terminals["integer parameter input"]["output"]).toBeInstanceOf(OutputParameterTerminal);
|
||||
expect(terminals["any collection"]["input"]).toBeInstanceOf(InputCollectionTerminal);
|
||||
expect(terminals["any collection"]["output"]).toBeInstanceOf(OutputCollectionTerminal);
|
||||
expect(terminals["multi data"]["advanced|advanced_threshold"]).toBeInstanceOf(InputParameterTerminal);
|
||||
expect(terminals["list collection input"]["input1"]).toBeInstanceOf(InputCollectionTerminal);
|
||||
expect(terminals["filter_failed"]["input"]).toBeInstanceOf(InputCollectionTerminal);
|
||||
expect(terminals["filter_failed"]["output"]).toBeInstanceOf(OutputCollectionTerminal);
|
||||
expect(terminals["data input"]?.["output"]).toBeInstanceOf(OutputTerminal);
|
||||
expect(terminals["simple data"]?.["input"]).toBeInstanceOf(InputTerminal);
|
||||
expect(terminals["simple data"]?.["out_file1"]).toBeInstanceOf(OutputTerminal);
|
||||
expect(terminals["simple data 2"]?.["input"]).toBeInstanceOf(InputTerminal);
|
||||
expect(terminals["simple data 2"]?.["out_file1"]).toBeInstanceOf(OutputTerminal);
|
||||
expect(terminals["multiple simple data"]?.["input1"]).toBeInstanceOf(InputTerminal);
|
||||
expect(terminals["multiple simple data"]?.["queries_0|input2"]).toBeInstanceOf(InputTerminal);
|
||||
expect(terminals["multiple simple data"]?.["out_file1"]).toBeInstanceOf(OutputTerminal);
|
||||
expect(terminals["optional data input"]?.["output"]).toBeInstanceOf(OutputTerminal);
|
||||
expect(terminals["list input"]?.["output"]).toBeInstanceOf(OutputCollectionTerminal);
|
||||
expect(terminals["list:list input"]?.["output"]).toBeInstanceOf(OutputCollectionTerminal);
|
||||
expect(terminals["paired input"]?.["output"]).toBeInstanceOf(OutputCollectionTerminal);
|
||||
expect(terminals["multi data"]?.["f1"]).toBeInstanceOf(InputTerminal);
|
||||
expect(terminals["multi data"]?.["f2"]).toBeInstanceOf(InputTerminal);
|
||||
expect(terminals["multi data"]?.["out1"]).toBeInstanceOf(OutputTerminal);
|
||||
expect(terminals["multi data"]?.["out2"]).toBeInstanceOf(OutputTerminal);
|
||||
expect(terminals["integer parameter input"]?.["output"]).toBeInstanceOf(OutputParameterTerminal);
|
||||
expect(terminals["any collection"]?.["input"]).toBeInstanceOf(InputCollectionTerminal);
|
||||
expect(terminals["any collection"]?.["output"]).toBeInstanceOf(OutputCollectionTerminal);
|
||||
expect(terminals["multi data"]?.["advanced|advanced_threshold"]).toBeInstanceOf(InputParameterTerminal);
|
||||
expect(terminals["list collection input"]?.["input1"]).toBeInstanceOf(InputCollectionTerminal);
|
||||
expect(terminals["filter_failed"]?.["input"]).toBeInstanceOf(InputCollectionTerminal);
|
||||
expect(terminals["filter_failed"]?.["output"]).toBeInstanceOf(OutputCollectionTerminal);
|
||||
});
|
||||
it("throws error on invalid terminalSource", () => {
|
||||
const invalidFactory = () => terminalFactory(1, {} as any, testDatatypesMapper);
|
||||
@@ -103,8 +103,8 @@ describe("canAccept", () => {
|
||||
});
|
||||
|
||||
it("accepts simple data -> data connections", () => {
|
||||
const dataOut = terminals["data input"]["output"] as OutputTerminal;
|
||||
const dataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
const dataOut = terminals["data input"]!["output"] as OutputTerminal;
|
||||
const dataIn = terminals["simple data"]!["input"] as InputTerminal;
|
||||
expect(dataIn.canAccept(dataOut).canAccept).toBe(true);
|
||||
dataIn.connect(dataOut);
|
||||
expect(dataIn.canAccept(dataOut).canAccept).toBe(false);
|
||||
@@ -112,8 +112,8 @@ describe("canAccept", () => {
|
||||
expect(dataIn.canAccept(dataOut).canAccept).toBe(true);
|
||||
});
|
||||
it("accepts collection data -> data connection", () => {
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]!["input"] as InputTerminal;
|
||||
expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION);
|
||||
expect(dataIn.canAccept(collectionOut).canAccept).toBe(true);
|
||||
dataIn.connect(collectionOut);
|
||||
@@ -127,15 +127,15 @@ describe("canAccept", () => {
|
||||
expect(dataIn.mapOver).toEqual(NULL_COLLECTION_TYPE_DESCRIPTION);
|
||||
});
|
||||
it("accepts mapped over data output on mapped over data input", () => {
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["multiple simple data"]["input1"] as InputTerminal;
|
||||
const dataInTwo = terminals["multiple simple data"]["queries_0|input2"] as InputTerminal;
|
||||
const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["multiple simple data"]!["input1"] as InputTerminal;
|
||||
const dataInTwo = terminals["multiple simple data"]!["queries_0|input2"] as InputTerminal;
|
||||
dataIn.connect(collectionOut);
|
||||
expect(dataInTwo.canAccept(collectionOut).canAccept).toBe(true);
|
||||
});
|
||||
it("accepts list:list data -> data connection", () => {
|
||||
const collectionOut = terminals["list:list input"]["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
const collectionOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]!["input"] as InputTerminal;
|
||||
expect(dataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION);
|
||||
expect(dataIn.canAccept(collectionOut).canAccept).toBe(true);
|
||||
dataIn.connect(collectionOut);
|
||||
@@ -146,37 +146,37 @@ describe("canAccept", () => {
|
||||
expect(dataIn.mapOver).toEqual(NULL_COLLECTION_TYPE_DESCRIPTION);
|
||||
});
|
||||
it("treats multi data input as list input", () => {
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
const multiDataIn = terminals["multi data"]["f1"] as InputTerminal;
|
||||
const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal;
|
||||
const multiDataIn = terminals["multi data"]!["f1"] as InputTerminal;
|
||||
expect(multiDataIn.canAccept(collectionOut).canAccept).toBe(true);
|
||||
multiDataIn.connect(collectionOut);
|
||||
expect(multiDataIn.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION);
|
||||
});
|
||||
it("accepts separate list:list inputs on separate multi-data inputs of same tool", () => {
|
||||
const collectionOut = terminals["list:list input"]["output"] as OutputCollectionTerminal;
|
||||
const multiDataInOne = terminals["multi data"]["f1"] as InputTerminal;
|
||||
const multiDataInTwo = terminals["multi data"]["f2"] as InputTerminal;
|
||||
const collectionOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal;
|
||||
const multiDataInOne = terminals["multi data"]!["f1"] as InputTerminal;
|
||||
const multiDataInTwo = terminals["multi data"]!["f2"] as InputTerminal;
|
||||
multiDataInOne.connect(collectionOut);
|
||||
expect(multiDataInTwo.canAccept(collectionOut).canAccept).toBe(true);
|
||||
});
|
||||
it("rejects connecting output to input of same step", () => {
|
||||
const dataOut = terminals["simple data"]["out_file1"] as OutputTerminal;
|
||||
const dataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
const dataOut = terminals["simple data"]!["out_file1"] as OutputTerminal;
|
||||
const dataIn = terminals["simple data"]!["input"] as InputTerminal;
|
||||
expect(dataIn.canAccept(dataOut).canAccept).toBe(false);
|
||||
expect(dataIn.canAccept(dataOut).reason).toBe("Cannot connect output to input of same step.");
|
||||
});
|
||||
it("rejects paired input on multi-data input", () => {
|
||||
const multiDataIn = terminals["multi data"]["f1"] as InputTerminal;
|
||||
const pairedOut = terminals["paired input"]["output"] as OutputCollectionTerminal;
|
||||
const multiDataIn = terminals["multi data"]!["f1"] as InputTerminal;
|
||||
const pairedOut = terminals["paired input"]!["output"] as OutputCollectionTerminal;
|
||||
expect(multiDataIn.canAccept(pairedOut).canAccept).toBe(false);
|
||||
expect(multiDataIn.canAccept(pairedOut).reason).toBe(
|
||||
"Cannot attach paired inputs to multiple data parameters, only lists may be treated this way."
|
||||
);
|
||||
});
|
||||
it("rejects collections on multi data inputs if non-collection already connected", () => {
|
||||
const multiDataIn = terminals["multi data"]["f1"] as InputTerminal;
|
||||
const dataOut = terminals["data input"]["output"] as OutputTerminal;
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
const multiDataIn = terminals["multi data"]!["f1"] as InputTerminal;
|
||||
const dataOut = terminals["data input"]!["output"] as OutputTerminal;
|
||||
const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal;
|
||||
multiDataIn.connect(dataOut);
|
||||
expect(multiDataIn.canAccept(collectionOut).canAccept).toBe(false);
|
||||
expect(multiDataIn.canAccept(collectionOut).reason).toBe(
|
||||
@@ -184,16 +184,16 @@ describe("canAccept", () => {
|
||||
);
|
||||
});
|
||||
it("maps list:list over multi data input", () => {
|
||||
const collectionOut = terminals["list:list input"]["output"] as OutputCollectionTerminal;
|
||||
const multiDataIn = terminals["multi data"]["f1"] as InputTerminal;
|
||||
const collectionOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal;
|
||||
const multiDataIn = terminals["multi data"]!["f1"] as InputTerminal;
|
||||
expect(multiDataIn.canAccept(collectionOut).canAccept).toBe(true);
|
||||
multiDataIn.connect(collectionOut);
|
||||
expect(multiDataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
});
|
||||
it("rejects attaching multiple collections to a single multi data input", () => {
|
||||
const collectionOut = terminals["list:list input"]["output"] as OutputCollectionTerminal;
|
||||
const otherCollectionOut = terminals["list:list input"]["output"] as OutputCollectionTerminal;
|
||||
const multiDataIn = terminals["multi data"]["f1"] as InputTerminal;
|
||||
const collectionOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal;
|
||||
const otherCollectionOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal;
|
||||
const multiDataIn = terminals["multi data"]!["f1"] as InputTerminal;
|
||||
multiDataIn.connect(collectionOut);
|
||||
expect(multiDataIn.canAccept(otherCollectionOut).canAccept).toBe(false);
|
||||
expect(multiDataIn.canAccept(otherCollectionOut).reason).toBe(
|
||||
@@ -201,52 +201,52 @@ describe("canAccept", () => {
|
||||
);
|
||||
});
|
||||
it("rejects data -> collection connection", () => {
|
||||
const dataOut = terminals["data input"]["output"] as OutputTerminal;
|
||||
const collectionInput = terminals["any collection"]["input"] as InputCollectionTerminal;
|
||||
const dataOut = terminals["data input"]!["output"] as OutputTerminal;
|
||||
const collectionInput = terminals["any collection"]!["input"] as InputCollectionTerminal;
|
||||
expect(collectionInput.canAccept(dataOut).canAccept).toBe(false);
|
||||
expect(collectionInput.canAccept(dataOut).reason).toBe("Cannot attach a data output to a collection input.");
|
||||
});
|
||||
it("rejects optional data -> required data", () => {
|
||||
const optionalDataOut = terminals["optional data input"]["output"] as OutputTerminal;
|
||||
const dataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
const optionalDataOut = terminals["optional data input"]!["output"] as OutputTerminal;
|
||||
const dataIn = terminals["simple data"]!["input"] as InputTerminal;
|
||||
expect(dataIn.canAccept(optionalDataOut).canAccept).toBe(false);
|
||||
expect(dataIn.canAccept(optionalDataOut).reason).toBe(
|
||||
"Cannot connect an optional output to a non-optional input"
|
||||
);
|
||||
});
|
||||
it("rejects parameter to data connection", () => {
|
||||
const dataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
const dataIn = terminals["simple data"]!["input"] as InputTerminal;
|
||||
// # type system would reject this, but test runtime too
|
||||
const integerParam = terminals["integer parameter input"]["output"] as any;
|
||||
const integerParam = terminals["integer parameter input"]!["output"] as any;
|
||||
expect(dataIn.canAccept(integerParam).canAccept).toBe(false);
|
||||
expect(dataIn.canAccept(integerParam).reason).toBe("Cannot connect workflow parameter to data input.");
|
||||
});
|
||||
it("accepts integer parameter to integer parameter connection", () => {
|
||||
const integerInputParam = terminals["multi data"]["advanced|advanced_threshold"] as InputParameterTerminal;
|
||||
const integerOutputParam = terminals["integer parameter input"]["output"] as OutputParameterTerminal;
|
||||
const integerInputParam = terminals["multi data"]!["advanced|advanced_threshold"] as InputParameterTerminal;
|
||||
const integerOutputParam = terminals["integer parameter input"]!["output"] as OutputParameterTerminal;
|
||||
expect(integerInputParam.canAccept(integerOutputParam).canAccept).toBe(true);
|
||||
// regression test for https://github.com/galaxyproject/galaxy/issues/15417
|
||||
expect(integerInputParam.canAccept(integerOutputParam).reason).toBe(null);
|
||||
});
|
||||
it("rejects text to integer parameter connection", () => {
|
||||
const integerInputParam = terminals["multi data"]["advanced|advanced_threshold"] as InputParameterTerminal;
|
||||
const textOutputParam = terminals["text parameter input"]["output"] as OutputParameterTerminal;
|
||||
const integerInputParam = terminals["multi data"]!["advanced|advanced_threshold"] as InputParameterTerminal;
|
||||
const textOutputParam = terminals["text parameter input"]!["output"] as OutputParameterTerminal;
|
||||
expect(integerInputParam.canAccept(textOutputParam).canAccept).toBe(false);
|
||||
expect(integerInputParam.canAccept(textOutputParam).reason).toBe(
|
||||
"Cannot attach a text parameter to a integer input"
|
||||
);
|
||||
});
|
||||
it("rejects data to parameter connection", () => {
|
||||
const dataOut = terminals["data input"]["output"] as OutputTerminal;
|
||||
const integerInputParam = terminals["multi data"]["advanced|advanced_threshold"] as InputParameterTerminal;
|
||||
const dataOut = terminals["data input"]!["output"] as OutputTerminal;
|
||||
const integerInputParam = terminals["multi data"]!["advanced|advanced_threshold"] as InputParameterTerminal;
|
||||
expect(integerInputParam.canAccept(dataOut).canAccept).toBe(false);
|
||||
expect(integerInputParam.canAccept(dataOut).reason).toBe("Cannot attach a data parameter to a integer input");
|
||||
});
|
||||
it("rejects increasing map over if output connected to data input", () => {
|
||||
const dataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
const dataOut = terminals["simple data"]["out_file1"] as OutputTerminal;
|
||||
const constrainingDataIn = terminals["simple data 2"]["input"] as InputTerminal;
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]!["input"] as InputTerminal;
|
||||
const dataOut = terminals["simple data"]!["out_file1"] as OutputTerminal;
|
||||
const constrainingDataIn = terminals["simple data 2"]!["input"] as InputTerminal;
|
||||
const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal;
|
||||
// connect simple data to simple data 2
|
||||
constrainingDataIn.connect(dataOut);
|
||||
// now we can't connect a collection out to the data input of simple data
|
||||
@@ -256,11 +256,11 @@ describe("canAccept", () => {
|
||||
);
|
||||
});
|
||||
it("rejects increasing map over to list:list if data is mapped over a list input", () => {
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
const listListOut = terminals["list:list input"]["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
const dataOut = terminals["simple data"]["out_file1"] as OutputTerminal;
|
||||
const dataInTwo = terminals["simple data 2"]["input"] as InputTerminal;
|
||||
const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal;
|
||||
const listListOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]!["input"] as InputTerminal;
|
||||
const dataOut = terminals["simple data"]!["out_file1"] as OutputTerminal;
|
||||
const dataInTwo = terminals["simple data 2"]!["input"] as InputTerminal;
|
||||
dataIn.connect(collectionOut);
|
||||
dataInTwo.connect(dataOut);
|
||||
expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
@@ -275,11 +275,11 @@ describe("canAccept", () => {
|
||||
);
|
||||
});
|
||||
it("rejects attaching non-collection outputs to mapped-over inputs", () => {
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
const simpleDataOut = terminals["data input"]["output"] as OutputTerminal;
|
||||
const dataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
const dataOut = terminals["simple data"]["out_file1"] as OutputTerminal;
|
||||
const dataInTwo = terminals["simple data 2"]["input"] as InputTerminal;
|
||||
const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal;
|
||||
const simpleDataOut = terminals["data input"]!["output"] as OutputTerminal;
|
||||
const dataIn = terminals["simple data"]!["input"] as InputTerminal;
|
||||
const dataOut = terminals["simple data"]!["out_file1"] as OutputTerminal;
|
||||
const dataInTwo = terminals["simple data 2"]!["input"] as InputTerminal;
|
||||
dataIn.connect(collectionOut);
|
||||
dataInTwo.connect(dataOut);
|
||||
expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
@@ -303,12 +303,12 @@ describe("canAccept", () => {
|
||||
expect(rebuiltDataIn.canAccept(simpleDataOut).canAccept).toBe(true);
|
||||
});
|
||||
it("accepts attaching dataset to non-mapped over input for a mapped over step", () => {
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
const simpleDataOut = terminals["data input"]["output"] as OutputTerminal;
|
||||
const otherDataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
const multiSimpleInputOne = terminals["multiple simple data"]["input1"] as InputTerminal;
|
||||
const multiSimpleInputTwo = terminals["multiple simple data"]["queries_0|input2"] as InputTerminal;
|
||||
const multiSimpleOut = terminals["multiple simple data"]["out_file1"] as OutputTerminal;
|
||||
const collectionOut = terminals["list input"]?.["output"] as OutputCollectionTerminal;
|
||||
const simpleDataOut = terminals["data input"]?.["output"] as OutputTerminal;
|
||||
const otherDataIn = terminals["simple data"]?.["input"] as InputTerminal;
|
||||
const multiSimpleInputOne = terminals["multiple simple data"]?.["input1"] as InputTerminal;
|
||||
const multiSimpleInputTwo = terminals["multiple simple data"]?.["queries_0|input2"] as InputTerminal;
|
||||
const multiSimpleOut = terminals["multiple simple data"]?.["out_file1"] as OutputTerminal;
|
||||
// map over simple input
|
||||
multiSimpleInputOne.connect(collectionOut);
|
||||
// constrain input by connecting an output to same step
|
||||
@@ -318,7 +318,7 @@ describe("canAccept", () => {
|
||||
// can also connect a connection of the same map over state
|
||||
expect(multiSimpleInputTwo.canAccept(collectionOut).canAccept).toBe(true);
|
||||
// cannot connect connection of deeper nesting
|
||||
const listListOut = terminals["list:list input"]["output"] as OutputCollectionTerminal;
|
||||
const listListOut = terminals["list:list input"]?.["output"] as OutputCollectionTerminal;
|
||||
expect(multiSimpleInputTwo.canAccept(listListOut).canAccept).toBe(false);
|
||||
expect(multiSimpleInputTwo.canAccept(listListOut).reason).toBe(
|
||||
"Can't map over this input with output collection type - other inputs have an incompatible map over collection type. Disconnect inputs (and potentially outputs) and retry."
|
||||
@@ -331,11 +331,12 @@ describe("canAccept", () => {
|
||||
"Cannot attach non-collection output to mapped over input, consider disconnecting inputs and outputs to reset this input's mapping."
|
||||
);
|
||||
});
|
||||
// TODO: test mapOver reset when constraint removed
|
||||
it("resets mapOver when constraint is lifted", () => {
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
const dataOut = terminals["simple data"]["out_file1"] as OutputTerminal;
|
||||
const dataInTwo = terminals["simple data 2"]["input"] as InputTerminal;
|
||||
const collectionOut = terminals["list input"]?.["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]?.["input"] as InputTerminal;
|
||||
const dataOut = terminals["simple data"]?.["out_file1"] as OutputTerminal;
|
||||
const dataInTwo = terminals["simple data 2"]?.["input"] as InputTerminal;
|
||||
dataIn.connect(collectionOut);
|
||||
dataInTwo.connect(dataOut);
|
||||
expect(dataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
@@ -348,16 +349,16 @@ describe("canAccept", () => {
|
||||
expect(dataIn.mapOver).toEqual(NULL_COLLECTION_TYPE_DESCRIPTION);
|
||||
});
|
||||
it("rejects connecting incompatible connection types", () => {
|
||||
const pairedOut = terminals["paired input"]["output"] as OutputCollectionTerminal;
|
||||
const collectionIn = terminals["list collection input"]["input1"] as InputCollectionTerminal;
|
||||
const pairedOut = terminals["paired input"]!["output"] as OutputCollectionTerminal;
|
||||
const collectionIn = terminals["list collection input"]!["input1"] as InputCollectionTerminal;
|
||||
expect(collectionIn.canAccept(pairedOut).canAccept).toBe(false);
|
||||
expect(collectionIn.canAccept(pairedOut).reason).toBe("Incompatible collection type(s) for attachment.");
|
||||
});
|
||||
it("rejects mapping over collection input if other inputs have an incompatible map over collection type", () => {
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
const listListOut = terminals["list:list input"]["output"] as OutputCollectionTerminal;
|
||||
const listOneIn = terminals["two list inputs"]["kind|f1"] as InputCollectionTerminal;
|
||||
const listTwoIn = terminals["two list inputs"]["kind|f2"] as InputCollectionTerminal;
|
||||
const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal;
|
||||
const listListOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal;
|
||||
const listOneIn = terminals["two list inputs"]!["kind|f1"] as InputCollectionTerminal;
|
||||
const listTwoIn = terminals["two list inputs"]!["kind|f2"] as InputCollectionTerminal;
|
||||
listOneIn.connect(listListOut);
|
||||
expect(listTwoIn.canAccept(collectionOut).canAccept).toBe(false);
|
||||
expect(listTwoIn.canAccept(collectionOut).reason).toBe(
|
||||
@@ -365,12 +366,12 @@ describe("canAccept", () => {
|
||||
);
|
||||
});
|
||||
it("rejects mapping over collection input if outputs constrain input to incompatible collection type", () => {
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
const listListOut = terminals["list:list input"]["output"] as OutputCollectionTerminal;
|
||||
const listOneIn = terminals["two list inputs"]["kind|f1"] as InputCollectionTerminal;
|
||||
const listTwoIn = terminals["two list inputs"]["kind|f2"] as InputCollectionTerminal;
|
||||
const mapOverOut = terminals["two list inputs"]["out1"] as OutputTerminal;
|
||||
const dataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal;
|
||||
const listListOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal;
|
||||
const listOneIn = terminals["two list inputs"]!["kind|f1"] as InputCollectionTerminal;
|
||||
const listTwoIn = terminals["two list inputs"]!["kind|f2"] as InputCollectionTerminal;
|
||||
const mapOverOut = terminals["two list inputs"]!["out1"] as OutputTerminal;
|
||||
const dataIn = terminals["simple data"]!["input"] as InputTerminal;
|
||||
listOneIn.connect(listListOut);
|
||||
dataIn.connect(mapOverOut);
|
||||
listOneIn.disconnect(listListOut);
|
||||
@@ -381,17 +382,17 @@ describe("canAccept", () => {
|
||||
);
|
||||
});
|
||||
it("tracks transitive map over", () => {
|
||||
const collectionOut = terminals["list:list input"]["output"] as OutputCollectionTerminal;
|
||||
const listListListOut = terminals["list:list:list input"]["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
const simpleDataOut = terminals["simple data"]["out_file1"] as OutputTerminal;
|
||||
const collectionOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal;
|
||||
const listListListOut = terminals["list:list:list input"]!["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]!["input"] as InputTerminal;
|
||||
const simpleDataOut = terminals["simple data"]!["out_file1"] as OutputTerminal;
|
||||
dataIn.connect(collectionOut);
|
||||
expect(dataIn.mapOver).toEqual({ collectionType: "list:list", isCollection: true, rank: 2 });
|
||||
const otherDataIn = terminals["multi data"]["f1"] as InputTerminal;
|
||||
const otherDataIn = terminals["multi data"]!["f1"] as InputTerminal;
|
||||
expect(otherDataIn.canAccept(simpleDataOut).canAccept).toBe(true);
|
||||
otherDataIn.connect(simpleDataOut);
|
||||
expect(otherDataIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
const otherDataInTwo = terminals["multi data"]["f2"] as InputTerminal;
|
||||
const otherDataInTwo = terminals["multi data"]!["f2"] as InputTerminal;
|
||||
expect(otherDataInTwo.canAccept(collectionOut).canAccept).toBe(true);
|
||||
expect(otherDataInTwo.canAccept(listListListOut).canAccept).toBe(false);
|
||||
expect(otherDataInTwo.canAccept(listListListOut).reason).toBe(
|
||||
@@ -399,25 +400,25 @@ describe("canAccept", () => {
|
||||
);
|
||||
});
|
||||
it("tracks transitive map over through collection inputs", () => {
|
||||
const collectionOut = terminals["list:list input"]["output"] as OutputCollectionTerminal;
|
||||
const collectionIn = terminals["list collection input"]["input1"] as InputCollectionTerminal;
|
||||
const collectionOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal;
|
||||
const collectionIn = terminals["list collection input"]!["input1"] as InputCollectionTerminal;
|
||||
expect(collectionIn.canAccept(collectionOut).canAccept).toBe(true);
|
||||
expect(collectionIn.isMappedOver()).toBe(false);
|
||||
collectionIn.connect(collectionOut);
|
||||
expect(collectionIn.isMappedOver()).toBe(true);
|
||||
expect(collectionIn.mapOver).toEqual({ collectionType: "list", isCollection: true, rank: 1 });
|
||||
const intermediateOut = terminals["list collection input"]["out_file1"] as OutputCollectionTerminal;
|
||||
const otherListIn = terminals["list collection input 2"]["input1"] as InputCollectionTerminal;
|
||||
const intermediateOut = terminals["list collection input"]!["out_file1"] as OutputCollectionTerminal;
|
||||
const otherListIn = terminals["list collection input 2"]!["input1"] as InputCollectionTerminal;
|
||||
expect(otherListIn.canAccept(intermediateOut).canAccept).toBe(true);
|
||||
otherListIn.connect(intermediateOut);
|
||||
expect(otherListIn.mapOver).toEqual(NULL_COLLECTION_TYPE_DESCRIPTION);
|
||||
});
|
||||
it("rejects connections to input collection constrained by output connection", () => {
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
const collectionIn = terminals["list collection input"]["input1"] as InputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
const mappedOverListOut = terminals["list collection input"]["out_file1"] as OutputCollectionTerminal;
|
||||
const listListOut = terminals["list:list input"]["output"] as OutputCollectionTerminal;
|
||||
const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal;
|
||||
const collectionIn = terminals["list collection input"]!["input1"] as InputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]!["input"] as InputTerminal;
|
||||
const mappedOverListOut = terminals["list collection input"]!["out_file1"] as OutputCollectionTerminal;
|
||||
const listListOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal;
|
||||
// This constrains collectionIn to list because it's output is mapped over
|
||||
dataIn.connect(mappedOverListOut);
|
||||
collectionIn.connect(collectionOut);
|
||||
@@ -430,12 +431,12 @@ describe("canAccept", () => {
|
||||
);
|
||||
});
|
||||
it("rejects connections to input collection constrained by other input", () => {
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]["input"] as InputTerminal;
|
||||
const listOneIn = terminals["two list inputs"]["kind|f1"] as InputCollectionTerminal;
|
||||
const listTwoIn = terminals["two list inputs"]["kind|f2"] as InputCollectionTerminal;
|
||||
const mapOverOut = terminals["two list inputs"]["out1"] as OutputTerminal;
|
||||
const listListOut = terminals["list:list input"]["output"] as OutputCollectionTerminal;
|
||||
const collectionOut = terminals["list input"]!["output"] as OutputCollectionTerminal;
|
||||
const dataIn = terminals["simple data"]!["input"] as InputTerminal;
|
||||
const listOneIn = terminals["two list inputs"]!["kind|f1"] as InputCollectionTerminal;
|
||||
const listTwoIn = terminals["two list inputs"]!["kind|f2"] as InputCollectionTerminal;
|
||||
const mapOverOut = terminals["two list inputs"]!["out1"] as OutputTerminal;
|
||||
const listListOut = terminals["list:list input"]!["output"] as OutputCollectionTerminal;
|
||||
// This constrains "two list inputs" to list:list because it's output is mapped over
|
||||
listOneIn.connect(listListOut);
|
||||
dataIn.connect(mapOverOut);
|
||||
@@ -446,10 +447,10 @@ describe("canAccept", () => {
|
||||
);
|
||||
});
|
||||
it("disconnects invalid input terminals", () => {
|
||||
const dataOut = terminals["simple data"]["out_file1"] as OutputTerminal;
|
||||
const dataIn = terminals["simple data 2"]["input"] as InputTerminal;
|
||||
const dataOut = terminals["simple data"]!["out_file1"] as OutputTerminal;
|
||||
const dataIn = terminals["simple data 2"]!["input"] as InputTerminal;
|
||||
dataIn.connect(dataOut);
|
||||
const oldStep = stepStore.getStep(dataOut.stepId);
|
||||
const oldStep = stepStore.getStep(dataOut.stepId) as Step;
|
||||
const stepOutput = oldStep.outputs[0] as DataOutput;
|
||||
stepOutput["extensions"] = ["ab1"];
|
||||
dataOut.datatypes = ["ab1"];
|
||||
@@ -462,8 +463,8 @@ describe("canAccept", () => {
|
||||
);
|
||||
});
|
||||
it("disconnects invalid output terminals", () => {
|
||||
const dataOut = terminals["simple data"]["out_file1"] as OutputTerminal;
|
||||
const dataIn = terminals["simple data 2"]["input"] as InputTerminal;
|
||||
const dataOut = terminals["simple data"]!["out_file1"] as OutputTerminal;
|
||||
const dataIn = terminals["simple data 2"]!["input"] as InputTerminal;
|
||||
dataIn.connect(dataOut);
|
||||
dataIn.datatypes = ["ab1"];
|
||||
dataIn.destroyInvalidConnections();
|
||||
@@ -475,14 +476,14 @@ describe("canAccept", () => {
|
||||
);
|
||||
});
|
||||
it("resolves collection type source", () => {
|
||||
const filterFailedInput = terminals["filter_failed"]["input"] as InputCollectionTerminal;
|
||||
const filterFailedOutput = terminals["filter_failed"]["output"] as OutputCollectionTerminal;
|
||||
const filterFailedInput = terminals["filter_failed"]?.["input"] as InputCollectionTerminal;
|
||||
const filterFailedOutput = terminals["filter_failed"]?.["output"] as OutputCollectionTerminal;
|
||||
expect(filterFailedOutput.collectionType).toBe(ANY_COLLECTION_TYPE_DESCRIPTION);
|
||||
const collectionOut = terminals["list input"]["output"] as OutputCollectionTerminal;
|
||||
const collectionOut = terminals["list input"]?.["output"] as OutputCollectionTerminal;
|
||||
filterFailedInput.connect(collectionOut);
|
||||
expect(rebuildTerminal(filterFailedOutput).collectionType).toStrictEqual(new CollectionTypeDescription("list"));
|
||||
filterFailedInput.disconnect(collectionOut);
|
||||
const listPairedOutput = terminals["list:paired input"]["output"] as OutputCollectionTerminal;
|
||||
const listPairedOutput = terminals["list:paired input"]?.["output"] as OutputCollectionTerminal;
|
||||
filterFailedInput.connect(listPairedOutput);
|
||||
expect(rebuildTerminal(filterFailedOutput).collectionType).toStrictEqual(
|
||||
new CollectionTypeDescription("list:paired")
|
||||
@@ -502,7 +503,7 @@ describe("Input terminal", () => {
|
||||
Object.values(simpleSteps).map((step) => {
|
||||
stepStore.addStep(step);
|
||||
terminals[step.id] = {};
|
||||
const stepTerminals = terminals[step.id];
|
||||
const stepTerminals = terminals[step.id]!;
|
||||
step.inputs?.map((input) => {
|
||||
stepTerminals[input.name] = terminalFactory(step.id, input, testDatatypesMapper);
|
||||
});
|
||||
@@ -516,9 +517,9 @@ describe("Input terminal", () => {
|
||||
expect(stepStore.getStep(1)).toEqual(simpleSteps["1"]);
|
||||
});
|
||||
it("infers correct state", () => {
|
||||
const firstInputTerminal = terminals[1]["input"] as InputTerminal;
|
||||
const firstInputTerminal = terminals[1]!["input"] as InputTerminal;
|
||||
expect(firstInputTerminal).toBeInstanceOf(InputTerminal);
|
||||
const dataInputOutputTerminal = terminals[0]["output"] as OutputTerminal;
|
||||
const dataInputOutputTerminal = terminals[0]!["output"] as OutputTerminal;
|
||||
expect(dataInputOutputTerminal).toBeInstanceOf(OutputTerminal);
|
||||
expect(firstInputTerminal.connections.length).toBe(1);
|
||||
expect(firstInputTerminal.mapOver).toBe(NULL_COLLECTION_TYPE_DESCRIPTION);
|
||||
@@ -538,9 +539,9 @@ describe("Input terminal", () => {
|
||||
expect(firstInputTerminal._producesAcceptableDatatype(dataInputOutputTerminal).canAccept).toBe(true);
|
||||
});
|
||||
it("can accept new connection", () => {
|
||||
const firstInputTerminal = terminals[1]["input"] as InputTerminal;
|
||||
const dataInputOutputTerminal = terminals[0]["output"] as OutputTerminal;
|
||||
const connection = firstInputTerminal.connections[0];
|
||||
const firstInputTerminal = terminals[1]!["input"] as InputTerminal;
|
||||
const dataInputOutputTerminal = terminals[0]!["output"] as OutputTerminal;
|
||||
const connection = firstInputTerminal.connections[0]!;
|
||||
expect(firstInputTerminal.canAccept(dataInputOutputTerminal).canAccept).toBe(false);
|
||||
expect(dataInputOutputTerminal.validInputTerminals().length).toBe(0);
|
||||
firstInputTerminal.disconnect(connection);
|
||||
@@ -550,9 +551,9 @@ describe("Input terminal", () => {
|
||||
expect(firstInputTerminal.canAccept(dataInputOutputTerminal).canAccept).toBe(false);
|
||||
});
|
||||
it("will maintain invalid connections", () => {
|
||||
const connection = connectionStore.connections[0];
|
||||
const connection = connectionStore.connections[0]!;
|
||||
connection.output.name = "I don't exist";
|
||||
const firstInputTerminal = terminals[1]["input"] as InputTerminal;
|
||||
const firstInputTerminal = terminals[1]?.["input"] as InputTerminal;
|
||||
const invalidTerminals = firstInputTerminal.getConnectedTerminals();
|
||||
expect(invalidTerminals.length).toBe(1);
|
||||
expect(invalidTerminals[0]).toBeInstanceOf(InvalidOutputTerminal);
|
||||
|
||||
@@ -19,6 +19,7 @@ import type {
|
||||
} from "@/stores/workflowStepStore";
|
||||
import type { DatatypesMapperModel } from "@/components/Datatypes/model";
|
||||
import Vue from "vue";
|
||||
import { assertDefined } from "@/utils/assertions";
|
||||
|
||||
export class ConnectionAcceptable {
|
||||
reason: string | null;
|
||||
@@ -108,15 +109,20 @@ class Terminal extends EventEmitter {
|
||||
}
|
||||
outputVal = val.effectiveMapOver(description);
|
||||
}
|
||||
|
||||
const effectiveMapOver = this._effectiveMapOver(outputVal);
|
||||
|
||||
if (!this.localMapOver.equal(effectiveMapOver)) {
|
||||
this.stepStore.changeStepInputMapOver(this.stepId, this.name, effectiveMapOver);
|
||||
this.localMapOver = effectiveMapOver;
|
||||
}
|
||||
|
||||
if (
|
||||
!this.mapOver.equal(effectiveMapOver) &&
|
||||
(effectiveMapOver.isCollection ||
|
||||
!Object.values(this.stepStore.stepInputMapOver[this.stepId]).find((mapOver) => mapOver.isCollection))
|
||||
!Object.values(this.stepStore.stepInputMapOver[this.stepId] ?? []).find(
|
||||
(mapOver) => mapOver.isCollection
|
||||
))
|
||||
) {
|
||||
this.stepStore.changeStepMapOver(this.stepId, effectiveMapOver);
|
||||
}
|
||||
@@ -176,8 +182,8 @@ class BaseInputTerminal extends Terminal {
|
||||
this.datatypes = attr.input.datatypes;
|
||||
this.multiple = attr.input.multiple;
|
||||
this.optional = attr.input.optional;
|
||||
if (this.stepStore.stepInputMapOver[this.stepId] && this.stepStore.stepInputMapOver[this.stepId][this.name]) {
|
||||
this.localMapOver = this.stepStore.stepInputMapOver[this.stepId][this.name];
|
||||
if (this.stepStore.stepInputMapOver[this.stepId] && this.stepStore.stepInputMapOver[this.stepId]?.[this.name]) {
|
||||
this.localMapOver = this.stepStore.stepInputMapOver[this.stepId]![this.name]!;
|
||||
} else {
|
||||
this.localMapOver = NULL_COLLECTION_TYPE_DESCRIPTION;
|
||||
}
|
||||
@@ -229,11 +235,15 @@ class BaseInputTerminal extends Terminal {
|
||||
}
|
||||
Array.from(new Set(outputStepIds)).forEach((stepId) => {
|
||||
const step = this.stepStore.getStep(stepId);
|
||||
// step must have an output, since it is or was connected to this step
|
||||
const terminalSource = step.outputs[0];
|
||||
if (terminalSource) {
|
||||
const terminal = terminalFactory(step.id, terminalSource, this.datatypesMapper);
|
||||
terminal.resetMappingIfNeeded();
|
||||
if (step) {
|
||||
// step must have an output, since it is or was connected to this step
|
||||
const terminalSource = step.outputs[0];
|
||||
if (terminalSource) {
|
||||
const terminal = terminalFactory(step.id, terminalSource, this.datatypesMapper);
|
||||
terminal.resetMappingIfNeeded();
|
||||
}
|
||||
} else {
|
||||
console.error(`Invalid step. Could not fine step with id ${stepId} in store.`);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -273,9 +283,14 @@ class BaseInputTerminal extends Terminal {
|
||||
_collectionAttached() {
|
||||
const outputTerminals = this._getOutputTerminals();
|
||||
return outputTerminals.some((outputTerminal) => {
|
||||
const output = this.stepStore
|
||||
.getStep(outputTerminal.stepId)
|
||||
.outputs.find((output) => output.name == outputTerminal.name);
|
||||
const step = this.stepStore.getStep(outputTerminal.stepId);
|
||||
|
||||
if (!step) {
|
||||
console.error(`Invalid step. Could not find step with id ${outputTerminal.stepId} in store.`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const output = step.outputs.find((output) => output.name == outputTerminal.name);
|
||||
|
||||
if (
|
||||
output &&
|
||||
@@ -353,11 +368,14 @@ class BaseInputTerminal extends Terminal {
|
||||
outputStep.post_job_actions &&
|
||||
postJobActionKey in outputStep.post_job_actions
|
||||
) {
|
||||
terminalSource = {
|
||||
const extensionType = outputStep.post_job_actions![postJobActionKey]!.action_arguments.newtype;
|
||||
|
||||
(terminalSource as DataOutput | CollectionOutput) = {
|
||||
...terminalSource,
|
||||
extensions: [outputStep.post_job_actions[postJobActionKey].action_arguments.newtype],
|
||||
extensions: extensionType ? [extensionType] : [],
|
||||
};
|
||||
}
|
||||
|
||||
return terminalFactory(outputStep.id, terminalSource, this.datatypesMapper);
|
||||
});
|
||||
}
|
||||
@@ -529,9 +547,11 @@ export class InputCollectionTerminal extends BaseInputTerminal {
|
||||
const canMatch = collectionTypes.some((collectionType) => collectionType.canMatch(otherCollectionType));
|
||||
if (!canMatch) {
|
||||
for (const collectionTypeIndex in collectionTypes) {
|
||||
const collectionType = collectionTypes[collectionTypeIndex];
|
||||
const collectionType = collectionTypes[collectionTypeIndex]!;
|
||||
|
||||
if (otherCollectionType.canMapOver(collectionType)) {
|
||||
const effectiveMapOver = otherCollectionType.effectiveMapOver(collectionType);
|
||||
|
||||
if (effectiveMapOver != NULL_COLLECTION_TYPE_DESCRIPTION) {
|
||||
return effectiveMapOver;
|
||||
}
|
||||
@@ -613,6 +633,8 @@ class BaseOutputTerminal extends Terminal {
|
||||
getConnectedTerminals(): InputTerminalsAndInvalid[] {
|
||||
return this.connections.map((connection) => {
|
||||
const inputStep = this.stepStore.getStep(connection.input.stepId);
|
||||
assertDefined(inputStep, `Invalid step. Could not find step with id ${connection.input.stepId} in store.`);
|
||||
|
||||
const extraStepInput = this.stepStore.getStepExtraInputs(inputStep.id);
|
||||
const terminalSource = [...extraStepInput, ...inputStep.inputs].find(
|
||||
(input) => input.name === connection.input.name
|
||||
@@ -703,6 +725,8 @@ export class OutputCollectionTerminal extends BaseOutputTerminal {
|
||||
if (connection) {
|
||||
const outputStep = this.stepStore.getStep(connection.output.stepId);
|
||||
const inputStep = this.stepStore.getStep(this.stepId);
|
||||
assertDefined(inputStep, `Invalid step. Could not find step with id ${connection.input.stepId} in store.`);
|
||||
|
||||
if (outputStep) {
|
||||
const stepOutput = outputStep.outputs.find((output) => output.name == connection.output.name);
|
||||
const stepInput = inputStep.inputs.find((input) => input.name === this.collectionTypeSource);
|
||||
@@ -771,17 +795,20 @@ export function producesAcceptableDatatype(
|
||||
otherDatatypes: string[]
|
||||
) {
|
||||
for (const t in inputDatatypes) {
|
||||
const thisDatatype = inputDatatypes[t];
|
||||
if (thisDatatype == "input") {
|
||||
const thisDatatype = inputDatatypes[t]!;
|
||||
|
||||
if (thisDatatype === "input") {
|
||||
return new ConnectionAcceptable(true, null);
|
||||
}
|
||||
|
||||
// FIXME: No idea what to do about case when datatype is 'input'
|
||||
const validMatch = otherDatatypes.some(
|
||||
(otherDatatype) =>
|
||||
otherDatatype == "input" ||
|
||||
otherDatatype == "_sniff_" ||
|
||||
otherDatatype === "input" ||
|
||||
otherDatatype === "_sniff_" ||
|
||||
datatypesMapper.isSubType(otherDatatype, thisDatatype)
|
||||
);
|
||||
|
||||
if (validMatch) {
|
||||
return new ConnectionAcceptable(true, null);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { Last } from "types/utilityTypes";
|
||||
|
||||
export const zoomLevels = [
|
||||
0.1, 0.2, 0.25, 0.33, 0.5, 0.67, 0.75, 0.8, 0.9, 1, 1.1, 1.25, 1.33, 1.5, 2, 2.5, 3, 4, 5,
|
||||
] as const;
|
||||
@@ -5,7 +7,7 @@ export const zoomLevels = [
|
||||
export type ZoomLevel = (typeof zoomLevels)[number];
|
||||
|
||||
export const minZoom = zoomLevels[0];
|
||||
export const maxZoom = zoomLevels[zoomLevels.length - 1];
|
||||
export const maxZoom = zoomLevels[zoomLevels.length - 1] as Last<typeof zoomLevels>;
|
||||
|
||||
/**
|
||||
* Finds the closest snapped zoom level
|
||||
@@ -30,7 +32,7 @@ export function getZoomInLevel(zoom: number): ZoomLevel {
|
||||
if (index === zoomLevels.length - 1) {
|
||||
return snapped;
|
||||
} else {
|
||||
return zoomLevels[index + 1];
|
||||
return zoomLevels[index + 1] as ZoomLevel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +48,7 @@ export function getZoomOutLevel(zoom: number): ZoomLevel {
|
||||
if (index === 0) {
|
||||
return snapped;
|
||||
} else {
|
||||
return zoomLevels[index - 1];
|
||||
return zoomLevels[index - 1] as ZoomLevel;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { useWorkflowStepStore } from "@/stores/workflowStepStore";
|
||||
import { state } from "@/store/tagStore";
|
||||
import Vue from "vue";
|
||||
|
||||
interface InvalidConnections {
|
||||
@@ -49,6 +48,20 @@ interface TerminalToInputTerminals {
|
||||
[index: string]: InputTerminal[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Pushes a value to an array in an object, if the array exists. Else creates a new array containing value.
|
||||
* @param object Object which contains array
|
||||
* @param key Key which array is in
|
||||
* @param value Value to push
|
||||
*/
|
||||
function pushOrSet<T>(object: { [key: string | number]: Array<T> }, key: string | number, value: T) {
|
||||
if (key in object) {
|
||||
object[key]!.push(value);
|
||||
} else {
|
||||
object[key] = [value];
|
||||
}
|
||||
}
|
||||
|
||||
export const useConnectionStore = defineStore("workflowConnectionStore", {
|
||||
state: (): State => ({
|
||||
connections: [] as Connection[],
|
||||
@@ -60,9 +73,7 @@ export const useConnectionStore = defineStore("workflowConnectionStore", {
|
||||
state.connections.map((connection) => {
|
||||
const terminals = getTerminals(connection);
|
||||
const inputTerminalId = getTerminalId(terminals.input);
|
||||
inputTerminalId in inputTerminalToOutputTerminals
|
||||
? inputTerminalToOutputTerminals[inputTerminalId].push(terminals.output)
|
||||
: (inputTerminalToOutputTerminals[inputTerminalId] = [terminals.output]);
|
||||
pushOrSet(inputTerminalToOutputTerminals, inputTerminalId, terminals.output);
|
||||
});
|
||||
return (terminalId: string): OutputTerminal[] => {
|
||||
return inputTerminalToOutputTerminals[terminalId] || [];
|
||||
@@ -73,9 +84,7 @@ export const useConnectionStore = defineStore("workflowConnectionStore", {
|
||||
state.connections.map((connection) => {
|
||||
const terminals = getTerminals(connection);
|
||||
const outputTerminalId = getTerminalId(terminals.output);
|
||||
outputTerminalId in outputTerminalToInputTerminals
|
||||
? outputTerminalToInputTerminals[outputTerminalId].push(terminals.input)
|
||||
: (outputTerminalToInputTerminals[outputTerminalId] = [terminals.input]);
|
||||
pushOrSet(outputTerminalToInputTerminals, outputTerminalId, terminals.input);
|
||||
});
|
||||
return (terminalId: string): BaseTerminal[] => {
|
||||
return outputTerminalToInputTerminals[terminalId] || [];
|
||||
@@ -86,17 +95,10 @@ export const useConnectionStore = defineStore("workflowConnectionStore", {
|
||||
state.connections.map((connection) => {
|
||||
const terminals = getTerminals(connection);
|
||||
const outputTerminalId = getTerminalId(terminals.output);
|
||||
if (outputTerminalId in terminalToConnection) {
|
||||
terminalToConnection[outputTerminalId].push(connection);
|
||||
} else {
|
||||
terminalToConnection[outputTerminalId] = [connection];
|
||||
}
|
||||
pushOrSet(terminalToConnection, outputTerminalId, connection);
|
||||
|
||||
const inputTerminalId = getTerminalId(terminals.input);
|
||||
if (inputTerminalId in terminalToConnection) {
|
||||
terminalToConnection[inputTerminalId].push(connection);
|
||||
} else {
|
||||
terminalToConnection[inputTerminalId] = [connection];
|
||||
}
|
||||
pushOrSet(terminalToConnection, inputTerminalId, connection);
|
||||
});
|
||||
return (terminalId: string): Connection[] => {
|
||||
return terminalToConnection[terminalId] || [];
|
||||
@@ -105,12 +107,8 @@ export const useConnectionStore = defineStore("workflowConnectionStore", {
|
||||
getConnectionsForStep(state: State) {
|
||||
const stepToConnections: { [index: number]: Connection[] } = {};
|
||||
state.connections.map((connection) => {
|
||||
connection.input.stepId in stepToConnections
|
||||
? stepToConnections[connection.input.stepId].push(connection)
|
||||
: (stepToConnections[connection.input.stepId] = [connection]);
|
||||
connection.output.stepId in stepToConnections
|
||||
? stepToConnections[connection.output.stepId].push(connection)
|
||||
: (stepToConnections[connection.output.stepId] = [connection]);
|
||||
pushOrSet(stepToConnections, connection.input.stepId, connection);
|
||||
pushOrSet(stepToConnections, connection.output.stepId, connection);
|
||||
});
|
||||
return (stepId: number): Connection[] => stepToConnections[stepId] || [];
|
||||
},
|
||||
|
||||
@@ -51,17 +51,17 @@ export const useWorkflowStateStore = defineStore("workflowStateStore", {
|
||||
},
|
||||
actions: {
|
||||
setInputTerminalPosition(stepId: number, inputName: string, position: TerminalPosition) {
|
||||
if (!this.inputTerminals[stepId]) {
|
||||
Vue.set(this.inputTerminals, stepId, { [inputName]: position });
|
||||
if (this.inputTerminals[stepId]) {
|
||||
Vue.set(this.inputTerminals[stepId]!, inputName, position);
|
||||
} else {
|
||||
Vue.set(this.inputTerminals[stepId], inputName, position);
|
||||
Vue.set(this.inputTerminals, stepId, { [inputName]: position });
|
||||
}
|
||||
},
|
||||
setOutputTerminalPosition(stepId: number, outputName: string, position: TerminalPosition) {
|
||||
if (!this.outputTerminals[stepId]) {
|
||||
Vue.set(this.outputTerminals, stepId, { [outputName]: position });
|
||||
if (this.outputTerminals[stepId]) {
|
||||
Vue.set(this.outputTerminals[stepId]!, outputName, position);
|
||||
} else {
|
||||
Vue.set(this.outputTerminals[stepId], outputName, position);
|
||||
Vue.set(this.outputTerminals, stepId, { [outputName]: position });
|
||||
}
|
||||
},
|
||||
deleteInputTerminalPosition(stepId: number, inputName: string) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { defineStore } from "pinia";
|
||||
import { useConnectionStore } from "@/stores/workflowConnectionStore";
|
||||
import { Connection } from "@/stores/workflowConnectionStore";
|
||||
import type { CollectionTypeDescriptor } from "@/components/Workflow/Editor/modules/collectionTypeDescription";
|
||||
import { assertDefined } from "@/utils/assertions";
|
||||
|
||||
interface State {
|
||||
steps: { [index: string]: Step };
|
||||
@@ -149,7 +150,7 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
|
||||
}),
|
||||
getters: {
|
||||
getStep(state: State) {
|
||||
return (stepId: number): Step => {
|
||||
return (stepId: number): Step | undefined => {
|
||||
return state.steps[stepId.toString()];
|
||||
};
|
||||
},
|
||||
@@ -169,7 +170,7 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
|
||||
extensions: [],
|
||||
};
|
||||
if (extraInputs[step.id]) {
|
||||
extraInputs[step.id].push(terminalSource);
|
||||
extraInputs[step.id]!.push(terminalSource);
|
||||
} else {
|
||||
extraInputs[step.id] = [terminalSource];
|
||||
}
|
||||
@@ -226,13 +227,17 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
|
||||
},
|
||||
changeStepInputMapOver(stepId: number, inputName: string, mapOver: CollectionTypeDescriptor) {
|
||||
if (this.stepInputMapOver[stepId]) {
|
||||
Vue.set(this.stepInputMapOver[stepId], inputName, mapOver);
|
||||
Vue.set(this.stepInputMapOver[stepId]!, inputName, mapOver);
|
||||
} else {
|
||||
Vue.set(this.stepInputMapOver, stepId, { [inputName]: mapOver });
|
||||
}
|
||||
},
|
||||
addConnection(connection: Connection) {
|
||||
const inputStep = this.getStep(connection.input.stepId);
|
||||
assertDefined(
|
||||
inputStep,
|
||||
`Failed to add connection, because step with id ${connection.input.stepId} is undefined`
|
||||
);
|
||||
const input = inputStep.inputs.find((input) => input.name === connection.input.name);
|
||||
const connectionLink: ConnectionOutputLink = {
|
||||
output_name: connection.output.name,
|
||||
@@ -252,6 +257,11 @@ export const useWorkflowStepStore = defineStore("workflowStepStore", {
|
||||
},
|
||||
removeConnection(connection: Connection) {
|
||||
const inputStep = this.getStep(connection.input.stepId);
|
||||
assertDefined(
|
||||
inputStep,
|
||||
`Failed to remove connection, because step with id ${connection.input.stepId} is undefined`
|
||||
);
|
||||
|
||||
if (this.getStepExtraInputs(inputStep.id).find((input) => connection.input.name === input.name)) {
|
||||
inputStep.input_connections[connection.input.name] = undefined;
|
||||
} else {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Asserts that a value is not undefined or null
|
||||
* @param value value to test
|
||||
* @param errorMessage optional error message
|
||||
*/
|
||||
export function assertDefined<T>(value: T, errorMessage?: string): asserts value is NonNullable<T> {
|
||||
if (value === undefined || value === null) {
|
||||
const message = errorMessage ?? `Value is undefined or null`;
|
||||
throw message;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HistoryFilters } from "components/History/HistoryFilters";
|
||||
import { HistoryFilters } from "@/components/History/HistoryFilters";
|
||||
|
||||
const filterTexts = [
|
||||
"name:'name of item' hid>10 hid<100 create-time>'2021-01-01' update-time<'2022-01-01' state:success extension:ext tag:first deleted:False visible:'TRUE'",
|
||||
|
||||
@@ -13,58 +13,62 @@ type Converter<T> = (value: T) => T;
|
||||
type Handler<T> = (v: T, q: T) => boolean;
|
||||
|
||||
/** Add comparison aliases i.e. '*>value' is converted to '*_gt=value' */
|
||||
const defaultValidAliases = [
|
||||
const defaultValidAliases: Array<[string, string]> = [
|
||||
[">", "_gt"],
|
||||
["<", "_lt"],
|
||||
];
|
||||
|
||||
const operatorForAlias: Record<string, string> = {
|
||||
const operatorForAlias = {
|
||||
lt: "<",
|
||||
le: "<=",
|
||||
ge: ">=",
|
||||
gt: ">",
|
||||
eq: ":",
|
||||
};
|
||||
} as const satisfies Record<string, string>;
|
||||
|
||||
type OperatorForAlias = typeof operatorForAlias;
|
||||
type Alias = keyof OperatorForAlias;
|
||||
type Operator = OperatorForAlias[Alias];
|
||||
|
||||
/** Converts user input to backend compatible date
|
||||
* @param {string} value
|
||||
* @returns {Number} seconds since epoch
|
||||
* @param value
|
||||
* @returns seconds since epoch
|
||||
* */
|
||||
export function toDate(value: string): number {
|
||||
return Date.parse(value) / 1000;
|
||||
}
|
||||
|
||||
/** Converts user input for case-insensitive filtering
|
||||
* @param {string} value
|
||||
* @returns {string} Lowercase value
|
||||
* @param value
|
||||
* @returns Lowercase value
|
||||
* */
|
||||
export function toLower<T>(value: T): string {
|
||||
return String(value).toLowerCase();
|
||||
}
|
||||
|
||||
/** Converts user input to boolean
|
||||
* @param {string} value
|
||||
* @returns {boolean} true if value is 'true', false if value is 'false'
|
||||
* @param value
|
||||
* @returns true if value is 'true', false if value is 'false'
|
||||
* */
|
||||
export function toBool<T>(value: T): boolean {
|
||||
return toLower(value) === "true";
|
||||
}
|
||||
|
||||
/** Converts user input to lower case and strips quotation marks
|
||||
* @param {string} value
|
||||
* @returns {string} Lowercase value without quotation marks
|
||||
* @param value
|
||||
* @returns Lowercase value without quotation marks
|
||||
* */
|
||||
export function toLowerNoQuotes<T>(value: T): string {
|
||||
return toLower(value).split("'").join("");
|
||||
return toLower(value).replace(/'/g, "");
|
||||
}
|
||||
|
||||
/** Converts name tags starting with '#' to 'name:'
|
||||
* @param {string} value
|
||||
* @returns {string} Lowercase value with 'name:' replaced with '#'
|
||||
* @param value
|
||||
* @returns Lowercase value with 'name:' replaced with '#'
|
||||
* */
|
||||
export function expandNameTag(value: string | object): string {
|
||||
if (value && typeof value === "string" && value.startsWith("#")) {
|
||||
value = value.replace("#", "name:");
|
||||
if (value && typeof value === "string") {
|
||||
value = value.replace(/^#/, "name:");
|
||||
}
|
||||
return toLower(value);
|
||||
}
|
||||
@@ -73,7 +77,7 @@ export function expandNameTag(value: string | object): string {
|
||||
* @param alias
|
||||
* @returns Arithmetic operator, e.g.: '>'
|
||||
* */
|
||||
export function getOperatorForAlias(alias: string): string {
|
||||
export function getOperatorForAlias(alias: Alias): Operator {
|
||||
return operatorForAlias[alias];
|
||||
}
|
||||
|
||||
@@ -86,9 +90,9 @@ type HandlerReturn<T> = {
|
||||
|
||||
/**
|
||||
* Checks if a query value is equal to the item value
|
||||
* @param {string} attribute of the content item
|
||||
* @param {string} [query] parameter if the attribute does not match the server query key
|
||||
* @param {function} [converter] if item attribute value has to be transformed e.g. to a date.
|
||||
* @param attribute of the content item
|
||||
* @param query parameter if the attribute does not match the server query key
|
||||
* @param converter if item attribute value has to be transformed e.g. to a date.
|
||||
*/
|
||||
export function equals<T>(attribute: string, query?: string, converter?: Converter<T>): HandlerReturn<T> {
|
||||
return {
|
||||
@@ -107,9 +111,9 @@ export function equals<T>(attribute: string, query?: string, converter?: Convert
|
||||
|
||||
/**
|
||||
* Checks if a query value is part of the item value
|
||||
* @param {string} attribute of the content item
|
||||
* @param {string} [query] parameter if the attribute does not match the server query key
|
||||
* @param {function} [converter] if item attribute value has to be transformed e.g. to a date.
|
||||
* @param attribute of the content item
|
||||
* @param query parameter if the attribute does not match the server query key
|
||||
* @param converter if item attribute value has to be transformed e.g. to a date.
|
||||
*/
|
||||
export function contains<T>(attribute: string, query?: string, converter?: Converter<T>): HandlerReturn<T> {
|
||||
return {
|
||||
@@ -128,9 +132,9 @@ export function contains<T>(attribute: string, query?: string, converter?: Conve
|
||||
|
||||
/**
|
||||
* Checks if a value is greater or smaller than the item value
|
||||
* @param {string} attribute of the content item
|
||||
* @param {string} variant specifying the comparison operation e.g. le(<=) and gt(>)
|
||||
* @param {function} [converter] if item attribute value has to be transformed e.g. to a date.
|
||||
* @param attribute of the content item
|
||||
* @param variant specifying the comparison operation e.g. le(<=) and gt(>)
|
||||
* @param converter if item attribute value has to be transformed e.g. to a date.
|
||||
*/
|
||||
export function compare<T>(attribute: string, variant: string, converter?: Converter<T>): HandlerReturn<T> {
|
||||
return {
|
||||
@@ -160,21 +164,25 @@ export function compare<T>(attribute: string, variant: string, converter?: Conve
|
||||
|
||||
export default class Filtering<T> {
|
||||
validFilters: Record<string, HandlerReturn<T>>;
|
||||
validAliases: string[][];
|
||||
validAliases: Array<[string, string]>;
|
||||
useDefaultFilters: boolean;
|
||||
defaultFilters: Record<string, boolean> = {
|
||||
deleted: false,
|
||||
visible: true,
|
||||
};
|
||||
|
||||
constructor(validFilters: Record<string, HandlerReturn<T>>, useDefaultFilters = true, validAliases?: string[][]) {
|
||||
constructor(
|
||||
validFilters: Record<string, HandlerReturn<T>>,
|
||||
useDefaultFilters = true,
|
||||
validAliases?: Array<[string, string]>
|
||||
) {
|
||||
this.validFilters = validFilters;
|
||||
this.useDefaultFilters = useDefaultFilters;
|
||||
this.validAliases = validAliases || defaultValidAliases;
|
||||
}
|
||||
|
||||
/** Returns normalize defaults by adding the operator to the key identifier
|
||||
* @returns {Object} Dictionary with query key and values for default filters
|
||||
* @returns Dictionary with query key and values for default filters
|
||||
* */
|
||||
getDefaults(): Record<string, boolean> {
|
||||
const normalized: Record<string, boolean> = {};
|
||||
@@ -185,8 +193,8 @@ export default class Filtering<T> {
|
||||
}
|
||||
|
||||
/** Returns true if default filter values are not changed
|
||||
* @param {Object} filterSettings Object containing filter settings
|
||||
* @returns {Boolean} True if default filter values are not changed
|
||||
* @param filterSettings Object containing filter settings
|
||||
* @returns true if default filter values are not changed
|
||||
* **/
|
||||
containsDefaults(filterSettings: Record<string, string | boolean>): boolean {
|
||||
const normalized = this.getDefaults();
|
||||
@@ -203,8 +211,8 @@ export default class Filtering<T> {
|
||||
}
|
||||
|
||||
/** Build a text filter from filter settings
|
||||
* @param {Object} filterSettings Object containing filter settings
|
||||
* @returns {String} Parsed filter text string
|
||||
* @param filterSettings Object containing filter settings
|
||||
* @returns Parsed filter text string
|
||||
* */
|
||||
getFilterText(filterSettings: Record<string, string | boolean>): string {
|
||||
const normalized = this.getDefaults();
|
||||
@@ -227,8 +235,8 @@ export default class Filtering<T> {
|
||||
}
|
||||
|
||||
/** Parses single text input into a dict of field->value pairs.
|
||||
* @param {string} filterText Raw filter text string
|
||||
* @returns {object} Filters as dict of field->value pairs
|
||||
* @param filterText Raw filter text string
|
||||
* @returns Filters as dict of field->value pairs
|
||||
* */
|
||||
getFilters(filterText: string): [string, T][] {
|
||||
const pairSplitRE = /[^\s']+(?:'[^']*'[^\s']*)*|(?:'[^']*'[^\s']*)+/g;
|
||||
@@ -240,9 +248,9 @@ export default class Filtering<T> {
|
||||
const elgRE = /(\S+)([:><])(.+)/g;
|
||||
const elgMatch = elgRE.exec(pair);
|
||||
if (elgMatch) {
|
||||
let field = elgMatch[1];
|
||||
const elg = elgMatch[2];
|
||||
const value = elgMatch[3];
|
||||
let field = elgMatch[1]!;
|
||||
const elg = elgMatch[2]!;
|
||||
const value = elgMatch[3]!;
|
||||
// replace alias for less and greater symbol
|
||||
for (const [alias, substitute] of this.validAliases) {
|
||||
if (elg === alias) {
|
||||
@@ -287,10 +295,10 @@ export default class Filtering<T> {
|
||||
* e.g.: Unlike getFilters or getQueryDict, this maintains "hid>":"3" instead
|
||||
* of changing it to "hid-gt":"3"
|
||||
* Only used to sync filterSettings (in HistoryFilters)
|
||||
* @param {Object} filters Parsed filterText from getFilters()
|
||||
* @returns {Object} filterSettings
|
||||
* @param filters Parsed filterText from getFilters()
|
||||
* @returns filterSettings
|
||||
*/
|
||||
toAlias(filters: [string, T][]): object {
|
||||
toAlias(filters: [string, T][]) {
|
||||
const result: Record<string, T> = {};
|
||||
for (const [key, value] of filters) {
|
||||
let hasAlias = false;
|
||||
@@ -310,23 +318,23 @@ export default class Filtering<T> {
|
||||
}
|
||||
|
||||
/** Returns a dictionary with query key and values.
|
||||
* @param {String} filterText Raw filter text string
|
||||
* @returns {Object} Dictionary with query key and values
|
||||
* @param filterText Raw filter text string
|
||||
* @returns Dictionary with query key and values
|
||||
*/
|
||||
getQueryDict(filterText: string): object {
|
||||
getQueryDict(filterText: string) {
|
||||
const queryDict: Record<string, T> = {};
|
||||
const filters = this.getFilters(filterText);
|
||||
for (const [key, value] of filters) {
|
||||
const query = this.validFilters[key].query;
|
||||
const converter = this.validFilters[key].converter;
|
||||
const query = this.validFilters[key]!.query;
|
||||
const converter = this.validFilters[key]!.converter;
|
||||
queryDict[query] = converter ? converter(value) : value;
|
||||
}
|
||||
return queryDict;
|
||||
}
|
||||
|
||||
/** Returns query string from filter text.
|
||||
* @param {String} filterText Raw filter text string to be parsed
|
||||
* @returns {String} Parsed query string
|
||||
* @param filterText Raw filter text string to be parsed
|
||||
* @returns Parsed query string
|
||||
* */
|
||||
getQueryString(filterText: string): string {
|
||||
const filterDict = this.getQueryDict(filterText);
|
||||
@@ -336,12 +344,12 @@ export default class Filtering<T> {
|
||||
}
|
||||
|
||||
/** Check the value of a particular filter.
|
||||
* @param {String} filterText Raw filter text string
|
||||
* @param {String} filterName Filter key to check
|
||||
* @param {String | Object | Boolean} filterValue The filter value to check
|
||||
* @returns {Boolean} True if the filter is set to the given value
|
||||
* @param filterText Raw filter text string
|
||||
* @param filterName Filter key to check
|
||||
* @param filterValue The filter value to check
|
||||
* @returns True if the filter is set to the given value
|
||||
* */
|
||||
checkFilter<T>(filterText: string, filterName: string, filterValue: T): boolean {
|
||||
checkFilter(filterText: string, filterName: string, filterValue: string | object | boolean): boolean {
|
||||
const testValue = this.getFilterValue(filterText, filterName);
|
||||
return toLowerNoQuotes(testValue) === toLowerNoQuotes(filterValue);
|
||||
}
|
||||
@@ -349,10 +357,10 @@ export default class Filtering<T> {
|
||||
/** Get the value of a particular filter from filterText.
|
||||
* @param filterText Raw filter text string
|
||||
* @param filterName Filter key to check
|
||||
* @param [alias="eq"] String alias for filter operator, e.g.:"lt"
|
||||
* @param alias default: `eq` String alias for filter operator, e.g.:"lt"
|
||||
* @returns The filterValue for the filter
|
||||
* */
|
||||
getFilterValue(filterText: string, filterName: string, alias = "eq"): string | boolean {
|
||||
getFilterValue(filterText: string, filterName: string, alias: Alias = "eq"): string | boolean | undefined {
|
||||
const op = getOperatorForAlias(alias);
|
||||
const reString = `${filterName}(?:${op}|[-|_]${alias}:)(?:'([^']*[^\\s']*)'|(\\S+))`;
|
||||
const re = new RegExp(reString);
|
||||
@@ -365,17 +373,21 @@ export default class Filtering<T> {
|
||||
}
|
||||
|
||||
/** Test if an item passes all filters.
|
||||
* @param {Object} filters Parsed in key-value pairs from getFilters()
|
||||
* @param {Object} item Item to test against the filters
|
||||
* @returns {Boolean} True if the item passes all filters
|
||||
* @param filters Parsed in key-value pairs from getFilters()
|
||||
* @param item Item to test against the filters
|
||||
* @returns True if the item passes all filters
|
||||
* */
|
||||
testFilters(filters: [string, T][], item: Record<string, T>): boolean {
|
||||
for (const [key, filterValue] of filters) {
|
||||
const filterAttribute = this.validFilters[key].attribute;
|
||||
const filterHandler = this.validFilters[key].handler;
|
||||
const itemValue = item[filterAttribute];
|
||||
if (!filterHandler(itemValue, filterValue)) {
|
||||
return false;
|
||||
if (!(key in this.validFilters)) {
|
||||
console.error(`Invalid filter ${key}`);
|
||||
} else {
|
||||
const filterAttribute = this.validFilters[key]!.attribute;
|
||||
const filterHandler = this.validFilters[key]!.handler;
|
||||
const itemValue = item[filterAttribute];
|
||||
if (itemValue === undefined || !filterHandler(itemValue, filterValue)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -2,7 +2,7 @@ import RuleDefs from "@/components/RuleBuilder/rule-definitions";
|
||||
import SPEC_TEST_CASES from "./rules_dsl_spec.yml";
|
||||
|
||||
function applyRules(rules: Array<any>, data: Array<Array<string>>, sources: Array<number>) {
|
||||
const columns = Array(data[0].length).fill("new");
|
||||
const columns = Array(data[0]?.length).fill("new");
|
||||
return RuleDefs.applyRules(data, sources, columns, rules);
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
|
||||
"allowJs": true,
|
||||
"checkJs": false,
|
||||
"allowSyntheticDefaultImports": true
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"noUncheckedIndexedAccess": true
|
||||
},
|
||||
"vueCompilerOptions": {
|
||||
"target": 2.7
|
||||
|
||||
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
/** length of const array or tuple */
|
||||
export type Length<T extends any[]> = T["length"];
|
||||
|
||||
/** build a tuple with length L, and type T */
|
||||
export type Tuple<L extends number, T = any, R extends T[] = []> = R extends { length: L } ? R : Tuple<L, T, [...R, T]>;
|
||||
|
||||
/** adds two constant positive whole number types */
|
||||
export type Add<A extends number, B extends number> = Length<[...Tuple<A>, ...Tuple<B>]>;
|
||||
|
||||
/** subtracts one constant positive whole number type from another */
|
||||
export type Subtract<A extends number, B extends number> = Tuple<A> extends [...infer U, ...Tuple<B>]
|
||||
? Length<U>
|
||||
: never;
|
||||
|
||||
/** type of the last element of a readonly array or tuple */
|
||||
export type Last<A extends readonly [...any]> = A[Subtract<Length<A>, 1>];
|
||||
|
||||
/** type of the first element of a readonly array or tuple */
|
||||
export type First<A extends readonly [...any]> = A[Length<A>];
|
||||
Reference in New Issue
Block a user