Skip to content

Commit d4ae0cd

Browse files
implement extensive typing for workflow refactoring methods
Adds an entire `lintingTypes.d.ts` file to store not just the schema defined refactor request types for each action, but also all types of "lint state" objects generated on the client for those requests.
1 parent e23e51d commit d4ae0cd

File tree

9 files changed

+177
-79
lines changed

9 files changed

+177
-79
lines changed

client/src/api/workflows.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { rethrowSimple } from "@/utils/simple-error";
44
import { GalaxyApi } from "./client";
55

66
export type Creator = components["schemas"]["Person"] | components["schemas"]["CreatorOrganization"];
7+
export type RefactorRequestAction = components["schemas"]["RefactorRequest"]["actions"][number];
8+
export type RefactorResponse = components["schemas"]["RefactorResponse"];
9+
export type RefactorResponseActionExecution = RefactorResponse["action_executions"][number];
710
export type StoredWorkflowDetailed = components["schemas"]["StoredWorkflowDetailed"];
811
export type WorkflowStepTyped = StoredWorkflowDetailed["steps"][number];
912

@@ -107,6 +110,24 @@ export async function getWorkflowInfo(workflowId: string, version?: number, inst
107110
return data;
108111
}
109112

113+
export async function refactor(id: string, actions: RefactorRequestAction[], style: string, dryRun = false) {
114+
const { data, error } = await GalaxyApi().PUT("/api/workflows/{workflow_id}/refactor", {
115+
params: {
116+
path: { workflow_id: id },
117+
},
118+
body: {
119+
actions: actions,
120+
style: style,
121+
dry_run: dryRun,
122+
},
123+
});
124+
if (error) {
125+
rethrowSimple(error);
126+
}
127+
128+
return data as RefactorResponse;
129+
}
130+
110131
export async function undeleteWorkflow(id: string): Promise<WorkflowSummary> {
111132
const { data, error } = await GalaxyApi().POST("/api/workflows/{workflow_id}/undelete", {
112133
params: {

client/src/components/Workflow/Editor/Lint.vue

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { useWorkflowStores } from "@/composables/workflowStores";
88
import type { Steps } from "@/stores/workflowStepStore";
99
1010
import type { Rectangle } from "./modules/geometry";
11-
import type { LintState } from "./modules/linting";
1211
import {
1312
bestPracticeWarningAnnotation,
1413
bestPracticeWarningAnnotationLength,
@@ -19,7 +18,9 @@ import {
1918
fixDisconnectedInput,
2019
fixUnlabeledOutputs,
2120
fixUntypedParameter,
21+
isStateForInputOrOutput,
2222
} from "./modules/linting";
23+
import type { DisconnectedInputState, LintState, UntypedParameterState } from "./modules/lintingTypes";
2324
import type { LintData } from "./modules/useLinting";
2425
2526
import LintSectionSeparator from "./LintSectionSeparator.vue";
@@ -123,7 +124,7 @@ async function onFixUntypedParameter(item: LintState) {
123124
);
124125
125126
if (confirmed) {
126-
emit("onRefactor", [fixUntypedParameter(item)]);
127+
emit("onRefactor", [fixUntypedParameter(item as UntypedParameterState)]);
127128
} else {
128129
emit("onScrollTo", item.stepId);
129130
}
@@ -145,7 +146,7 @@ async function onFixDisconnectedInput(item: LintState) {
145146
"Fix Disconnected Input",
146147
);
147148
if (confirmed) {
148-
emit("onRefactor", [fixDisconnectedInput(item)]);
149+
emit("onRefactor", [fixDisconnectedInput(item as DisconnectedInputState)]);
149150
} else {
150151
emit("onScrollTo", item.stepId);
151152
}
@@ -169,7 +170,11 @@ function openAndFocus(item: LintState) {
169170
}
170171
171172
function onHighlight(item: LintState) {
172-
const bounds = searchStore.getBoundsForItemCached(item.stepId, item.highlightType || "step", item.name);
173+
const bounds = searchStore.getBoundsForItemCached(
174+
item.stepId,
175+
isStateForInputOrOutput(item) ? item.highlightType : "step",
176+
"name" in item ? item.name : undefined,
177+
);
173178
if (bounds) {
174179
emit("onHighlightRegion", bounds);
175180
}

client/src/components/Workflow/Editor/LintSection.vue

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ import {
1010
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
1111
import { computed } from "vue";
1212
13-
import { dataAttributes, type LintState } from "./modules/linting";
13+
import { dataAttributes } from "./modules/linting";
14+
import type { LintState } from "./modules/lintingTypes";
1415
1516
import GButton from "@/components/BaseComponents/GButton.vue";
1617
import GLink from "@/components/BaseComponents/GLink.vue";
@@ -77,7 +78,7 @@ const isOkay = computed(() => props.okay && !hasWarningItems.value);
7778
data-description="autofix item link"
7879
v-bind="dataAttributes(item)"
7980
@click="emit('onClick', item)">
80-
<FontAwesomeIcon v-if="item.autofix" :icon="faMagic" class="mr-1" />
81+
<FontAwesomeIcon v-if="'autofix' in item && item.autofix" :icon="faMagic" class="mr-1" />
8182
<FontAwesomeIcon v-else :icon="faSearch" class="mr-1" />
8283
<strong>Step {{ item.stepId + 1 }}</strong>
8384
{{ item.stepLabel }}: {{ item.warningLabel }}

client/src/components/Workflow/Editor/RefactorConfirmationModal.test.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import { shallowMount, type Wrapper } from "@vue/test-utils";
33
import flushPromises from "flush-promises";
44
import { beforeEach, describe, expect, it, vi } from "vitest";
55

6-
import { refactor } from "./modules/services";
6+
import { refactor, type RefactorResponse } from "@/api/workflows";
77

88
import RefactorConfirmationModal from "./RefactorConfirmationModal.vue";
99
import GModal from "@/components/BaseComponents/GModal.vue";
1010

11-
vi.mock("./modules/services", () => ({
11+
vi.mock("@/api/workflows", () => ({
1212
refactor: vi.fn(),
1313
}));
1414

@@ -49,7 +49,7 @@ describe("RefactorConfirmationModal.vue", () => {
4949
await flushPromises();
5050
expect(wrapper.emitted().onWorkflowError!.length).toBe(1);
5151
expect(vi.mocked(refactor).mock.calls[0]![0]).toEqual(TEST_WORKFLOW_ID);
52-
expect(vi.mocked(refactor).mock.calls[0]![2]).toBeTruthy(); // dry run argument
52+
expect(vi.mocked(refactor).mock.calls[0]![3]).toBeTruthy(); // dry run argument
5353

5454
// onRefactor never emitted because there was a failure
5555
expect(wrapper.emitted().onRefactor).toBeFalsy();
@@ -60,7 +60,7 @@ describe("RefactorConfirmationModal.vue", () => {
6060
new Promise((then) => {
6161
then({
6262
action_executions: [],
63-
});
63+
} as unknown as RefactorResponse);
6464
}),
6565
);
6666
await wrapper.setProps({
@@ -70,10 +70,10 @@ describe("RefactorConfirmationModal.vue", () => {
7070
expect(wrapper.emitted().onWorkflowError).toBeFalsy();
7171
// called with dry run as true...
7272
expect(vi.mocked(refactor).mock.calls[0]![0]).toEqual(TEST_WORKFLOW_ID);
73-
expect(vi.mocked(refactor).mock.calls[0]![2]).toBeTruthy();
73+
expect(vi.mocked(refactor).mock.calls[0]![3]).toBeTruthy();
7474
// ... and then as false
7575
expect(vi.mocked(refactor).mock.calls[1]![0]).toEqual(TEST_WORKFLOW_ID);
76-
expect(vi.mocked(refactor).mock.calls[1]![2]).toBeFalsy();
76+
expect(vi.mocked(refactor).mock.calls[1]![3]).toBeFalsy();
7777
// second time onRefactor emitted with the final response
7878
expect(wrapper.emitted().onRefactor!.length).toBe(1);
7979
});
@@ -93,7 +93,7 @@ describe("RefactorConfirmationModal.vue", () => {
9393
],
9494
},
9595
],
96-
});
96+
} as unknown as RefactorResponse);
9797
}),
9898
);
9999

@@ -108,7 +108,7 @@ describe("RefactorConfirmationModal.vue", () => {
108108

109109
// called with dry run...
110110
expect(vi.mocked(refactor).mock.calls[0]![0]).toEqual(TEST_WORKFLOW_ID);
111-
expect(vi.mocked(refactor).mock.calls[0]![2]).toBeTruthy();
111+
expect(vi.mocked(refactor).mock.calls[0]![3]).toBeTruthy();
112112
// but didn't follow up with executing the action because we need to confirm
113113
expect(vi.mocked(refactor).mock.calls.length).toBe(1);
114114

client/src/components/Workflow/Editor/RefactorConfirmationModal.vue

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
11
<script setup lang="ts">
22
import { ref, watch } from "vue";
33
4-
import { refactor } from "./modules/services";
4+
import {
5+
refactor,
6+
type RefactorRequestAction,
7+
type RefactorResponse,
8+
type RefactorResponseActionExecution,
9+
} from "@/api/workflows";
510
611
import GModal from "@/components/BaseComponents/GModal.vue";
712
813
interface Props {
9-
refactorActions: any[]; // TODO: type from schema
14+
refactorActions: RefactorRequestAction[];
1015
workflowId: string;
1116
title?: string;
1217
message?: string;
@@ -20,11 +25,11 @@ const emit = defineEmits<{
2025
(e: "onShow"): void;
2126
(e: "onWorkflowMessage", message: string, type: string): void;
2227
(e: "onWorkflowError", message: string, response: any): void;
23-
(e: "onRefactor", data: any): void; // TODO: type from schema
28+
(e: "onRefactor", data: RefactorResponse): void;
2429
}>();
2530
2631
const show = ref(props.refactorActions.length > 0);
27-
const confirmActionExecutions = ref<any[]>([]); // TODO: type from schema
32+
const confirmActionExecutions = ref<RefactorResponseActionExecution[]>([]);
2833
2934
watch(
3035
() => props.refactorActions,
@@ -46,21 +51,23 @@ watch(show, (newShow) => {
4651
async function dryRun() {
4752
emit("onWorkflowMessage", "Pre-checking requested workflow changes (dry run)...", "progress");
4853
try {
49-
const data = await refactor(props.workflowId, props.refactorActions, true); // dry run
54+
const data = await refactor(props.workflowId, props.refactorActions, "editor", true);
5055
onDryRunResponse(data);
5156
} catch (response) {
52-
onError(response);
57+
onError(response as string);
5358
}
5459
}
5560
56-
function onError(response: any) {
61+
function onError(response: string) {
5762
emit("onWorkflowError", "Reworking workflow failed...", response);
5863
}
5964
60-
function onDryRunResponse(data: any) {
65+
function onDryRunResponse(data: RefactorResponse) {
6166
// TODO: type from schema
6267
const actionExecutions = data.action_executions;
63-
const anyRequireConfirmation = actionExecutions.some((execution: any) => execution.messages.length > 0);
68+
const anyRequireConfirmation = actionExecutions.some(
69+
(execution: RefactorResponseActionExecution) => execution.messages.length > 0,
70+
);
6471
if (anyRequireConfirmation) {
6572
confirmActionExecutions.value = actionExecutions;
6673
show.value = true;
@@ -73,10 +80,10 @@ async function executeRefactoring() {
7380
show.value = false;
7481
emit("onWorkflowMessage", "Applying requested workflow changes...", "progress");
7582
try {
76-
const data = await refactor(props.workflowId, props.refactorActions, false);
83+
const data = await refactor(props.workflowId, props.refactorActions, "editor", false);
7784
emit("onRefactor", data);
7885
} catch (response) {
79-
onError(response);
86+
onError(response as string);
8087
}
8188
}
8289
</script>

client/src/components/Workflow/Editor/modules/linting.ts

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,19 @@ import type { Steps } from "@/stores/workflowStepStore";
55
import { assertDefined } from "@/utils/assertions";
66

77
import { isWorkflowInput } from "../../constants";
8+
import type {
9+
DisconnectedInputState,
10+
DuplicateLabelState,
11+
ExtractInputAction,
12+
ExtractUntypedParameter,
13+
LintState,
14+
MetadataLintState,
15+
RemoveUnlabeledWorkflowOutputs,
16+
UnlabeledOuputState,
17+
UntypedParameterState,
18+
} from "./lintingTypes";
819
import { terminalFactory } from "./terminals";
920

10-
export interface LintState {
11-
stepId: number;
12-
stepLabel: string;
13-
warningLabel: string;
14-
name?: string;
15-
inputName?: string;
16-
autofix?: boolean;
17-
data?: Record<string, string>;
18-
/** Whether to highlight an input or output terminal */
19-
highlightType?: "input" | "output";
20-
}
21-
2221
export const bestPracticeWarningAnnotation =
2322
"This workflow does not provide a short description. Providing a short description helps workflow executors understand the purpose and usage of the workflow.";
2423
export const bestPracticeWarningAnnotationLength =
@@ -35,7 +34,7 @@ export function getDisconnectedInputs(
3534
datatypesMapper: DatatypesMapperModel,
3635
stores: ReturnType<typeof useWorkflowStores>,
3736
) {
38-
const inputs: LintState[] = [];
37+
const inputs: DisconnectedInputState[] = [];
3938
Object.values(steps).forEach((step) => {
4039
step.inputs.map((inputSource) => {
4140
const inputTerminal = terminalFactory(step.id, inputSource, datatypesMapper, stores);
@@ -57,13 +56,13 @@ export function getDisconnectedInputs(
5756
}
5857

5958
export function getMissingMetadata(steps: Steps) {
60-
const inputs: LintState[] = [];
59+
const inputs: MetadataLintState[] = [];
6160
Object.values(steps).forEach((step) => {
6261
if (isWorkflowInput(step.type)) {
6362
const noAnnotation = !step.annotation;
6463
const noLabel = !step.label;
6564
let warningLabel = null;
66-
const data = {
65+
const data: MetadataLintState["data"] = {
6766
"missing-label": "false",
6867
"missing-annotation": "false",
6968
};
@@ -91,17 +90,21 @@ export function getMissingMetadata(steps: Steps) {
9190
return inputs;
9291
}
9392

94-
export function dataAttributes(action: LintState): Record<string, string> {
95-
const result: Record<string, string> = {};
96-
for (const [key, value] of Object.entries(action.data || {})) {
97-
result[`data-${key}`] = value;
93+
// TODO: Maybe type of action should already be `MetadataLintState`?
94+
export function dataAttributes(action: LintState) {
95+
const result: Record<string, "true" | "false"> = {};
96+
// Ensure we have an attributes lint state (`MetadataLintState` with data)
97+
if (isMetadataLintState(action)) {
98+
for (const [key, value] of Object.entries(action.data)) {
99+
result[`data-${key}`] = value;
100+
}
98101
}
99102

100103
return result;
101104
}
102105

103106
export function getDuplicateLabels(steps: Steps, stores: ReturnType<typeof useWorkflowStores>) {
104-
const duplicates: LintState[] = [];
107+
const duplicates: DuplicateLabelState[] = [];
105108

106109
const { stepStore } = stores;
107110
const labels = stepStore.duplicateLabels;
@@ -127,7 +130,7 @@ export function getDuplicateLabels(steps: Steps, stores: ReturnType<typeof useWo
127130
}
128131

129132
export function getUnlabeledOutputs(steps: Steps) {
130-
const outputs: LintState[] = [];
133+
const outputs: UnlabeledOuputState[] = [];
131134
Object.values(steps).forEach((step) => {
132135
if (isWorkflowInput(step.type)) {
133136
// For now skip these... maybe should push this logic into linting though
@@ -153,7 +156,7 @@ export function getUnlabeledOutputs(steps: Steps) {
153156
}
154157

155158
export function getUntypedParameters(untypedParameters: UntypedParameters) {
156-
const items: LintState[] = [];
159+
const items: UntypedParameterState[] = [];
157160
if (untypedParameters) {
158161
untypedParameters.parameters.forEach((parameter) => {
159162
try {
@@ -179,9 +182,9 @@ export function getUntypedParameters(untypedParameters: UntypedParameters) {
179182
}
180183

181184
export function fixAllIssues(
182-
untypedParameters: LintState[],
183-
disconnectedInputs: LintState[],
184-
unlabeledOutputs: LintState[],
185+
untypedParameters: UntypedParameterState[],
186+
disconnectedInputs: DisconnectedInputState[],
187+
unlabeledOutputs: UnlabeledOuputState[],
185188
) {
186189
const actions = [];
187190
for (const untypedParameter of untypedParameters) {
@@ -200,14 +203,14 @@ export function fixAllIssues(
200203
return actions;
201204
}
202205

203-
export function fixUntypedParameter(untypedParameter: LintState) {
206+
export function fixUntypedParameter(untypedParameter: UntypedParameterState): ExtractUntypedParameter {
204207
return {
205208
action_type: "extract_untyped_parameter",
206209
name: untypedParameter.name,
207210
};
208211
}
209212

210-
export function fixDisconnectedInput(disconnectedInput: LintState) {
213+
export function fixDisconnectedInput(disconnectedInput: DisconnectedInputState): ExtractInputAction {
211214
return {
212215
action_type: "extract_input",
213216
input: {
@@ -217,6 +220,21 @@ export function fixDisconnectedInput(disconnectedInput: LintState) {
217220
};
218221
}
219222

220-
export function fixUnlabeledOutputs() {
223+
export function fixUnlabeledOutputs(): RemoveUnlabeledWorkflowOutputs {
221224
return { action_type: "remove_unlabeled_workflow_outputs" };
222225
}
226+
227+
export function isDisconnectedInputState(state: LintState): state is DisconnectedInputState {
228+
return isStateForInputOrOutput(state) && state.highlightType === "input";
229+
}
230+
231+
function isMetadataLintState(state: LintState): state is MetadataLintState {
232+
return "data" in state;
233+
}
234+
235+
/** Type guard for linting states that are for a workflow step input or output. */
236+
export function isStateForInputOrOutput(
237+
state: LintState,
238+
): state is DisconnectedInputState | DuplicateLabelState | UnlabeledOuputState {
239+
return "highlightType" in state;
240+
}

0 commit comments

Comments
 (0)