@shipfox/client-workflows 12.0.2 → 14.0.0

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 (325) hide show
  1. package/.storybook/main.ts +16 -1
  2. package/.storybook/preview.tsx +3 -3
  3. package/.storybook/workspace-source.ts +31 -0
  4. package/.turbo/turbo-build.log +1 -1
  5. package/CHANGELOG.md +79 -0
  6. package/dist/components/job-graph/job-duration-format.d.ts +2 -2
  7. package/dist/components/job-graph/job-duration-format.d.ts.map +1 -1
  8. package/dist/components/job-graph/job-duration-format.js +4 -2
  9. package/dist/components/job-graph/job-duration-format.js.map +1 -1
  10. package/dist/components/job-graph/job-duration-label.js +1 -1
  11. package/dist/components/job-graph/job-duration-label.js.map +1 -1
  12. package/dist/components/job-graph/job-graph-content.d.ts +2 -1
  13. package/dist/components/job-graph/job-graph-content.d.ts.map +1 -1
  14. package/dist/components/job-graph/job-graph-content.js +3 -3
  15. package/dist/components/job-graph/job-graph-content.js.map +1 -1
  16. package/dist/components/job-graph/job-graph-view.d.ts +2 -1
  17. package/dist/components/job-graph/job-graph-view.d.ts.map +1 -1
  18. package/dist/components/job-graph/job-graph-view.js +2 -2
  19. package/dist/components/job-graph/job-graph-view.js.map +1 -1
  20. package/dist/components/job-graph/job-node.d.ts +1 -1
  21. package/dist/components/job-graph/job-node.d.ts.map +1 -1
  22. package/dist/components/job-graph/job-node.js +10 -9
  23. package/dist/components/job-graph/job-node.js.map +1 -1
  24. package/dist/components/job-graph/types.d.ts +2 -1
  25. package/dist/components/job-graph/types.d.ts.map +1 -1
  26. package/dist/components/job-graph/types.js.map +1 -1
  27. package/dist/components/step-list/step-list.d.ts +2 -2
  28. package/dist/components/step-list/step-list.d.ts.map +1 -1
  29. package/dist/components/step-list/step-list.js.map +1 -1
  30. package/dist/components/workflow-run-duration-label.js +1 -1
  31. package/dist/components/workflow-run-duration-label.js.map +1 -1
  32. package/dist/components/workflow-run-list/job-status-strip.d.ts +21 -0
  33. package/dist/components/workflow-run-list/job-status-strip.d.ts.map +1 -0
  34. package/dist/components/workflow-run-list/job-status-strip.js +185 -0
  35. package/dist/components/workflow-run-list/job-status-strip.js.map +1 -0
  36. package/dist/components/workflow-run-list/run-display.d.ts +27 -3
  37. package/dist/components/workflow-run-list/run-display.d.ts.map +1 -1
  38. package/dist/components/workflow-run-list/run-display.js +78 -7
  39. package/dist/components/workflow-run-list/run-display.js.map +1 -1
  40. package/dist/components/workflow-run-list/types.d.ts +23 -24
  41. package/dist/components/workflow-run-list/types.d.ts.map +1 -1
  42. package/dist/components/workflow-run-list/types.js.map +1 -1
  43. package/dist/components/workflow-run-list/workflow-run-filter-menu.d.ts +22 -0
  44. package/dist/components/workflow-run-list/workflow-run-filter-menu.d.ts.map +1 -0
  45. package/dist/components/workflow-run-list/workflow-run-filter-menu.js +68 -0
  46. package/dist/components/workflow-run-list/workflow-run-filter-menu.js.map +1 -0
  47. package/dist/components/workflow-run-list/workflow-run-filters.d.ts +18 -0
  48. package/dist/components/workflow-run-list/workflow-run-filters.d.ts.map +1 -0
  49. package/dist/components/workflow-run-list/workflow-run-filters.js +236 -0
  50. package/dist/components/workflow-run-list/workflow-run-filters.js.map +1 -0
  51. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts +8 -4
  52. package/dist/components/workflow-run-list/workflow-run-list-content.d.ts.map +1 -1
  53. package/dist/components/workflow-run-list/workflow-run-list-content.js +40 -12
  54. package/dist/components/workflow-run-list/workflow-run-list-content.js.map +1 -1
  55. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts +29 -2
  56. package/dist/components/workflow-run-list/workflow-run-list-states.d.ts.map +1 -1
  57. package/dist/components/workflow-run-list/workflow-run-list-states.js +103 -44
  58. package/dist/components/workflow-run-list/workflow-run-list-states.js.map +1 -1
  59. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts +1 -1
  60. package/dist/components/workflow-run-list/workflow-run-list-view.d.ts.map +1 -1
  61. package/dist/components/workflow-run-list/workflow-run-list-view.js +60 -43
  62. package/dist/components/workflow-run-list/workflow-run-list-view.js.map +1 -1
  63. package/dist/components/workflow-run-list/workflow-run-list.d.ts +1 -1
  64. package/dist/components/workflow-run-list/workflow-run-list.d.ts.map +1 -1
  65. package/dist/components/workflow-run-list/workflow-run-list.js +20 -8
  66. package/dist/components/workflow-run-list/workflow-run-list.js.map +1 -1
  67. package/dist/components/workflow-run-list/workflow-run-row.d.ts +19 -9
  68. package/dist/components/workflow-run-list/workflow-run-row.d.ts.map +1 -1
  69. package/dist/components/workflow-run-list/workflow-run-row.js +179 -112
  70. package/dist/components/workflow-run-list/workflow-run-row.js.map +1 -1
  71. package/dist/components/workflow-run-number-label.d.ts +15 -0
  72. package/dist/components/workflow-run-number-label.d.ts.map +1 -0
  73. package/dist/components/workflow-run-number-label.js +41 -0
  74. package/dist/components/workflow-run-number-label.js.map +1 -0
  75. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts +3 -3
  76. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.d.ts.map +1 -1
  77. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js +10 -9
  78. package/dist/components/workflow-run-summary/workflow-run-attempt-switcher.js.map +1 -1
  79. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts +4 -9
  80. package/dist/components/workflow-run-summary/workflow-run-summary.d.ts.map +1 -1
  81. package/dist/components/workflow-run-summary/workflow-run-summary.js +107 -74
  82. package/dist/components/workflow-run-summary/workflow-run-summary.js.map +1 -1
  83. package/dist/components/workflow-run-tabs/index.d.ts +6 -0
  84. package/dist/components/workflow-run-tabs/index.d.ts.map +1 -0
  85. package/dist/components/workflow-run-tabs/index.js +7 -0
  86. package/dist/components/workflow-run-tabs/index.js.map +1 -0
  87. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts +11 -0
  88. package/dist/components/workflow-run-tabs/run-annotation-summary-line.d.ts.map +1 -0
  89. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js +71 -0
  90. package/dist/components/workflow-run-tabs/run-annotation-summary-line.js.map +1 -0
  91. package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts +2 -0
  92. package/dist/components/workflow-run-tabs/run-annotations-empty.d.ts.map +1 -0
  93. package/dist/components/workflow-run-tabs/run-annotations-empty.js +11 -0
  94. package/dist/components/workflow-run-tabs/run-annotations-empty.js.map +1 -0
  95. package/dist/components/workflow-run-tabs/run-jobs-list.d.ts +18 -0
  96. package/dist/components/workflow-run-tabs/run-jobs-list.d.ts.map +1 -0
  97. package/dist/components/workflow-run-tabs/run-jobs-list.js +107 -0
  98. package/dist/components/workflow-run-tabs/run-jobs-list.js.map +1 -0
  99. package/dist/components/workflow-run-tabs/run-tab-count.d.ts +6 -0
  100. package/dist/components/workflow-run-tabs/run-tab-count.d.ts.map +1 -0
  101. package/dist/components/workflow-run-tabs/run-tab-count.js +27 -0
  102. package/dist/components/workflow-run-tabs/run-tab-count.js.map +1 -0
  103. package/dist/components/workflow-run-tabs/run-tab-strip.d.ts +8 -0
  104. package/dist/components/workflow-run-tabs/run-tab-strip.d.ts.map +1 -0
  105. package/dist/components/workflow-run-tabs/run-tab-strip.js +82 -0
  106. package/dist/components/workflow-run-tabs/run-tab-strip.js.map +1 -0
  107. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts +2 -2
  108. package/dist/components/workflow-run-view/agent-config-failure-callout.d.ts.map +1 -1
  109. package/dist/components/workflow-run-view/agent-config-failure-callout.js +4 -4
  110. package/dist/components/workflow-run-view/agent-config-failure-callout.js.map +1 -1
  111. package/dist/components/workflow-run-view/job-card.d.ts +2 -2
  112. package/dist/components/workflow-run-view/job-card.d.ts.map +1 -1
  113. package/dist/components/workflow-run-view/job-card.js +51 -32
  114. package/dist/components/workflow-run-view/job-card.js.map +1 -1
  115. package/dist/components/workflow-run-view/job-execution-switcher.d.ts +1 -1
  116. package/dist/components/workflow-run-view/job-execution-switcher.d.ts.map +1 -1
  117. package/dist/components/workflow-run-view/job-execution-switcher.js +20 -8
  118. package/dist/components/workflow-run-view/job-execution-switcher.js.map +1 -1
  119. package/dist/components/workflow-run-view/workflow-run-selection.d.ts.map +1 -1
  120. package/dist/components/workflow-run-view/workflow-run-selection.js +40 -1
  121. package/dist/components/workflow-run-view/workflow-run-selection.js.map +1 -1
  122. package/dist/components/workflow-run-view/workflow-run-states.d.ts +1 -0
  123. package/dist/components/workflow-run-view/workflow-run-states.d.ts.map +1 -1
  124. package/dist/components/workflow-run-view/workflow-run-states.js +10 -1
  125. package/dist/components/workflow-run-view/workflow-run-states.js.map +1 -1
  126. package/dist/components/workflow-run-view/workflow-run-view.d.ts +10 -5
  127. package/dist/components/workflow-run-view/workflow-run-view.d.ts.map +1 -1
  128. package/dist/components/workflow-run-view/workflow-run-view.js +136 -85
  129. package/dist/components/workflow-run-view/workflow-run-view.js.map +1 -1
  130. package/dist/components/workflow-source-panel/index.d.ts +1 -0
  131. package/dist/components/workflow-source-panel/index.d.ts.map +1 -1
  132. package/dist/components/workflow-source-panel/index.js +1 -0
  133. package/dist/components/workflow-source-panel/index.js.map +1 -1
  134. package/dist/components/workflow-source-panel/workflow-source-content.d.ts +12 -0
  135. package/dist/components/workflow-source-panel/workflow-source-content.d.ts.map +1 -0
  136. package/dist/components/workflow-source-panel/workflow-source-content.js +53 -0
  137. package/dist/components/workflow-source-panel/workflow-source-content.js.map +1 -0
  138. package/dist/components/workflow-source-panel/workflow-source-panel.d.ts +1 -1
  139. package/dist/components/workflow-source-panel/workflow-source-panel.d.ts.map +1 -1
  140. package/dist/components/workflow-source-panel/workflow-source-panel.js +7 -54
  141. package/dist/components/workflow-source-panel/workflow-source-panel.js.map +1 -1
  142. package/dist/components/workflow-status/status-visuals.d.ts +4 -4
  143. package/dist/components/workflow-status/status-visuals.d.ts.map +1 -1
  144. package/dist/components/workflow-status/status-visuals.js +7 -0
  145. package/dist/components/workflow-status/status-visuals.js.map +1 -1
  146. package/dist/components/workflow-status/workflow-status-icon.d.ts +3 -4
  147. package/dist/components/workflow-status/workflow-status-icon.d.ts.map +1 -1
  148. package/dist/components/workflow-status/workflow-status-icon.js +6 -4
  149. package/dist/components/workflow-status/workflow-status-icon.js.map +1 -1
  150. package/dist/core/entities/job-execution.d.ts +10 -0
  151. package/dist/core/entities/job-execution.d.ts.map +1 -1
  152. package/dist/core/entities/job-execution.js +11 -0
  153. package/dist/core/entities/job-execution.js.map +1 -1
  154. package/dist/core/entities/job.d.ts +4 -2
  155. package/dist/core/entities/job.d.ts.map +1 -1
  156. package/dist/core/entities/job.js +8 -1
  157. package/dist/core/entities/job.js.map +1 -1
  158. package/dist/core/entities/step.d.ts +1 -1
  159. package/dist/core/entities/step.d.ts.map +1 -1
  160. package/dist/core/entities/step.js +2 -0
  161. package/dist/core/entities/step.js.map +1 -1
  162. package/dist/core/entities/workflow-run.d.ts +52 -4
  163. package/dist/core/entities/workflow-run.d.ts.map +1 -1
  164. package/dist/core/entities/workflow-run.js +22 -3
  165. package/dist/core/entities/workflow-run.js.map +1 -1
  166. package/dist/core/workflow-run-tabs.d.ts +9 -0
  167. package/dist/core/workflow-run-tabs.d.ts.map +1 -0
  168. package/dist/core/workflow-run-tabs.js +3 -0
  169. package/dist/core/workflow-run-tabs.js.map +1 -0
  170. package/dist/core/workflow-run.d.ts +6 -6
  171. package/dist/core/workflow-run.d.ts.map +1 -1
  172. package/dist/core/workflow-run.js +3 -3
  173. package/dist/core/workflow-run.js.map +1 -1
  174. package/dist/feature.d.ts +7 -7
  175. package/dist/feature.js +5 -5
  176. package/dist/feature.js.map +1 -1
  177. package/dist/hooks/api/workflow-run-mapper.d.ts +4 -3
  178. package/dist/hooks/api/workflow-run-mapper.d.ts.map +1 -1
  179. package/dist/hooks/api/workflow-run-mapper.js +31 -3
  180. package/dist/hooks/api/workflow-run-mapper.js.map +1 -1
  181. package/dist/hooks/api/workflow-runs.d.ts +4 -4
  182. package/dist/hooks/api/workflow-runs.d.ts.map +1 -1
  183. package/dist/hooks/api/workflow-runs.js +12 -4
  184. package/dist/hooks/api/workflow-runs.js.map +1 -1
  185. package/dist/index.d.ts +4 -3
  186. package/dist/index.d.ts.map +1 -1
  187. package/dist/index.js +3 -2
  188. package/dist/index.js.map +1 -1
  189. package/dist/pages/project-workflows-page.js +46 -1
  190. package/dist/pages/project-workflows-page.js.map +1 -1
  191. package/dist/pages/workflow-run-detail-page.d.ts +11 -0
  192. package/dist/pages/workflow-run-detail-page.d.ts.map +1 -0
  193. package/dist/pages/workflow-run-detail-page.js +52 -0
  194. package/dist/pages/workflow-run-detail-page.js.map +1 -0
  195. package/dist/pages/workflow-run-list-page.d.ts +10 -0
  196. package/dist/pages/workflow-run-list-page.d.ts.map +1 -0
  197. package/dist/pages/workflow-run-list-page.js +43 -0
  198. package/dist/pages/workflow-run-list-page.js.map +1 -0
  199. package/dist/routes/inputs.d.ts +94 -5
  200. package/dist/routes/inputs.d.ts.map +1 -1
  201. package/dist/routes/inputs.js +199 -13
  202. package/dist/routes/inputs.js.map +1 -1
  203. package/dist/routes/project-route.d.ts +6 -0
  204. package/dist/routes/project-route.d.ts.map +1 -0
  205. package/dist/routes/project-route.js +25 -0
  206. package/dist/routes/project-route.js.map +1 -0
  207. package/dist/routes/run-detail.d.ts.map +1 -1
  208. package/dist/routes/run-detail.js +12 -7
  209. package/dist/routes/run-detail.js.map +1 -1
  210. package/dist/routes/runs.d.ts.map +1 -1
  211. package/dist/routes/runs.js +11 -6
  212. package/dist/routes/runs.js.map +1 -1
  213. package/dist/routes/workflows.d.ts.map +1 -1
  214. package/dist/routes/workflows.js +6 -5
  215. package/dist/routes/workflows.js.map +1 -1
  216. package/dist/tsconfig.test.tsbuildinfo +1 -1
  217. package/package.json +9 -9
  218. package/src/components/job-graph/job-duration-format.ts +11 -2
  219. package/src/components/job-graph/job-duration-label.tsx +5 -1
  220. package/src/components/job-graph/job-graph-content.tsx +7 -4
  221. package/src/components/job-graph/job-graph-view.test.tsx +19 -8
  222. package/src/components/job-graph/job-graph-view.tsx +6 -3
  223. package/src/components/job-graph/job-graph.stories.tsx +24 -20
  224. package/src/components/job-graph/job-node.stories.tsx +11 -6
  225. package/src/components/job-graph/job-node.test.tsx +82 -12
  226. package/src/components/job-graph/job-node.tsx +22 -15
  227. package/src/components/job-graph/types.ts +5 -1
  228. package/src/components/step-list/step-list.stories.tsx +2 -3
  229. package/src/components/step-list/step-list.tsx +3 -2
  230. package/src/components/workflow-run-duration-label.tsx +1 -1
  231. package/src/components/workflow-run-list/job-status-strip.tsx +189 -0
  232. package/src/components/workflow-run-list/run-display.test.ts +169 -30
  233. package/src/components/workflow-run-list/run-display.ts +116 -9
  234. package/src/components/workflow-run-list/types.ts +21 -17
  235. package/src/components/workflow-run-list/workflow-run-filter-menu.tsx +100 -0
  236. package/src/components/workflow-run-list/workflow-run-filters.tsx +243 -0
  237. package/src/components/workflow-run-list/workflow-run-list-content.tsx +44 -16
  238. package/src/components/workflow-run-list/workflow-run-list-states.tsx +128 -27
  239. package/src/components/workflow-run-list/workflow-run-list-view.test.tsx +441 -104
  240. package/src/components/workflow-run-list/workflow-run-list-view.tsx +73 -43
  241. package/src/components/workflow-run-list/workflow-run-list.stories.tsx +140 -20
  242. package/src/components/workflow-run-list/workflow-run-list.tsx +20 -10
  243. package/src/components/workflow-run-list/workflow-run-row.tsx +173 -106
  244. package/src/components/workflow-run-number-label.test.tsx +45 -0
  245. package/src/components/workflow-run-number-label.tsx +47 -0
  246. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.test.tsx +7 -8
  247. package/src/components/workflow-run-summary/workflow-run-attempt-switcher.tsx +15 -14
  248. package/src/components/workflow-run-summary/workflow-run-summary.stories.tsx +29 -38
  249. package/src/components/workflow-run-summary/workflow-run-summary.test.tsx +30 -24
  250. package/src/components/workflow-run-summary/workflow-run-summary.tsx +102 -77
  251. package/src/components/workflow-run-tabs/index.ts +5 -0
  252. package/src/components/workflow-run-tabs/run-annotation-summary-line.test.tsx +69 -0
  253. package/src/components/workflow-run-tabs/run-annotation-summary-line.tsx +97 -0
  254. package/src/components/workflow-run-tabs/run-annotations-empty.tsx +11 -0
  255. package/src/components/workflow-run-tabs/run-jobs-list.tsx +151 -0
  256. package/src/components/workflow-run-tabs/run-tab-count.tsx +28 -0
  257. package/src/components/workflow-run-tabs/run-tab-strip.stories.tsx +52 -0
  258. package/src/components/workflow-run-tabs/run-tab-strip.test.tsx +117 -0
  259. package/src/components/workflow-run-tabs/run-tab-strip.tsx +97 -0
  260. package/src/components/workflow-run-view/agent-config-failure-callout.stories.tsx +2 -3
  261. package/src/components/workflow-run-view/agent-config-failure-callout.tsx +4 -4
  262. package/src/components/workflow-run-view/job-card.stories.tsx +11 -10
  263. package/src/components/workflow-run-view/job-card.test.tsx +115 -0
  264. package/src/components/workflow-run-view/job-card.tsx +74 -35
  265. package/src/components/workflow-run-view/job-execution-switcher.tsx +31 -9
  266. package/src/components/workflow-run-view/workflow-run-selection.test.ts +48 -0
  267. package/src/components/workflow-run-view/workflow-run-selection.ts +48 -1
  268. package/src/components/workflow-run-view/workflow-run-states.tsx +12 -1
  269. package/src/components/workflow-run-view/workflow-run-view.test.tsx +152 -94
  270. package/src/components/workflow-run-view/workflow-run-view.tsx +219 -95
  271. package/src/components/workflow-source-panel/index.ts +1 -0
  272. package/src/components/workflow-source-panel/workflow-source-content.tsx +84 -0
  273. package/src/components/workflow-source-panel/workflow-source-panel.tsx +14 -81
  274. package/src/components/workflow-status/status-visuals.test.ts +9 -0
  275. package/src/components/workflow-status/status-visuals.ts +6 -4
  276. package/src/components/workflow-status/workflow-status-icon.stories.tsx +10 -2
  277. package/src/components/workflow-status/workflow-status-icon.test.tsx +18 -0
  278. package/src/components/workflow-status/workflow-status-icon.tsx +17 -24
  279. package/src/core/entities/job-execution.ts +22 -0
  280. package/src/core/entities/job-status.test.ts +104 -0
  281. package/src/core/entities/job.ts +18 -3
  282. package/src/core/entities/step.ts +4 -0
  283. package/src/core/entities/workflow-run.ts +76 -7
  284. package/src/core/workflow-run-tabs.ts +8 -0
  285. package/src/core/workflow-run.test.ts +24 -9
  286. package/src/core/workflow-run.ts +17 -2
  287. package/src/feature.ts +5 -5
  288. package/src/hooks/api/workflow-run-mapper.ts +30 -3
  289. package/src/hooks/api/workflow-runs.test.tsx +2 -0
  290. package/src/hooks/api/workflow-runs.ts +12 -7
  291. package/src/index.ts +7 -2
  292. package/src/page-harness-budget.test.ts +1 -1
  293. package/src/pages/project-workflows-page.test.tsx +59 -5
  294. package/src/pages/project-workflows-page.tsx +34 -1
  295. package/src/pages/workflow-run-detail-page.tsx +72 -0
  296. package/src/pages/workflow-run-list-page.tsx +65 -0
  297. package/src/pages/workflow-run-pages.test.tsx +823 -0
  298. package/src/routes/inputs.test.ts +240 -12
  299. package/src/routes/inputs.ts +215 -11
  300. package/src/routes/project-route.tsx +25 -0
  301. package/src/routes/run-detail.tsx +15 -8
  302. package/src/routes/runs.tsx +14 -7
  303. package/src/routes/workflows.tsx +5 -4
  304. package/src/storybook-config.test.ts +27 -0
  305. package/test/fixtures/workflow-run.ts +76 -7
  306. package/test/pages.tsx +18 -8
  307. package/test/render.tsx +8 -2
  308. package/tsconfig.build.tsbuildinfo +1 -1
  309. package/tsconfig.test.json +7 -1
  310. package/dist/components/workflow-run-list/workflow-run-list-header.d.ts +0 -10
  311. package/dist/components/workflow-run-list/workflow-run-list-header.d.ts.map +0 -1
  312. package/dist/components/workflow-run-list/workflow-run-list-header.js +0 -55
  313. package/dist/components/workflow-run-list/workflow-run-list-header.js.map +0 -1
  314. package/dist/pages/workflow-run-first-time-use.d.ts +0 -6
  315. package/dist/pages/workflow-run-first-time-use.d.ts.map +0 -1
  316. package/dist/pages/workflow-run-first-time-use.js +0 -17
  317. package/dist/pages/workflow-run-first-time-use.js.map +0 -1
  318. package/dist/pages/workflow-run-page.d.ts +0 -10
  319. package/dist/pages/workflow-run-page.d.ts.map +0 -1
  320. package/dist/pages/workflow-run-page.js +0 -90
  321. package/dist/pages/workflow-run-page.js.map +0 -1
  322. package/src/components/workflow-run-list/workflow-run-list-header.tsx +0 -53
  323. package/src/pages/workflow-run-first-time-use.tsx +0 -17
  324. package/src/pages/workflow-run-page.test.tsx +0 -519
  325. package/src/pages/workflow-run-page.tsx +0 -94
