@shipfox/client-workflows 5.0.0 → 6.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +68 -0
  3. package/dist/components/workflow-run-list/types.d.ts +12 -0
  4. package/dist/components/workflow-run-list/types.d.ts.map +1 -1
  5. package/dist/components/workflow-run-list/types.js.map +1 -1
  6. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts +1 -1
  7. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts.map +1 -1
  8. package/dist/components/workflow-run-list/workflow-run-list-view.js +32 -11
  9. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  10. package/dist/components/workflow-run-list/workflow-run-list.d.ts +1 -1
  11. package/dist/components/workflow-run-list/workflow-run-list.d.ts.map +1 -1
  12. package/dist/components/workflow-run-list/workflow-run-list.js +7 -2
  13. package/dist/components/workflow-run-list/workflow-run-list.js.map +1 -1
  14. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +2 -3
  15. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  16. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  17. package/dist/components/workflow-run-view/job-card.d.ts.map +1 -1
  18. package/dist/components/workflow-run-view/job-card.js +9 -8
  19. package/dist/components/workflow-run-view/job-card.js.map +1 -1
  20. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  21. package/dist/core/entities/job-execution.d.ts +11 -6
  22. package/dist/core/entities/job-execution.d.ts.map +1 -1
  23. package/dist/core/entities/job-execution.js +0 -19
  24. package/dist/core/entities/job-execution.js.map +1 -1
  25. package/dist/core/entities/job.d.ts +32 -10
  26. package/dist/core/entities/job.d.ts.map +1 -1
  27. package/dist/core/entities/job.js +0 -22
  28. package/dist/core/entities/job.js.map +1 -1
  29. package/dist/core/entities/step-attempt.d.ts +28 -3
  30. package/dist/core/entities/step-attempt.d.ts.map +1 -1
  31. package/dist/core/entities/step-attempt.js +0 -17
  32. package/dist/core/entities/step-attempt.js.map +1 -1
  33. package/dist/core/entities/step.d.ts +6 -6
  34. package/dist/core/entities/step.d.ts.map +1 -1
  35. package/dist/core/entities/step.js +19 -49
  36. package/dist/core/entities/step.js.map +1 -1
  37. package/dist/core/entities/workflow-run-attempt.d.ts +0 -2
  38. package/dist/core/entities/workflow-run-attempt.d.ts.map +1 -1
  39. package/dist/core/entities/workflow-run-attempt.js +0 -12
  40. package/dist/core/entities/workflow-run-attempt.js.map +1 -1
  41. package/dist/core/entities/workflow-run.d.ts +6 -7
  42. package/dist/core/entities/workflow-run.d.ts.map +1 -1
  43. package/dist/core/entities/workflow-run.js +1 -67
  44. package/dist/core/entities/workflow-run.js.map +1 -1
  45. package/dist/core/workflow-run.d.ts +9 -9
  46. package/dist/core/workflow-run.d.ts.map +1 -1
  47. package/dist/core/workflow-run.js +6 -6
  48. package/dist/core/workflow-run.js.map +1 -1
  49. package/dist/feature.d.ts +13 -0
  50. package/dist/feature.d.ts.map +1 -1
  51. package/dist/feature.js +17 -16
  52. package/dist/feature.js.map +1 -1
  53. package/dist/hooks/api/workflow-run-mapper.d.ts +12 -0
  54. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -0
  55. package/dist/hooks/api/workflow-run-mapper.js +207 -0
  56. package/dist/hooks/api/workflow-run-mapper.js.map +1 -0
  57. package/dist/hooks/api/workflow-runs.d.ts +25 -55
  58. package/dist/hooks/api/workflow-runs.d.ts.map +1 -1
  59. package/dist/hooks/api/workflow-runs.js +95 -51
  60. package/dist/hooks/api/workflow-runs.js.map +1 -1
  61. package/dist/index.d.ts +2 -2
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +1 -1
  64. package/dist/index.js.map +1 -1
  65. package/dist/pages/project-workflows-page.d.ts.map +1 -1
  66. package/dist/pages/project-workflows-page.js +14 -14
  67. package/dist/pages/project-workflows-page.js.map +1 -1
  68. package/dist/pages/workflow-run-page.d.ts +3 -1
  69. package/dist/pages/workflow-run-page.d.ts.map +1 -1
  70. package/dist/pages/workflow-run-page.js +21 -14
  71. package/dist/pages/workflow-run-page.js.map +1 -1
  72. package/dist/routes/inputs.d.ts +22 -0
  73. package/dist/routes/inputs.d.ts.map +1 -0
  74. package/dist/routes/inputs.js +76 -0
  75. package/dist/routes/inputs.js.map +1 -0
  76. package/dist/routes/run-detail.d.ts +2 -0
  77. package/dist/routes/run-detail.d.ts.map +1 -1
  78. package/dist/routes/run-detail.js +6 -6
  79. package/dist/routes/run-detail.js.map +1 -1
  80. package/dist/routes/runs.d.ts +2 -0
  81. package/dist/routes/runs.d.ts.map +1 -1
  82. package/dist/routes/runs.js +6 -6
  83. package/dist/routes/runs.js.map +1 -1
  84. package/dist/routes/workflows.js +3 -5
  85. package/dist/routes/workflows.js.map +1 -1
  86. package/dist/tsconfig.test.tsbuildinfo +1 -1
  87. package/package.json +13 -14
  88. package/src/components/workflow-run-list/types.ts +6 -0
  89. package/src/components/workflow-run-list/workflow-run-list-view.tsx +27 -11
  90. package/src/components/workflow-run-list/workflow-run-list.tsx +6 -0
  91. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.test.tsx +5 -6
  92. package/src/components/workflow-run-summary/workflow-run-summary.tsx +3 -3
  93. package/src/components/workflow-run-view/job-card.tsx +20 -11
  94. package/src/components/workflow-run-view/workflow-run-view.test.tsx +3 -3
  95. package/src/components/workflow-run-view/workflow-run-view.tsx +2 -2
  96. package/src/components/workflow-source-panel/workflow-source-panel.stories.tsx +4 -0
  97. package/src/core/entities/job-execution.ts +12 -28
  98. package/src/core/entities/job.ts +44 -45
  99. package/src/core/entities/step-attempt.ts +9 -20
  100. package/src/core/entities/step.ts +38 -66
  101. package/src/core/entities/workflow-run-attempt.ts +0 -14
  102. package/src/core/entities/workflow-run.ts +8 -86
  103. package/src/core/workflow-run.test.ts +13 -11
  104. package/src/core/workflow-run.ts +8 -13
  105. package/src/feature.ts +19 -17
  106. package/src/hooks/api/workflow-run-mapper.ts +246 -0
  107. package/src/hooks/api/workflow-runs.test.tsx +57 -59
  108. package/src/hooks/api/workflow-runs.ts +187 -88
  109. package/src/index.ts +4 -0
  110. package/src/pages/project-workflows-page.test.tsx +1 -1
  111. package/src/pages/project-workflows-page.tsx +31 -26
  112. package/src/pages/workflow-run-page.test.tsx +4 -2
  113. package/src/pages/workflow-run-page.tsx +22 -17
  114. package/src/routes/inputs.test.ts +22 -0
  115. package/src/routes/inputs.ts +58 -0
  116. package/src/routes/run-detail.tsx +12 -8
  117. package/src/routes/runs.tsx +11 -4
  118. package/src/routes/workflows.tsx +3 -3
  119. package/test/fixtures/workflow-run.ts +9 -7
  120. package/test/pages.tsx +20 -5
  121. package/tsconfig.build.tsbuildinfo +1 -1
  122. package/vercel.json +5 -0
@@ -1,12 +1,8 @@
1
- import type {
2
- WorkflowRunDetailResponseDto,
3
- WorkflowRunListResponseDto,
4
- } from '@shipfox/api-workflows-dto';
5
1
  import {configureApiClient} from '@shipfox/client-api';
6
2
  import {type InfiniteData, QueryClient, QueryClientProvider} from '@tanstack/react-query';
7
3
  import {act, cleanup, renderHook, waitFor} from '@testing-library/react';
8
4
  import type {ReactNode} from 'react';
9
- import {toWorkflowRun} from '#core/workflow-run.js';
5
+ import type {WorkflowRunListPage} from '#core/workflow-run.js';
10
6
  import {
11
7
  runAttemptsResponseDto,
12
8
  workflowJobDto,
@@ -15,6 +11,7 @@ import {
15
11
  workflowRunDto,
16
12
  workflowRunListResponseDto,
17
13
  } from '#test/fixtures/workflow-run.js';
14
+ import {toWorkflowRun, toWorkflowRunListPage} from './workflow-run-mapper.js';
18
15
  import {
19
16
  fireManualWorkflow,
20
17
  useCancelWorkflowRunMutation,
@@ -55,7 +52,7 @@ describe('workflow run API hooks', () => {
55
52
  configureApiClient({baseUrl: '', fetchImpl: undefined});
56
53
  });
57
54
 
58
- test('maps list DTO pages to workflow run models while keeping the cache DTO-shaped', async () => {
55
+ test('maps list DTO pages to workflow run models before caching', async () => {
59
56
  const body = workflowRunListResponseDto({
60
57
  runs: [
61
58
  workflowRunDto({
@@ -92,19 +89,19 @@ describe('workflow run API hooks', () => {
92
89
  expect(result.current.data?.pages[0]?.nextCursor).toBe('cursor-2');
93
90
  expect(result.current.data?.pages[0]?.filteredTotalCount).toBe(8);
94
91
 
95
- const cached = queryClient.getQueryData<
96
- InfiniteData<WorkflowRunListResponseDto, string | undefined>
97
- >(workflowRunsQueryKeys.list(PROJECT_ID, {}));
92
+ const cached = queryClient.getQueryData<InfiniteData<WorkflowRunListPage, string | undefined>>(
93
+ workflowRunsQueryKeys.list(PROJECT_ID, {}),
94
+ );
98
95
  expect(cached?.pages[0]?.runs[0]).toMatchObject({
99
- trigger_provider: 'github',
100
- trigger_source: 'github_acme',
101
- trigger_event: 'push',
102
- updated_at: '2026-05-07T01:02:00.000Z',
96
+ triggerProvider: 'github',
97
+ triggerSource: 'github_acme',
98
+ triggerEvent: 'push',
99
+ updatedAt: '2026-05-07T01:02:00.000Z',
103
100
  });
104
- expect(cached?.pages[0]).not.toHaveProperty('nextCursor');
101
+ expect(cached?.pages[0]).toHaveProperty('nextCursor', 'cursor-2');
105
102
  });
106
103
 
107
- test('maps detail DTOs to nested workflow run detail models while keeping the cache DTO-shaped', async () => {
104
+ test('maps detail DTOs to nested workflow run detail models before caching', async () => {
108
105
  const body = workflowRunDetailDto({
109
106
  id: RUN_ID,
110
107
  trigger_source: 'manual',
@@ -126,16 +123,25 @@ describe('workflow run API hooks', () => {
126
123
  jobs: [{name: 'build', runAttemptId: RUN_ID}],
127
124
  });
128
125
 
129
- const cached = queryClient.getQueryData<WorkflowRunDetailResponseDto>(
130
- workflowRunsQueryKeys.detail(RUN_ID),
131
- );
126
+ const cached = queryClient.getQueryData(workflowRunsQueryKeys.detail(RUN_ID));
132
127
  expect(cached).toMatchObject({
133
128
  id: RUN_ID,
134
- trigger_source: 'manual',
135
- trigger_event: 'fire',
136
- jobs: [{name: 'build', run_attempt_id: RUN_ID}],
129
+ triggerSource: 'manual',
130
+ triggerEvent: 'fire',
131
+ jobs: [{name: 'build', runAttemptId: RUN_ID}],
137
132
  });
138
- expect(cached).not.toHaveProperty('triggerSource');
133
+ expect(cached).toHaveProperty('triggerSource', 'manual');
134
+ });
135
+
136
+ test('rejects malformed detail responses before they reach the query cache', async () => {
137
+ const fetchImpl = vi.fn(async () => jsonResponse({id: RUN_ID, trigger_source: 'manual'}));
138
+ configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
139
+
140
+ const {result, queryClient} = renderWithQueryClient(() => useWorkflowRunQuery(RUN_ID));
141
+
142
+ await waitFor(() => expect(result.current.isError).toBe(true));
143
+ expect(result.current.error).toMatchObject({code: 'invalid-response'});
144
+ expect(queryClient.getQueryData(workflowRunsQueryKeys.detail(RUN_ID))).toBeUndefined();
139
145
  });
140
146
 
141
147
  test('maps run attempts and caches them by workflow run id', async () => {
@@ -173,7 +179,9 @@ describe('workflow run API hooks', () => {
173
179
  expect(firstRequest(fetchImpl).url).toBe(
174
180
  `https://api.example.test/workflows/runs/${RUN_ID}/attempts`,
175
181
  );
176
- expect(queryClient.getQueryData(workflowRunsQueryKeys.attempts(RUN_ID))).toEqual(body);
182
+ expect(queryClient.getQueryData(workflowRunsQueryKeys.attempts(RUN_ID))).toEqual(
183
+ result.current.data,
184
+ );
177
185
  });
178
186
 
179
187
  test('posts manual fire requests with and without inputs', async () => {
@@ -190,8 +198,8 @@ describe('workflow run API hooks', () => {
190
198
  inputs: {env: 'production'},
191
199
  });
192
200
 
193
- expect(withoutInputs.workflow_run_id).toBe(RUN_ID);
194
- expect(withInputs.workflow_run_id).toBe(RUN_ID);
201
+ expect(withoutInputs.workflowRunId).toBe(RUN_ID);
202
+ expect(withInputs.workflowRunId).toBe(RUN_ID);
195
203
  expect(postBodies).toEqual([{}, {inputs: {env: 'production'}}]);
196
204
  expect(firstRequest(fetchImpl).url).toBe(
197
205
  `https://api.example.test/workflow-definitions/${DEFINITION_ID}/fire-manual`,
@@ -210,13 +218,12 @@ describe('workflow run API hooks', () => {
210
218
  configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
211
219
  const {result, queryClient} = renderWithQueryClient(() => useFireManualWorkflowMutation());
212
220
  const listKey = workflowRunsQueryKeys.list(PROJECT_ID, {});
213
- queryClient.setQueryData<InfiniteData<WorkflowRunListResponseDto, string | undefined>>(
214
- listKey,
215
- {
216
- pages: [workflowRunListResponseDto({runs: [], filtered_total_count: 0})],
217
- pageParams: [undefined],
218
- },
219
- );
221
+ queryClient.setQueryData<InfiniteData<WorkflowRunListPage, string | undefined>>(listKey, {
222
+ pages: [
223
+ toWorkflowRunListPage(workflowRunListResponseDto({runs: [], filtered_total_count: 0})),
224
+ ],
225
+ pageParams: [undefined],
226
+ });
220
227
 
221
228
  const railListEntries = queryClient.getQueriesData({
222
229
  queryKey: workflowRunsQueryKeys.lists(PROJECT_ID),
@@ -229,18 +236,16 @@ describe('workflow run API hooks', () => {
229
236
 
230
237
  await waitFor(() => {
231
238
  const cached =
232
- queryClient.getQueryData<InfiniteData<WorkflowRunListResponseDto, string | undefined>>(
233
- listKey,
234
- );
239
+ queryClient.getQueryData<InfiniteData<WorkflowRunListPage, string | undefined>>(listKey);
235
240
  expect(cached?.pages[0]?.runs[0]).toMatchObject({
236
- project_id: PROJECT_ID,
237
- definition_id: DEFINITION_ID,
241
+ projectId: PROJECT_ID,
242
+ definitionId: DEFINITION_ID,
238
243
  status: 'pending',
239
- trigger_source: 'manual',
240
- trigger_provider: null,
244
+ triggerSource: 'manual',
245
+ triggerProvider: null,
241
246
  });
242
247
  expect(cached?.pages[0]?.runs[0]?.id).toMatch(TEMP_RUN_ID_PATTERN);
243
- expect(cached?.pages[0]?.filtered_total_count).toBe(1);
248
+ expect(cached?.pages[0]?.filteredTotalCount).toBe(1);
244
249
  });
245
250
 
246
251
  if (!resolveFire) throw new Error('Expected manual fire request');
@@ -262,13 +267,12 @@ describe('workflow run API hooks', () => {
262
267
  configureApiClient({baseUrl: 'https://api.example.test', fetchImpl});
263
268
  const {result, queryClient} = renderWithQueryClient(() => useFireManualWorkflowMutation());
264
269
  const listKey = workflowRunsQueryKeys.list(PROJECT_ID, {});
265
- queryClient.setQueryData<InfiniteData<WorkflowRunListResponseDto, string | undefined>>(
266
- listKey,
267
- {
268
- pages: [workflowRunListResponseDto({runs: [], filtered_total_count: 0})],
269
- pageParams: [undefined],
270
- },
271
- );
270
+ queryClient.setQueryData<InfiniteData<WorkflowRunListPage, string | undefined>>(listKey, {
271
+ pages: [
272
+ toWorkflowRunListPage(workflowRunListResponseDto({runs: [], filtered_total_count: 0})),
273
+ ],
274
+ pageParams: [undefined],
275
+ });
272
276
 
273
277
  act(() => {
274
278
  result.current.mutate({projectId: PROJECT_ID, definitionId: DEFINITION_ID});
@@ -279,11 +283,9 @@ describe('workflow run API hooks', () => {
279
283
 
280
284
  await waitFor(() => {
281
285
  const cached =
282
- queryClient.getQueryData<InfiniteData<WorkflowRunListResponseDto, string | undefined>>(
283
- listKey,
284
- );
286
+ queryClient.getQueryData<InfiniteData<WorkflowRunListPage, string | undefined>>(listKey);
285
287
  expect(cached?.pages[0]?.runs).toHaveLength(1);
286
- expect(cached?.pages[0]?.filtered_total_count).toBe(1);
288
+ expect(cached?.pages[0]?.filteredTotalCount).toBe(1);
287
289
  });
288
290
 
289
291
  act(() => {
@@ -296,11 +298,9 @@ describe('workflow run API hooks', () => {
296
298
  let secondTempWorkflowRunId: string | undefined;
297
299
  await waitFor(() => {
298
300
  const cached =
299
- queryClient.getQueryData<InfiniteData<WorkflowRunListResponseDto, string | undefined>>(
300
- listKey,
301
- );
301
+ queryClient.getQueryData<InfiniteData<WorkflowRunListPage, string | undefined>>(listKey);
302
302
  expect(cached?.pages[0]?.runs).toHaveLength(2);
303
- expect(cached?.pages[0]?.filtered_total_count).toBe(2);
303
+ expect(cached?.pages[0]?.filteredTotalCount).toBe(2);
304
304
  secondTempWorkflowRunId = cached?.pages[0]?.runs[0]?.id;
305
305
  expect(secondTempWorkflowRunId).toMatch(TEMP_RUN_ID_PATTERN);
306
306
  });
@@ -311,11 +311,9 @@ describe('workflow run API hooks', () => {
311
311
 
312
312
  await waitFor(() => {
313
313
  const cached =
314
- queryClient.getQueryData<InfiniteData<WorkflowRunListResponseDto, string | undefined>>(
315
- listKey,
316
- );
314
+ queryClient.getQueryData<InfiniteData<WorkflowRunListPage, string | undefined>>(listKey);
317
315
  expect(cached?.pages[0]?.runs.map((run) => run.id)).toEqual([secondTempWorkflowRunId]);
318
- expect(cached?.pages[0]?.filtered_total_count).toBe(1);
316
+ expect(cached?.pages[0]?.filteredTotalCount).toBe(1);
319
317
  });
320
318
 
321
319
  act(() => {
@@ -1,16 +1,20 @@
1
- import type {
2
- RerunWorkflowRunBodyDto,
3
- WorkflowRunAttemptsResponseDto,
4
- WorkflowRunDetailResponseDto,
5
- WorkflowRunDto,
6
- WorkflowRunListResponseDto,
7
- WorkflowRunRerunModeDto,
8
- WorkflowRunResponseDto,
1
+ import {
2
+ type RerunWorkflowRunBodyDto,
3
+ type WorkflowRunRerunModeDto,
4
+ workflowRunAttemptsResponseSchema,
5
+ workflowRunDetailResponseSchema,
6
+ workflowRunDtoSchema,
7
+ workflowRunListResponseSchema,
8
+ workflowRunResponseSchema,
9
9
  } from '@shipfox/api-workflows-dto';
10
- import {apiRequest} from '@shipfox/client-api';
10
+ import {checkedApiRequest, type StandardSchema} from '@shipfox/client-api';
11
11
  import {
12
12
  type InfiniteData,
13
+ infiniteQueryOptions,
13
14
  keepPreviousData,
15
+ queryOptions,
16
+ type UseInfiniteQueryOptions,
17
+ type UseQueryOptions,
14
18
  useInfiniteQuery,
15
19
  useMutation,
16
20
  useQuery,
@@ -18,16 +22,21 @@ import {
18
22
  } from '@tanstack/react-query';
19
23
  import {
20
24
  isWorkflowRunTerminal,
21
- toWorkflowRunAttempt,
22
- toWorkflowRunDetail,
23
- toWorkflowRunListItem,
24
- toWorkflowRunListPage,
25
+ type ManualWorkflowLaunch,
25
26
  type WorkflowRun,
26
27
  type WorkflowRunAttempt,
28
+ WorkflowRunAttemptSummary,
27
29
  type WorkflowRunDetail,
30
+ type WorkflowRunListItem,
28
31
  type WorkflowRunListPage,
29
32
  type WorkflowRunStatus,
30
33
  } from '#core/workflow-run.js';
34
+ import {
35
+ toWorkflowRunAttempt,
36
+ toWorkflowRunDetail,
37
+ toWorkflowRunListItem,
38
+ toWorkflowRunListPage,
39
+ } from './workflow-run-mapper.js';
31
40
 
32
41
  export interface WorkflowRunFilters {
33
42
  status?: WorkflowRunStatus | undefined;
@@ -48,6 +57,35 @@ export const workflowRunsQueryKeys = {
48
57
  [...workflowRunsQueryKeys.all, 'attempts', workflowRunId] as const,
49
58
  };
50
59
 
60
+ type WorkflowRunsListQueryKey =
61
+ | ReturnType<typeof workflowRunsQueryKeys.list>
62
+ | readonly ['workflow-runs', 'list'];
63
+ type WorkflowRunDetailQueryKey =
64
+ | ReturnType<typeof workflowRunsQueryKeys.detail>
65
+ | readonly ['workflow-runs', 'detail'];
66
+ type WorkflowRunAttemptsQueryKey =
67
+ | ReturnType<typeof workflowRunsQueryKeys.attempts>
68
+ | readonly ['workflow-runs', 'attempts'];
69
+ type WorkflowRunsInfiniteQueryOptions = UseInfiniteQueryOptions<
70
+ WorkflowRunListPage,
71
+ Error,
72
+ InfiniteData<WorkflowRunListPage, string | undefined>,
73
+ WorkflowRunsListQueryKey,
74
+ string | undefined
75
+ >;
76
+ type WorkflowRunDetailQueryOptions = UseQueryOptions<
77
+ WorkflowRunDetail,
78
+ Error,
79
+ WorkflowRunDetail,
80
+ WorkflowRunDetailQueryKey
81
+ >;
82
+ type WorkflowRunAttemptsQueryOptions = UseQueryOptions<
83
+ WorkflowRunAttempt[],
84
+ Error,
85
+ WorkflowRunAttempt[],
86
+ WorkflowRunAttemptsQueryKey
87
+ >;
88
+
51
89
  function normalizeFilters(filters: WorkflowRunFilters) {
52
90
  return {
53
91
  status: filters.status ?? null,
@@ -66,7 +104,7 @@ function appendFilters(params: URLSearchParams, filters: WorkflowRunFilters) {
66
104
  if (filters.createdTo) params.set('created_to', filters.createdTo);
67
105
  }
68
106
 
69
- async function listWorkflowRunsDto({
107
+ async function listWorkflowRuns({
70
108
  projectId,
71
109
  filters,
72
110
  limit = 50,
@@ -78,13 +116,16 @@ async function listWorkflowRunsDto({
78
116
  limit?: number;
79
117
  cursor?: string | undefined;
80
118
  signal?: AbortSignal;
81
- }) {
119
+ }): Promise<WorkflowRunListPage> {
82
120
  const params = new URLSearchParams({project_id: projectId, limit: String(limit)});
83
121
  if (cursor) params.set('cursor', cursor);
84
122
  appendFilters(params, filters);
85
- return await apiRequest<WorkflowRunListResponseDto>(`/workflows/runs?${params.toString()}`, {
86
- signal,
87
- });
123
+ const response = await checkedApiRequest(
124
+ workflowRunListResponseSchema,
125
+ `/workflows/runs?${params.toString()}`,
126
+ {signal},
127
+ );
128
+ return toWorkflowRunListPage(response);
88
129
  }
89
130
 
90
131
  /**
@@ -100,35 +141,36 @@ export async function fireManualWorkflow({
100
141
  }: {
101
142
  definitionId: string;
102
143
  inputs?: Record<string, unknown>;
103
- }) {
104
- return await apiRequest<{workflow_run_id: string}>(
144
+ }): Promise<ManualWorkflowLaunch> {
145
+ const response = await checkedApiRequest(
146
+ manualWorkflowResponseSchema,
105
147
  `/workflow-definitions/${definitionId}/fire-manual`,
106
148
  {
107
149
  method: 'POST',
108
150
  body: inputs ? {inputs} : {},
109
151
  },
110
152
  );
153
+ return {workflowRunId: response.workflow_run_id};
111
154
  }
112
155
 
113
156
  const ACTIVE_POLL_MS = 4_000;
114
157
  const IDLE_POLL_MS = 30_000;
115
158
 
116
- type RunListInfinite = InfiniteData<WorkflowRunListResponseDto, string | undefined>;
117
-
118
- function toWorkflowRunInfiniteData(
119
- data: InfiniteData<WorkflowRunListResponseDto, string | undefined>,
120
- ): InfiniteData<WorkflowRunListPage, string | undefined> {
121
- return {
122
- ...data,
123
- pages: data.pages.map(toWorkflowRunListPage),
124
- };
125
- }
159
+ type RunListInfinite = InfiniteData<WorkflowRunListPage, string | undefined>;
126
160
 
127
161
  export function useWorkflowRunsInfiniteQuery(
128
162
  projectId: string | undefined,
129
163
  filters: WorkflowRunFilters,
130
164
  limit = 50,
131
165
  ) {
166
+ return useInfiniteQuery(workflowRunsInfiniteQueryOptions(projectId, filters, limit));
167
+ }
168
+
169
+ export function workflowRunsInfiniteQueryOptions(
170
+ projectId: string | undefined,
171
+ filters: WorkflowRunFilters,
172
+ limit = 50,
173
+ ): WorkflowRunsInfiniteQueryOptions {
132
174
  // Polling is owned by react-query, not the page. Polling fast (4s) while
133
175
  // any non-terminal run is visible covers state transitions; polling slow
134
176
  // (30s) when idle covers brand-new external runs (webhook/schedule
@@ -140,16 +182,15 @@ export function useWorkflowRunsInfiniteQuery(
140
182
  // of rows can drop into a between-pages gap. Users who scrolled into
141
183
  // history opted into "reading mode": pause until they refocus, filter,
142
184
  // or scroll back.
143
- return useInfiniteQuery({
185
+ return infiniteQueryOptions({
144
186
  queryKey: projectId
145
187
  ? workflowRunsQueryKeys.list(projectId, filters)
146
- : [...workflowRunsQueryKeys.all, 'list'],
188
+ : ([...workflowRunsQueryKeys.all, 'list'] as const),
147
189
  enabled: Boolean(projectId),
148
190
  initialPageParam: undefined as string | undefined,
149
191
  queryFn: ({pageParam, signal}) =>
150
- listWorkflowRunsDto({projectId: projectId ?? '', filters, limit, cursor: pageParam, signal}),
151
- getNextPageParam: (lastPage) => lastPage.next_cursor ?? undefined,
152
- select: toWorkflowRunInfiniteData,
192
+ listWorkflowRuns({projectId: projectId ?? '', filters, limit, cursor: pageParam, signal}),
193
+ getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
153
194
  placeholderData: keepPreviousData,
154
195
  staleTime: 2_000,
155
196
  refetchOnWindowFocus: true,
@@ -165,7 +206,7 @@ export function useWorkflowRunsInfiniteQuery(
165
206
  });
166
207
  }
167
208
 
168
- async function getWorkflowRunDto({
209
+ async function getWorkflowRun({
169
210
  workflowRunId,
170
211
  runAttempt,
171
212
  signal,
@@ -173,37 +214,48 @@ async function getWorkflowRunDto({
173
214
  workflowRunId: string;
174
215
  runAttempt?: number | undefined;
175
216
  signal?: AbortSignal;
176
- }) {
217
+ }): Promise<WorkflowRunDetail> {
177
218
  const params = new URLSearchParams();
178
219
  if (runAttempt) params.set('attempt', String(runAttempt));
179
220
  const query = params.size > 0 ? `?${params.toString()}` : '';
180
- return await apiRequest<WorkflowRunDetailResponseDto>(
181
- `/workflows/runs/${workflowRunId}${query}`,
182
- {
183
- signal,
184
- },
221
+ return toWorkflowRunDetail(
222
+ await checkedApiRequest(
223
+ workflowRunDetailResponseSchema,
224
+ `/workflows/runs/${workflowRunId}${query}`,
225
+ {
226
+ signal,
227
+ },
228
+ ),
185
229
  );
186
230
  }
187
231
 
188
- async function getWorkflowRunAttemptsDto({
232
+ async function getWorkflowRunAttempts({
189
233
  workflowRunId,
190
234
  signal,
191
235
  }: {
192
236
  workflowRunId: string;
193
237
  signal?: AbortSignal;
194
- }) {
195
- return await apiRequest<WorkflowRunAttemptsResponseDto>(
238
+ }): Promise<WorkflowRunAttempt[]> {
239
+ const response = await checkedApiRequest(
240
+ workflowRunAttemptsResponseSchema,
196
241
  `/workflows/runs/${workflowRunId}/attempts`,
197
242
  {
198
243
  signal,
199
244
  },
200
245
  );
246
+ return response.attempts.map(toWorkflowRunAttempt);
201
247
  }
202
248
 
203
- async function cancelWorkflowRunDto({workflowRunId}: {workflowRunId: string}) {
204
- return await apiRequest<WorkflowRunDto>(`/workflows/runs/${workflowRunId}/cancel`, {
205
- method: 'POST',
206
- });
249
+ async function cancelWorkflowRun({
250
+ workflowRunId,
251
+ }: {
252
+ workflowRunId: string;
253
+ }): Promise<WorkflowRunListItem> {
254
+ return toWorkflowRunListItem(
255
+ await checkedApiRequest(workflowRunDtoSchema, `/workflows/runs/${workflowRunId}/cancel`, {
256
+ method: 'POST',
257
+ }),
258
+ );
207
259
  }
208
260
 
209
261
  export async function rerunWorkflowRun({
@@ -212,13 +264,32 @@ export async function rerunWorkflowRun({
212
264
  }: {
213
265
  workflowRunId: string;
214
266
  mode: WorkflowRunRerunModeDto;
215
- }) {
216
- return await apiRequest<WorkflowRunResponseDto>(`/workflows/runs/${workflowRunId}/rerun`, {
217
- method: 'POST',
218
- body: {mode} satisfies RerunWorkflowRunBodyDto,
219
- });
267
+ }): Promise<WorkflowRunListItem> {
268
+ return toWorkflowRunListItem(
269
+ await checkedApiRequest(workflowRunResponseSchema, `/workflows/runs/${workflowRunId}/rerun`, {
270
+ method: 'POST',
271
+ body: {mode} satisfies RerunWorkflowRunBodyDto,
272
+ }),
273
+ );
220
274
  }
221
275
 
276
+ const manualWorkflowResponseSchema: StandardSchema<unknown, {workflow_run_id: string}> = {
277
+ '~standard': {
278
+ version: 1,
279
+ vendor: 'client-workflows',
280
+ validate: (value) => {
281
+ if (
282
+ typeof value === 'object' &&
283
+ value !== null &&
284
+ 'workflow_run_id' in value &&
285
+ typeof value.workflow_run_id === 'string'
286
+ )
287
+ return {value: {workflow_run_id: value.workflow_run_id}};
288
+ return {issues: [{message: 'Expected workflow_run_id.'}]};
289
+ },
290
+ },
291
+ };
292
+
222
293
  export function useRerunWorkflowRunMutation(projectId: string) {
223
294
  const queryClient = useQueryClient();
224
295
 
@@ -261,25 +332,36 @@ function buildTempRun({
261
332
  definitionId: string;
262
333
  name: string;
263
334
  createdAt: string;
264
- }): WorkflowRunDto {
335
+ }): WorkflowRunListItem {
336
+ const id = `temp-${cryptoRandomId()}`;
265
337
  return {
266
- id: `temp-${cryptoRandomId()}`,
267
- project_id: projectId,
268
- definition_id: definitionId,
338
+ id,
339
+ projectId,
340
+ definitionId,
269
341
  name,
270
342
  status: 'pending',
271
- current_attempt: 1,
272
- latest_attempt: 1,
273
- trigger_provider: null,
274
- trigger_source: 'manual',
275
- trigger_event: 'fire',
276
- trigger_payload: {source: 'manual', event: 'fire'},
343
+ currentAttempt: 1,
344
+ latestAttempt: 1,
345
+ triggerProvider: null,
346
+ triggerSource: 'manual',
347
+ triggerEvent: 'fire',
348
+ triggerPayload: {source: 'manual', event: 'fire'},
349
+ triggerDisplayLabel: 'fire',
350
+ triggerLabel: 'manual · fire',
277
351
  inputs: null,
278
- source_snapshot: null,
279
- created_at: createdAt,
280
- updated_at: createdAt,
281
- started_at: null,
282
- finished_at: null,
352
+ sourceSnapshot: null,
353
+ createdAt,
354
+ updatedAt: createdAt,
355
+ shortId: id.slice(0, 8),
356
+ isTemporary: true,
357
+ runAttempt: new WorkflowRunAttemptSummary({
358
+ workflowRunId: id,
359
+ attempt: 1,
360
+ status: 'pending',
361
+ createdAt,
362
+ startedAt: null,
363
+ finishedAt: null,
364
+ }),
283
365
  };
284
366
  }
285
367
 
@@ -334,11 +416,11 @@ export function useFireManualWorkflowMutation() {
334
416
  if (!current || current.pages.length === 0) return current;
335
417
  const firstPage = current.pages[0];
336
418
  if (!firstPage) return current;
337
- const nextFirstPage: WorkflowRunListResponseDto = {
419
+ const nextFirstPage: WorkflowRunListPage = {
338
420
  ...firstPage,
339
421
  runs: [tempRun, ...firstPage.runs],
340
- filtered_total_count:
341
- firstPage.filtered_total_count != null ? firstPage.filtered_total_count + 1 : null,
422
+ filteredTotalCount:
423
+ firstPage.filteredTotalCount != null ? firstPage.filteredTotalCount + 1 : null,
342
424
  };
343
425
  return {...current, pages: [nextFirstPage, ...current.pages.slice(1)]};
344
426
  });
@@ -362,9 +444,9 @@ export function useFireManualWorkflowMutation() {
362
444
  return {
363
445
  ...page,
364
446
  runs,
365
- filtered_total_count:
366
- page.filtered_total_count != null
367
- ? Math.max(0, page.filtered_total_count - removedCount)
447
+ filteredTotalCount:
448
+ page.filteredTotalCount != null
449
+ ? Math.max(0, page.filteredTotalCount - removedCount)
368
450
  : null,
369
451
  };
370
452
  });
@@ -401,7 +483,7 @@ function lookupDefinitionName(
401
483
  definitionId: string,
402
484
  ): string | undefined {
403
485
  const entries = queryClient.getQueriesData<
404
- InfiniteData<{definitions: Array<{id: string; project_id: string; name: string}>}>
486
+ InfiniteData<{definitions: Array<{id: string; name: string}>}>
405
487
  >({queryKey: ['definitions', 'list', projectId]});
406
488
  for (const [, data] of entries) {
407
489
  if (!data) continue;
@@ -424,21 +506,29 @@ export function useWorkflowRunAttemptQuery({
424
506
  workflowRunId: string | undefined;
425
507
  runAttempt?: number | undefined;
426
508
  }) {
509
+ return useQuery(workflowRunQueryOptions({workflowRunId, runAttempt}));
510
+ }
511
+
512
+ export function workflowRunQueryOptions({
513
+ workflowRunId,
514
+ runAttempt,
515
+ }: {
516
+ workflowRunId: string | undefined;
517
+ runAttempt?: number | undefined;
518
+ }): WorkflowRunDetailQueryOptions {
427
519
  // Poll a non-terminal run so the open run detail stays live (same cadence as the run
428
520
  // list); stop once the run is terminal.
429
- return useQuery({
521
+ return queryOptions({
430
522
  queryKey: workflowRunId
431
523
  ? workflowRunsQueryKeys.detail(workflowRunId, runAttempt)
432
- : [...workflowRunsQueryKeys.all, 'detail'],
524
+ : ([...workflowRunsQueryKeys.all, 'detail'] as const),
433
525
  enabled: Boolean(workflowRunId),
434
- queryFn: ({signal}) =>
435
- getWorkflowRunDto({workflowRunId: workflowRunId ?? '', runAttempt, signal}),
436
- select: toWorkflowRunDetail,
526
+ queryFn: ({signal}) => getWorkflowRun({workflowRunId: workflowRunId ?? '', runAttempt, signal}),
437
527
  staleTime: 2_000,
438
528
  refetchOnWindowFocus: true,
439
529
  refetchInterval: (query) => {
440
530
  const status: WorkflowRunDetail['runAttempt']['status'] | undefined =
441
- query.state.data?.run_attempt.status;
531
+ query.state.data?.runAttempt.status;
442
532
  if (!status) return false;
443
533
  return isWorkflowRunTerminal(status) ? false : ACTIVE_POLL_MS;
444
534
  },
@@ -453,13 +543,22 @@ export function useWorkflowRunAttemptsQuery({
453
543
  workflowRunId: string | undefined;
454
544
  enabled: boolean;
455
545
  }) {
456
- return useQuery({
546
+ return useQuery(workflowRunAttemptsQueryOptions({workflowRunId, enabled}));
547
+ }
548
+
549
+ export function workflowRunAttemptsQueryOptions({
550
+ workflowRunId,
551
+ enabled,
552
+ }: {
553
+ workflowRunId: string | undefined;
554
+ enabled: boolean;
555
+ }): WorkflowRunAttemptsQueryOptions {
556
+ return queryOptions({
457
557
  queryKey: workflowRunId
458
558
  ? workflowRunsQueryKeys.attempts(workflowRunId)
459
- : [...workflowRunsQueryKeys.all, 'attempts'],
559
+ : ([...workflowRunsQueryKeys.all, 'attempts'] as const),
460
560
  enabled: Boolean(workflowRunId) && enabled,
461
- queryFn: ({signal}) => getWorkflowRunAttemptsDto({workflowRunId: workflowRunId ?? '', signal}),
462
- select: (dto): WorkflowRunAttempt[] => dto.attempts.map(toWorkflowRunAttempt),
561
+ queryFn: ({signal}) => getWorkflowRunAttempts({workflowRunId: workflowRunId ?? '', signal}),
463
562
  staleTime: 0,
464
563
  refetchOnMount: true,
465
564
  refetchOnWindowFocus: false,
@@ -469,9 +568,9 @@ export function useWorkflowRunAttemptsQuery({
469
568
  export function useCancelWorkflowRunMutation(run: WorkflowRun | undefined) {
470
569
  const queryClient = useQueryClient();
471
570
  return useMutation({
472
- mutationFn: async () => {
571
+ mutationFn: async (): Promise<WorkflowRunListItem> => {
473
572
  if (!run) throw new Error('Workflow run is not loaded');
474
- return toWorkflowRunListItem(await cancelWorkflowRunDto({workflowRunId: run.id}));
573
+ return await cancelWorkflowRun({workflowRunId: run.id});
475
574
  },
476
575
  onSuccess: async () => {
477
576
  if (!run) return;