@@ -1,22 +1,250 @@
1
- import {validateWorkflowRunsSearch, workflowRouteParams} from './inputs.js';
1
+ import {parseAppSearch, stringifyAppSearch} from '@shipfox/client-shell/runtime';
2
+ import {
3
+ applyWorkflowRunFilterPatch,
4
+ clearWorkflowRunFilters,
5
+ hasWorkflowRunFilters,
6
+ validateWorkflowRunsSearch,
7
+ type WorkflowRunsSearch,
8
+ workflowRouteParams,
9
+ workflowRunListSearchParams,
10
+ workflowRunSearchParams,
11
+ workflowRunTab,
12
+ } from './inputs.js';
2
13
 
3
- describe('workflow route inputs', () => {
4
- it('normalizes malformed search values to safe defaults', () => {
5
- expect(validateWorkflowRunsSearch({search: ['unexpected'], status: 'unknown'})).toEqual({});
6
- expect(validateWorkflowRunsSearch({status: 'failed', runAttempt: '2'})).toEqual({
7
- status: 'failed',
14
+ /** Everything a filter change writes, then reads back, exactly as the router would. */
15
+ function roundTrip(search: WorkflowRunsSearch): WorkflowRunsSearch {
16
+ const query = stringifyAppSearch(workflowRunSearchParams(search, {}));
17
+ return validateWorkflowRunsSearch(parseAppSearch(query));
18
+ }
19
+
20
+ describe('validateWorkflowRunsSearch', () => {
21
+ test('drops malformed values instead of rejecting the URL', () => {
22
+ expect(
23
+ validateWorkflowRunsSearch({
24
+ search: ['unexpected'],
25
+ status: 'unknown',
26
+ tab: 'unknown',
27
+ severity: 'critical',
28
+ after: 'yesterday',
29
+ before: '2026-13-01',
30
+ }),
31
+ ).toEqual({});
32
+ });
33
+
34
+ test('ignores parameters it does not recognize', () => {
35
+ expect(validateWorkflowRunsSearch({search: 'deploy', page: '3', sort: 'name'})).toEqual({
36
+ search: 'deploy',
37
+ });
38
+ });
39
+
40
+ test('reads a single repeated key as a one-element list', () => {
41
+ expect(validateWorkflowRunsSearch({status: 'failed'})).toEqual({status: ['failed']});
42
+ });
43
+
44
+ test('keeps only the statuses it knows out of a mixed list', () => {
45
+ expect(validateWorkflowRunsSearch({status: ['failed', 'nonsense', 'running']})).toEqual({
46
+ status: ['failed', 'running'],
47
+ });
48
+ });
49
+
50
+ test('coerces a numeric-looking branch back to a string', () => {
51
+ expect(validateWorkflowRunsSearch({branch: 2024})).toEqual({branch: ['2024']});
52
+ });
53
+
54
+ test('deduplicates a repeated value', () => {
55
+ expect(validateWorkflowRunsSearch({branch: ['main', 'main', 'next']})).toEqual({
56
+ branch: ['main', 'next'],
57
+ });
58
+ });
59
+
60
+ test('rejects a well-shaped but impossible date', () => {
61
+ expect(validateWorkflowRunsSearch({after: '2026-02-31'})).toEqual({});
62
+ expect(validateWorkflowRunsSearch({after: '2026-02-28'})).toEqual({after: '2026-02-28'});
63
+ });
64
+
65
+ test('keeps the run selection parameters the detail route owns', () => {
66
+ expect(validateWorkflowRunsSearch({status: 'failed', runAttempt: '2', job: 'job-1'})).toEqual({
67
+ status: ['failed'],
68
+ runAttempt: 2,
69
+ jobId: 'job-1',
70
+ });
71
+ });
72
+
73
+ test('accepts known tabs and annotation severities while dropping unknown values', () => {
74
+ expect(
75
+ validateWorkflowRunsSearch({tab: 'annotations', annotation: 'run-1', severity: 'error'}),
76
+ ).toEqual({tab: 'annotations', annotation: 'run-1', severity: 'error'});
77
+ expect(validateWorkflowRunsSearch({tab: 'unknown', severity: 'critical'})).toEqual({});
78
+ expect(workflowRunTab({})).toBe('summary');
79
+ expect(workflowRunTab({tab: 'source'})).toBe('source');
80
+ });
81
+
82
+ test.each([
83
+ ['job', {jobId: 'job-1'}],
84
+ ['job execution', {jobExecutionId: 'execution-1'}],
85
+ ['step', {stepId: 'step-1'}],
86
+ ['step attempt', {stepAttemptId: 'attempt-1'}],
87
+ ])('defaults a legacy %s selection URL to Jobs', (_selection, search) => {
88
+ expect(workflowRunTab(search)).toBe('jobs');
89
+ });
90
+
91
+ test('keeps an explicit Summary tab ahead of legacy selection inference', () => {
92
+ expect(workflowRunTab({tab: 'summary', jobId: 'job-1'})).toBe('summary');
93
+ });
94
+ });
95
+
96
+ describe('the run list URL contract', () => {
97
+ test.each([
98
+ ['search', {search: 'deploy-web'}],
99
+ ['status', {status: ['failed' as const, 'running' as const]}],
100
+ ['branch', {branch: ['main', 'release/v2']}],
101
+ ['actor', {actor: ['octocat', 'hubot']}],
102
+ ['event', {event: ['push', 'pull_request']}],
103
+ ['after and before', {after: '2026-05-01', before: '2026-05-31'}],
104
+ ])('round-trips %s through the URL', (_param, search) => {
105
+ expect(roundTrip(search)).toEqual(search);
106
+ });
107
+
108
+ test('round-trips every parameter at once', () => {
109
+ const search: WorkflowRunsSearch = {
110
+ search: 'deploy',
111
+ status: ['failed', 'cancelled'],
112
+ branch: ['main'],
113
+ actor: ['octocat'],
114
+ event: ['push'],
115
+ after: '2026-05-01',
116
+ before: '2026-05-31',
117
+ tab: 'annotations',
118
+ annotation: 'annotation-1',
119
+ severity: 'warning',
120
+ };
121
+
122
+ expect(roundTrip(search)).toEqual(search);
123
+ });
124
+
125
+ test('repeats the key rather than comma-joining a multi-select', () => {
126
+ const query = stringifyAppSearch(workflowRunSearchParams({status: ['failed', 'running']}, {}));
127
+
128
+ expect(query).toBe('?status=failed&status=running');
129
+ });
130
+
131
+ test('survives a branch name containing a comma', () => {
132
+ expect(roundTrip({branch: ['release,v2', 'main']})).toEqual({branch: ['release,v2', 'main']});
133
+ });
134
+
135
+ // A branch named for a year, a release train called `false`: names that happen to be valid
136
+ // JSON have to come back as themselves, or the filter matches nothing and the shared link
137
+ // is broken for good.
138
+ test.each([
139
+ ['a numeric branch beside a plain one', {branch: ['2024', 'main']}],
140
+ ['two numeric branches', {branch: ['2024', '2025']}],
141
+ ['a boolean-looking branch', {branch: ['false', 'main']}],
142
+ ['numeric actors', {actor: ['123', '456']}],
143
+ ])('survives %s', (_case, search) => {
144
+ expect(roundTrip(search)).toEqual(search);
145
+ });
146
+
147
+ test('writes nothing for an unfiltered list', () => {
148
+ expect(stringifyAppSearch(workflowRunSearchParams({}, {}))).toBe('');
149
+ });
150
+
151
+ test('omits the default Summary tab from the URL', () => {
152
+ expect(stringifyAppSearch(workflowRunSearchParams({tab: 'summary'}, {}))).toBe('');
153
+ expect(stringifyAppSearch(workflowRunSearchParams({tab: 'jobs'}, {}))).toBe('?tab=jobs');
154
+ });
155
+
156
+ test('serializes only list filters for the detail back link', () => {
157
+ expect(
158
+ workflowRunListSearchParams({
159
+ search: 'deploy',
160
+ status: ['running'],
161
+ tab: 'jobs',
162
+ annotation: 'annotation-1',
163
+ severity: 'error',
164
+ jobId: 'job-1',
165
+ }),
166
+ ).toEqual({search: 'deploy', status: ['running']});
167
+ });
168
+
169
+ test('has no page or cursor parameter to carry', () => {
170
+ const query = stringifyAppSearch(workflowRunSearchParams({search: 'deploy'}, {}));
171
+
172
+ expect(query).not.toContain('page');
173
+ expect(query).not.toContain('cursor');
174
+ });
175
+ });
176
+
177
+ describe('applyWorkflowRunFilterPatch', () => {
178
+ test('replaces only the dimensions the patch names', () => {
179
+ const next = applyWorkflowRunFilterPatch(
180
+ {search: 'deploy', status: ['failed']},
181
+ {
182
+ branch: ['main'],
183
+ },
184
+ );
185
+
186
+ expect(next).toEqual({search: 'deploy', status: ['failed'], branch: ['main']});
187
+ });
188
+
189
+ test('deletes a dimension set to undefined, an empty string, or an empty list', () => {
190
+ const search: WorkflowRunsSearch = {search: 'deploy', status: ['failed'], after: '2026-05-01'};
191
+
192
+ expect(applyWorkflowRunFilterPatch(search, {search: '', status: [], after: undefined})).toEqual(
193
+ {},
194
+ );
195
+ });
196
+
197
+ test('leaves the run selection parameters alone', () => {
198
+ const next = applyWorkflowRunFilterPatch({runAttempt: 2, jobId: 'job-1'}, {search: 'deploy'});
199
+
200
+ expect(next).toEqual({runAttempt: 2, jobId: 'job-1', search: 'deploy'});
201
+ });
202
+ });
203
+
204
+ describe('clearWorkflowRunFilters', () => {
205
+ test('drops every filter and keeps the selection parameters', () => {
206
+ const cleared = clearWorkflowRunFilters({
207
+ search: 'deploy',
208
+ status: ['failed'],
209
+ branch: ['main'],
210
+ actor: ['octocat'],
211
+ event: ['push'],
212
+ after: '2026-05-01',
213
+ before: '2026-05-31',
8
214
  runAttempt: 2,
9
215
  });
216
+
217
+ expect(cleared).toEqual({runAttempt: 2});
218
+ });
219
+ });
220
+
221
+ describe('hasWorkflowRunFilters', () => {
222
+ test.each([
223
+ ['search', {search: 'deploy'}],
224
+ ['status', {status: ['failed' as const]}],
225
+ ['branch', {branch: ['main']}],
226
+ ['actor', {actor: ['octocat']}],
227
+ ['event', {event: ['push']}],
228
+ ['after', {after: '2026-05-01'}],
229
+ ['before', {before: '2026-05-31'}],
230
+ ])('reports %s as an active filter', (_dimension, search) => {
231
+ expect(hasWorkflowRunFilters(search)).toBe(true);
232
+ });
233
+
234
+ test('does not count a run selection parameter as a filter', () => {
235
+ expect(hasWorkflowRunFilters({runAttempt: 2, jobId: 'job-1'})).toBe(false);
10
236
  });
237
+ });
11
238
 
12
- it('requires workspace and project path parameters', () => {
13
- expect(workflowRouteParams({wid: 'workspace-1', pid: 'project-1'})).toEqual({
14
- wid: 'workspace-1',
15
- pid: 'project-1',
239
+ describe('workflowRouteParams', () => {
240
+ test('requires workspace and project path parameters', () => {
241
+ expect(workflowRouteParams({workspaceSlug: 'workspace-1', projectSlug: 'project-1'})).toEqual({
242
+ workspaceSlug: 'workspace-1',
243
+ projectSlug: 'project-1',
16
244
  });
17
- expect(() => workflowRouteParams({wid: 'workspace-1'})).toThrow(
245
+ expect(() => workflowRouteParams({workspaceSlug: 'workspace-1'})).toThrow(
18
246
  'Workflow route is missing required path parameters.',
19
247
  );
20
- expect(() => workflowRouteParams({wid: 'workspace-1', pid: null})).toThrow();
248
+ expect(() => workflowRouteParams({workspaceSlug: 'workspace-1', projectSlug: null})).toThrow();
21
249
  });
22
250
  });
@@ -1,18 +1,77 @@
1
- import type {WorkflowRunListStatusFilter} from '#components/workflow-run-list/types.js';
2
1
  import type {WorkflowRunSelectionInput} from '#core/workflow-run-url-state.js';
3
2
 
3
+ /**
4
+ * Statuses the run list can filter by. Absent means all; there is deliberately no `all`
5
+ * value, so "no filter" and "every filter" are the same URL rather than two.
6
+ */
7
+ export const WORKFLOW_RUN_LIST_STATUSES = ['succeeded', 'failed', 'running', 'cancelled'] as const;
8
+
9
+ export type WorkflowRunListStatus = (typeof WORKFLOW_RUN_LIST_STATUSES)[number];
10
+
11
+ export const WORKFLOW_RUN_TABS = ['summary', 'jobs', 'annotations', 'source'] as const;
12
+
13
+ export type WorkflowRunTab = (typeof WORKFLOW_RUN_TABS)[number];
14
+
15
+ export const WORKFLOW_RUN_ANNOTATION_SEVERITIES = ['error', 'warning', 'info', 'success'] as const;
16
+
17
+ export type WorkflowRunAnnotationSeverity = (typeof WORKFLOW_RUN_ANNOTATION_SEVERITIES)[number];
18
+
4
19
  export interface WorkflowRunsSearch extends WorkflowRunSelectionInput {
5
20
  search?: string;
6
- status?: WorkflowRunListStatusFilter;
21
+ status?: WorkflowRunListStatus[];
22
+ branch?: string[];
23
+ actor?: string[];
24
+ event?: string[];
25
+ /** Inclusive lower bound on the run's local calendar date, `YYYY-MM-DD`. */
26
+ after?: string;
27
+ /** Inclusive upper bound on the run's local calendar date, `YYYY-MM-DD`. */
28
+ before?: string;
29
+ tab?: WorkflowRunTab;
30
+ annotation?: string;
31
+ severity?: WorkflowRunAnnotationSeverity;
7
32
  }
8
33
 
34
+ const STATUS_VALUES = new Set<string>(WORKFLOW_RUN_LIST_STATUSES);
35
+ const TAB_VALUES = new Set<string>(WORKFLOW_RUN_TABS);
36
+ const ANNOTATION_SEVERITY_VALUES = new Set<string>(WORKFLOW_RUN_ANNOTATION_SEVERITIES);
37
+ const CALENDAR_DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
38
+
39
+ /**
40
+ * Reads the run list and run detail query string.
41
+ *
42
+ * Every value is dropped rather than rejected when it does not fit: a truncated or
43
+ * hand-edited URL should degrade to a sensible page, never to an error screen. Unknown
44
+ * parameters are ignored for the same reason.
45
+ */
9
46
  export function validateWorkflowRunsSearch(input: Record<string, unknown>): WorkflowRunsSearch {
10
47
  const search = string(input.search);
11
- const status = input.status === 'failed' || input.status === 'running' ? input.status : 'all';
48
+ const status = repeatable(input.status).filter(isWorkflowRunListStatus);
49
+ const branch = repeatable(input.branch);
50
+ const actor = repeatable(input.actor);
51
+ const event = repeatable(input.event);
52
+ const after = calendarDate(input.after);
53
+ const before = calendarDate(input.before);
12
54
  const runAttempt = positiveInteger(input.runAttempt);
55
+ const tabValue = string(input.tab);
56
+ const tab = tabValue && TAB_VALUES.has(tabValue) ? (tabValue as WorkflowRunTab) : undefined;
57
+ const annotation = string(input.annotation);
58
+ const severityValue = string(input.severity);
59
+ const severity =
60
+ severityValue && ANNOTATION_SEVERITY_VALUES.has(severityValue)
61
+ ? (severityValue as WorkflowRunAnnotationSeverity)
62
+ : undefined;
63
+
13
64
  return {
14
65
  ...(search ? {search} : {}),
15
- ...(status === 'all' ? {} : {status}),
66
+ ...(status.length > 0 ? {status} : {}),
67
+ ...(branch.length > 0 ? {branch} : {}),
68
+ ...(actor.length > 0 ? {actor} : {}),
69
+ ...(event.length > 0 ? {event} : {}),
70
+ ...(after ? {after} : {}),
71
+ ...(before ? {before} : {}),
72
+ ...(tab ? {tab} : {}),
73
+ ...(annotation ? {annotation} : {}),
74
+ ...(severity ? {severity} : {}),
16
75
  ...(string(input.job) ? {jobId: string(input.job)} : {}),
17
76
  ...(string(input.jobExecution) ? {jobExecutionId: string(input.jobExecution)} : {}),
18
77
  ...(string(input.step) ? {stepId: string(input.step)} : {}),
@@ -21,13 +80,27 @@ export function validateWorkflowRunsSearch(input: Record<string, unknown>): Work
21
80
  };
22
81
  }
23
82
 
83
+ /**
84
+ * Writes the query string back.
85
+ *
86
+ * Array values reach the router as arrays and the app's `stringifySearch` expands each into
87
+ * repeated keys; joining them here would defeat that and break on branch names with commas.
88
+ */
24
89
  export function workflowRunSearchParams(
25
90
  search: WorkflowRunsSearch,
26
91
  selection: WorkflowRunSelectionInput = search,
27
92
  ) {
28
93
  return {
29
94
  ...(search.search ? {search: search.search} : {}),
30
- ...(search.status && search.status !== 'all' ? {status: search.status} : {}),
95
+ ...(search.status?.length ? {status: search.status} : {}),
96
+ ...(search.branch?.length ? {branch: search.branch} : {}),
97
+ ...(search.actor?.length ? {actor: search.actor} : {}),
98
+ ...(search.event?.length ? {event: search.event} : {}),
99
+ ...(search.after ? {after: search.after} : {}),
100
+ ...(search.before ? {before: search.before} : {}),
101
+ ...(search.tab && search.tab !== 'summary' ? {tab: search.tab} : {}),
102
+ ...(search.annotation ? {annotation: search.annotation} : {}),
103
+ ...(search.severity ? {severity: search.severity} : {}),
31
104
  ...(selection.jobId ? {job: selection.jobId} : {}),
32
105
  ...(selection.jobExecutionId ? {jobExecution: selection.jobExecutionId} : {}),
33
106
  ...(selection.stepId ? {step: selection.stepId} : {}),
@@ -36,16 +109,147 @@ export function workflowRunSearchParams(
36
109
  };
37
110
  }
38
111
 
112
+ /**
113
+ * Resolves the detail surface for a URL. Explicit tabs win; legacy selection-only URLs belong
114
+ * to Jobs so their selected detail remains visible instead of silently landing on Summary.
115
+ */
116
+ export function workflowRunTab(
117
+ search: Pick<WorkflowRunsSearch, 'tab' | 'jobId' | 'jobExecutionId' | 'stepId' | 'stepAttemptId'>,
118
+ ): WorkflowRunTab {
119
+ if (search.tab) return search.tab;
120
+ if (search.jobId || search.jobExecutionId || search.stepId || search.stepAttemptId) return 'jobs';
121
+ return 'summary';
122
+ }
123
+
124
+ /** Serializes only list filters when leaving a run detail page for the run list. */
125
+ export function workflowRunListSearchParams(search: WorkflowRunsSearch) {
126
+ const {tab: _tab, annotation: _annotation, severity: _severity, ...listSearch} = search;
127
+ return workflowRunSearchParams(listSearch, {});
128
+ }
129
+
130
+ /**
131
+ * A filter change. Each member is explicitly optional-or-undefined so that "present and
132
+ * undefined" (clear this filter) stays distinguishable from "absent" (leave it alone) under
133
+ * `exactOptionalPropertyTypes`.
134
+ */
135
+ export interface WorkflowRunFilterPatch {
136
+ search?: string | undefined;
137
+ status?: WorkflowRunListStatus[] | undefined;
138
+ branch?: string[] | undefined;
139
+ actor?: string[] | undefined;
140
+ event?: string[] | undefined;
141
+ after?: string | undefined;
142
+ before?: string | undefined;
143
+ }
144
+
145
+ /**
146
+ * Applies a filter change.
147
+ *
148
+ * A key present with `undefined`, an empty string, or an empty array clears that filter; a
149
+ * key left out is untouched. Cleared filters are deleted rather than stored empty, so "no
150
+ * filter" is one URL instead of several that render the same list.
151
+ */
152
+ export function applyWorkflowRunFilterPatch(
153
+ search: WorkflowRunsSearch,
154
+ patch: WorkflowRunFilterPatch,
155
+ ): WorkflowRunsSearch {
156
+ const next: WorkflowRunsSearch = {...search};
157
+ if ('search' in patch) setOrDelete(next, 'search', patch.search || undefined);
158
+ if ('status' in patch) setOrDelete(next, 'status', nonEmptyList(patch.status));
159
+ if ('branch' in patch) setOrDelete(next, 'branch', nonEmptyList(patch.branch));
160
+ if ('actor' in patch) setOrDelete(next, 'actor', nonEmptyList(patch.actor));
161
+ if ('event' in patch) setOrDelete(next, 'event', nonEmptyList(patch.event));
162
+ if ('after' in patch) setOrDelete(next, 'after', patch.after || undefined);
163
+ if ('before' in patch) setOrDelete(next, 'before', patch.before || undefined);
164
+ return next;
165
+ }
166
+
167
+ /** Drops every filter while keeping the run-selection parameters the detail route owns. */
168
+ export function clearWorkflowRunFilters(search: WorkflowRunsSearch): WorkflowRunsSearch {
169
+ return applyWorkflowRunFilterPatch(search, {
170
+ search: undefined,
171
+ status: undefined,
172
+ branch: undefined,
173
+ actor: undefined,
174
+ event: undefined,
175
+ after: undefined,
176
+ before: undefined,
177
+ });
178
+ }
179
+
180
+ /** True when any list filter is active, which separates "no matches" from "no runs". */
181
+ export function hasWorkflowRunFilters(search: WorkflowRunsSearch): boolean {
182
+ return Boolean(
183
+ search.search ||
184
+ search.status?.length ||
185
+ search.branch?.length ||
186
+ search.actor?.length ||
187
+ search.event?.length ||
188
+ search.after ||
189
+ search.before,
190
+ );
191
+ }
192
+
39
193
  export function workflowRouteParams(input: Record<string, unknown>): {
40
- wid: string;
41
- pid: string;
194
+ workspaceSlug: string;
195
+ projectSlug: string;
42
196
  workflowRunId?: string;
43
197
  } {
44
- const wid = string(input.wid);
45
- const pid = string(input.pid);
46
- if (!wid || !pid) throw new Error('Workflow route is missing required path parameters.');
198
+ const workspaceSlug = string(input.workspaceSlug);
199
+ const projectSlug = string(input.projectSlug);
200
+ if (!workspaceSlug || !projectSlug)
201
+ throw new Error('Workflow route is missing required path parameters.');
47
202
  const workflowRunId = string(input.workflowRunId);
48
- return workflowRunId ? {wid, pid, workflowRunId} : {wid, pid};
203
+ return workflowRunId ? {workspaceSlug, projectSlug, workflowRunId} : {workspaceSlug, projectSlug};
204
+ }
205
+
206
+ function setOrDelete<TKey extends keyof WorkflowRunsSearch>(
207
+ target: WorkflowRunsSearch,
208
+ key: TKey,
209
+ value: WorkflowRunsSearch[TKey] | undefined,
210
+ ): void {
211
+ if (value === undefined) delete target[key];
212
+ else target[key] = value;
213
+ }
214
+
215
+ function nonEmptyList<TValue>(value: readonly TValue[] | undefined): TValue[] | undefined {
216
+ return value && value.length > 0 ? [...value] : undefined;
217
+ }
218
+
219
+ function isWorkflowRunListStatus(value: string): value is WorkflowRunListStatus {
220
+ return STATUS_VALUES.has(value);
221
+ }
222
+
223
+ /**
224
+ * Normalizes a repeatable parameter into a deduplicated list.
225
+ *
226
+ * A single occurrence parses as a scalar rather than a one-element array, and the parser
227
+ * coerces bare digits to numbers, so `?branch=2024` has to survive as the string `"2024"`.
228
+ */
229
+ function repeatable(value: unknown): string[] {
230
+ const values = Array.isArray(value) ? value : [value];
231
+ const normalized = values.flatMap((entry) => {
232
+ if (typeof entry === 'string') return entry.length > 0 ? [entry] : [];
233
+ if (typeof entry === 'number' && Number.isFinite(entry)) return [String(entry)];
234
+ if (typeof entry === 'boolean') return [String(entry)];
235
+ return [];
236
+ });
237
+ return [...new Set(normalized)];
238
+ }
239
+
240
+ function calendarDate(value: unknown): string | undefined {
241
+ if (typeof value !== 'string' || !CALENDAR_DATE_PATTERN.test(value)) return undefined;
242
+ // Rejects a well-shaped but impossible date such as 2026-02-31, which Date rolls forward.
243
+ // Parsed as UTC: whether `2026-05-01` is a real date does not depend on the reader's zone,
244
+ // and a local-time parse would reject a day the reader's zone skipped outright.
245
+ const [year, month, day] = value.split('-').map(Number) as [number, number, number];
246
+ const parsed = new Date(`${value}T00:00:00.000Z`);
247
+ const isRealDate =
248
+ !Number.isNaN(parsed.getTime()) &&
249
+ parsed.getUTCFullYear() === year &&
250
+ parsed.getUTCMonth() === month - 1 &&
251
+ parsed.getUTCDate() === day;
252
+ return isRealDate ? value : undefined;
49
253
  }
50
254
 
51
255
  function string(value: unknown): string | undefined {
@@ -0,0 +1,25 @@
1
+ import {type Project, useMaybeActiveProjectQuery} from '@shipfox/client-projects';
2
+ import {QueryLoadError} from '@shipfox/client-ui';
3
+ import {EmptyState} from '@shipfox/react-ui/empty-state';
4
+ import {FullPageLoader} from '@shipfox/react-ui/loader';
5
+ import type {ReactNode} from 'react';
6
+
7
+ export function ProjectRoute({children}: {children: (project: Project) => ReactNode}) {
8
+ const projectQuery = useMaybeActiveProjectQuery();
9
+
10
+ if (projectQuery.isPending) return <FullPageLoader />;
11
+ if (projectQuery.isError && projectQuery.data === undefined) {
12
+ return <QueryLoadError query={projectQuery} subject="project" />;
13
+ }
14
+ if (!projectQuery.data) {
15
+ return (
16
+ <EmptyState
17
+ icon="errorWarningLine"
18
+ title="Project not found"
19
+ description="This project doesn't exist, or you don't have access to it."
20
+ />
21
+ );
22
+ }
23
+
24
+ return children(projectQuery.data);
25
+ }
@@ -1,19 +1,26 @@
1
1
  import {defineRoute, useRouteParams, useRouteSearch} from '@shipfox/client-shell/runtime';
2
- import {WorkflowRunPage} from '#pages/workflow-run-page.js';
2
+ import {WorkflowRunDetailPage} from '#pages/workflow-run-detail-page.js';
3
3
  import {validateWorkflowRunsSearch, workflowRouteParams} from './inputs.js';
4
+ import {ProjectRoute} from './project-route.js';
4
5
 
5
6
  export default defineRoute({
6
7
  staticData: {layout: 'full-bleed'},
7
8
  validateSearch: validateWorkflowRunsSearch,
8
9
  component: () => {
9
- const {wid, pid, workflowRunId} = useRouteParams(workflowRouteParams);
10
+ const {workspaceSlug, projectSlug, workflowRunId} = useRouteParams(workflowRouteParams);
11
+ const search = useRouteSearch(validateWorkflowRunsSearch);
10
12
  return (
11
- <WorkflowRunPage
12
- workspaceId={wid}
13
- projectId={pid}
14
- workflowRunId={workflowRunId}
15
- search={useRouteSearch(validateWorkflowRunsSearch)}
16
- />
13
+ <ProjectRoute>
14
+ {(project) => (
15
+ <WorkflowRunDetailPage
16
+ projectId={project.id}
17
+ workspaceSlug={workspaceSlug}
18
+ projectSlug={projectSlug}
19
+ workflowRunId={workflowRunId}
20
+ search={search}
21
+ />
22
+ )}
23
+ </ProjectRoute>
17
24
  );
18
25
  },
19
26
  });
@@ -1,18 +1,25 @@
1
1
  import {defineRoute, useRouteParams, useRouteSearch} from '@shipfox/client-shell/runtime';
2
- import {WorkflowRunPage} from '#pages/workflow-run-page.js';
2
+ import {WorkflowRunsPage} from '#pages/workflow-run-list-page.js';
3
3
  import {validateWorkflowRunsSearch, workflowRouteParams} from './inputs.js';
4
+ import {ProjectRoute} from './project-route.js';
4
5
 
5
6
  export default defineRoute({
6
7
  staticData: {layout: 'full-bleed'},
7
8
  validateSearch: validateWorkflowRunsSearch,
8
9
  component: () => {
9
- const {wid, pid} = useRouteParams(workflowRouteParams);
10
+ const {workspaceSlug, projectSlug} = useRouteParams(workflowRouteParams);
11
+ const search = useRouteSearch(validateWorkflowRunsSearch);
10
12
  return (
11
- <WorkflowRunPage
12
- workspaceId={wid}
13
- projectId={pid}
14
- search={useRouteSearch(validateWorkflowRunsSearch)}
15
- />
13
+ <ProjectRoute>
14
+ {(project) => (
15
+ <WorkflowRunsPage
16
+ projectId={project.id}
17
+ workspaceSlug={workspaceSlug}
18
+ projectSlug={projectSlug}
19
+ search={search}
20
+ />
21
+ )}
22
+ </ProjectRoute>
16
23
  );
17
24
  },
18
25
  });
@@ -1,10 +1,11 @@
1
- import {defineRoute, useRouteParams} from '@shipfox/client-shell/runtime';
1
+ import {defineRoute} from '@shipfox/client-shell/runtime';
2
2
  import {ProjectWorkflowsPage} from '#pages/project-workflows-page.js';
3
- import {workflowRouteParams} from './inputs.js';
3
+ import {ProjectRoute} from './project-route.js';
4
4
 
5
5
  export default defineRoute({
6
6
  component: () => {
7
- const {pid} = useRouteParams(workflowRouteParams);
8
- return <ProjectWorkflowsPage projectId={pid} />;
7
+ return (
8
+ <ProjectRoute>{(project) => <ProjectWorkflowsPage projectId={project.id} />}</ProjectRoute>
9
+ );
9
10
  },
10
11
  });
@@ -0,0 +1,27 @@
1
+ import {describe, expect, it} from '@shipfox/vitest/vi';
2
+ import {withWorkspaceSource} from '../.storybook/workspace-source';
3
+
4
+ describe('client-workflows Storybook Vite configuration', () => {
5
+ it('keeps workspace-source active for client and SSR package resolution', () => {
6
+ const config = withWorkspaceSource({
7
+ resolve: {conditions: ['browser']},
8
+ ssr: {resolve: {conditions: ['node']}},
9
+ });
10
+
11
+ expect(config.resolve?.conditions).toEqual(['browser', 'workspace-source']);
12
+ expect(config.ssr?.resolve?.conditions).toEqual(['node', 'workspace-source']);
13
+ expect(config.plugins?.map((plugin) => plugin && 'name' in plugin && plugin.name)).toContain(
14
+ 'shipfox:workspace-source-resolver',
15
+ );
16
+ });
17
+
18
+ it('does not duplicate an existing workspace-source condition', () => {
19
+ const config = withWorkspaceSource({
20
+ resolve: {conditions: ['browser', 'workspace-source']},
21
+ ssr: {resolve: {conditions: ['node', 'workspace-source']}},
22
+ });
23
+
24
+ expect(config.resolve?.conditions).toEqual(['browser', 'workspace-source']);
25
+ expect(config.ssr?.resolve?.conditions).toEqual(['node', 'workspace-source']);
26
+ });
27
+ });