@morscherlab/mint-sdk 1.4.0-beta.2 → 1.4.0-beta.21

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 (353) hide show
  1. package/README.md +14 -3
  2. package/dist/{ActionMenu-CScINahU.js → AlertBox-DwgNTEqf.js} +40 -209
  3. package/dist/AlertBox-DwgNTEqf.js.map +1 -0
  4. package/dist/{AppSidebar-BmiiPfz9.js → AppSidebar-CZZaxni0.js} +1249 -239
  5. package/dist/AppSidebar-CZZaxni0.js.map +1 -0
  6. package/dist/{ConfirmDialog-CJ7hqYUP.js → ConfirmDialog-CjWoxAD1.js} +2 -2
  7. package/dist/{ConfirmDialog-CJ7hqYUP.js.map → ConfirmDialog-CjWoxAD1.js.map} +1 -1
  8. package/dist/ExperimentPopover-DujiuQIO.js +337 -0
  9. package/dist/ExperimentPopover-DujiuQIO.js.map +1 -0
  10. package/dist/{ExperimentSelectorModal-D-9z7cNx.js → ExperimentSelectorModal-B4j4eUOJ.js} +244 -211
  11. package/dist/ExperimentSelectorModal-B4j4eUOJ.js.map +1 -0
  12. package/dist/{SegmentedControl-DERBN-Xd.js → SegmentedControl-DyUPH3I5.js} +2 -2
  13. package/dist/{SegmentedControl-DERBN-Xd.js.map → SegmentedControl-DyUPH3I5.js.map} +1 -1
  14. package/dist/{SettingsModal-CSLE31yg.js → SettingsModal-D2MOOtuO.js} +28 -27
  15. package/dist/SettingsModal-D2MOOtuO.js.map +1 -0
  16. package/dist/__stories__/experiment-helpers.d.ts +10 -0
  17. package/dist/__tests__/components/AppTopBarOnboarding.test.d.ts +1 -0
  18. package/dist/__tests__/components/FloatingPanelsInModal.test.d.ts +1 -0
  19. package/dist/__tests__/composables/useFloatingPanel.test.d.ts +1 -0
  20. package/dist/__tests__/composables/useOnboarding.test.d.ts +1 -0
  21. package/dist/__tests__/styles/paletteTokens.test.d.ts +1 -0
  22. package/dist/__tests__/styles/v2AuditRules.test.d.ts +1 -0
  23. package/dist/components/AppSidebar.vue.d.ts +1 -1
  24. package/dist/components/AppTopBar.vue.d.ts +20 -5
  25. package/dist/components/ArtifactSaveDialog.vue.d.ts +9 -1
  26. package/dist/components/AuditTrail.vue.d.ts +1 -1
  27. package/dist/components/BaseButton.vue.d.ts +4 -2
  28. package/dist/components/BaseSelect.vue.d.ts +2 -2
  29. package/dist/components/ChartContainer.vue.d.ts +2 -0
  30. package/dist/components/DatePicker.vue.d.ts +1 -1
  31. package/dist/components/ExperimentPopover.vue.d.ts +18 -0
  32. package/dist/components/ExperimentSelectorModal.vue.d.ts +15 -5
  33. package/dist/components/FilePicker.vue.d.ts +1 -1
  34. package/dist/components/FileUploader.vue.d.ts +2 -2
  35. package/dist/components/JobsStatusTray.vue.d.ts +15 -4
  36. package/dist/components/PluginIcon.vue.d.ts +2 -2
  37. package/dist/components/PluginWorkspaceView.props.d.ts +3 -0
  38. package/dist/components/PluginWorkspaceView.vue.d.ts +3 -0
  39. package/dist/components/SampleSelector.vue.d.ts +6 -6
  40. package/dist/components/SequenceProgressBar.vue.d.ts +3 -0
  41. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +3 -3
  42. package/dist/components/StepWizard.vue.d.ts +3 -0
  43. package/dist/components/internal/AppTopBarOnboardingInternal.vue.d.ts +7 -0
  44. package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -1
  45. package/dist/components/internal/AutoGroupModalInternal.vue.d.ts +3 -3
  46. package/dist/components/internal/DatePickerTimeInternal.vue.d.ts +1 -1
  47. package/dist/components/internal/OnboardingLayerInternal.vue.d.ts +7 -0
  48. package/dist/components/internal/ResourcePickerListInternal.vue.d.ts +2 -7
  49. package/dist/components/internal/SelectMultipleInternal.vue.d.ts +4 -0
  50. package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +8 -0
  51. package/dist/composables/createClient.d.ts +2 -0
  52. package/dist/composables/index.d.ts +1 -0
  53. package/dist/composables/pluginIdentity.d.ts +24 -0
  54. package/dist/composables/pluginWorkspaceModel.d.ts +2 -2
  55. package/dist/composables/useAppExperiment.d.ts +8 -0
  56. package/dist/composables/useCalendarGrid.d.ts +1 -1
  57. package/dist/composables/useExperimentSelector.d.ts +6 -2
  58. package/dist/composables/useFileImport.d.ts +7 -0
  59. package/dist/composables/useFloatingPanel.d.ts +29 -0
  60. package/dist/composables/useJobCenter.d.ts +41 -3
  61. package/dist/composables/useOnboarding.d.ts +119 -0
  62. package/dist/composables/usePlatform.d.ts +1 -0
  63. package/dist/{controlSchemaModel-CP5-_nAj.js → controlSchemaModel-Bsg0t5Ab.js} +2 -2
  64. package/dist/{controlSchemaModel-CP5-_nAj.js.map → controlSchemaModel-Bsg0t5Ab.js.map} +1 -1
  65. package/dist/{data-C2PwI_dn.js → data-Boeh4uPI.js} +180 -125
  66. package/dist/data-Boeh4uPI.js.map +1 -0
  67. package/dist/data.d.ts +1 -1
  68. package/dist/data.js +9 -9
  69. package/dist/{experiment-43OJtH7R.js → experiment-DyO54FQB.js} +25 -61
  70. package/dist/experiment-DyO54FQB.js.map +1 -0
  71. package/dist/{experimentStatus-BZmU4czV.js → experimentStatus-DCn2QKG7.js} +3 -3
  72. package/dist/{experimentStatus-BZmU4czV.js.map → experimentStatus-DCn2QKG7.js.map} +1 -1
  73. package/dist/index.js +20 -20
  74. package/dist/{install-DAID6c2-.js → install-BEHp7Mo-.js} +15 -14
  75. package/dist/{install-DAID6c2-.js.map → install-BEHp7Mo-.js.map} +1 -1
  76. package/dist/install.js +1 -1
  77. package/dist/{lab-DLwRl_Ql.js → lab-D61eaX6f.js} +136 -148
  78. package/dist/lab-D61eaX6f.js.map +1 -0
  79. package/dist/lab.js +4 -3
  80. package/dist/{records-BzTcxekG.js → records-D4aQXyZu.js} +647 -389
  81. package/dist/records-D4aQXyZu.js.map +1 -0
  82. package/dist/{runtime-D6gVat6r.js → runtime-q-R0CNLh.js} +417 -54
  83. package/dist/runtime-q-R0CNLh.js.map +1 -0
  84. package/dist/runtime.d.ts +1 -1
  85. package/dist/runtime.js +7 -7
  86. package/dist/{shell-5ju97nOS.js → shell-6CGFvUUw.js} +10 -5
  87. package/dist/{shell-5ju97nOS.js.map → shell-6CGFvUUw.js.map} +1 -1
  88. package/dist/shell.d.ts +1 -0
  89. package/dist/shell.js +6 -6
  90. package/dist/stores/index.d.ts +1 -1
  91. package/dist/stores/index.js +2 -2
  92. package/dist/stores/settings.d.ts +4 -12
  93. package/dist/styles.css +2753 -2101
  94. package/dist/types/componentWorkflowTypes.d.ts +2 -0
  95. package/dist/types/components.d.ts +2 -0
  96. package/dist/types/generated/platformContext.d.ts +1 -0
  97. package/dist/types/generated-ui.d.ts +4 -0
  98. package/dist/types/index.d.ts +1 -1
  99. package/dist/types/jobs.d.ts +9 -0
  100. package/dist/types/platform.d.ts +1 -1
  101. package/dist/{BaseModal-DfRIuVoj.js → useDropdownState-uFIz6Yv8.js} +75 -5
  102. package/dist/useDropdownState-uFIz6Yv8.js.map +1 -0
  103. package/dist/{useExperimentSamples-CxcnxZ8h.js → useExperimentSamples-QEFoq4tf.js} +48 -6
  104. package/dist/useExperimentSamples-QEFoq4tf.js.map +1 -0
  105. package/dist/{useListReorder-CcbEYxX-.js → useListReorder-DsuzwWGS.js} +219 -437
  106. package/dist/useListReorder-DsuzwWGS.js.map +1 -0
  107. package/dist/{useRackEditor-BD5NIo5s.js → useRackEditor-KpZ4iJ6o.js} +85 -81
  108. package/dist/useRackEditor-KpZ4iJ6o.js.map +1 -0
  109. package/dist/useRealtime-B58Lo-Tm.js +508 -0
  110. package/dist/useRealtime-B58Lo-Tm.js.map +1 -0
  111. package/dist/useRequest-BuDmW8Jc.js +215 -0
  112. package/dist/useRequest-BuDmW8Jc.js.map +1 -0
  113. package/dist/utils/time.d.ts +7 -0
  114. package/package.json +3 -2
  115. package/src/__stories__/experiment-helpers.ts +3 -3
  116. package/src/__tests__/components/ActionMenu.test.ts +2 -2
  117. package/src/__tests__/components/AppTopBar.test.ts +257 -3
  118. package/src/__tests__/components/AppTopBarOnboarding.test.ts +160 -0
  119. package/src/__tests__/components/AppTopBarPageSelector.test.ts +125 -9
  120. package/src/__tests__/components/AppTopBarPillNav.test.ts +35 -0
  121. package/src/__tests__/components/ArtifactSaveDialog.test.ts +78 -0
  122. package/src/__tests__/components/BaseButton.test.ts +19 -0
  123. package/src/__tests__/components/BaseModal.test.ts +13 -0
  124. package/src/__tests__/components/BaseSelect.multiple.test.ts +87 -32
  125. package/src/__tests__/components/BaseSelect.test.ts +2 -1
  126. package/src/__tests__/components/BatchProgressList.test.ts +16 -0
  127. package/src/__tests__/components/CalendarGridPanel.test.ts +2 -2
  128. package/src/__tests__/components/ChartContainer.test.ts +11 -0
  129. package/src/__tests__/components/ChemicalFormula.test.ts +6 -3
  130. package/src/__tests__/components/DatePicker.datetime.test.ts +6 -0
  131. package/src/__tests__/components/DatePicker.test.ts +21 -0
  132. package/src/__tests__/components/DatePicker.time.test.ts +2 -2
  133. package/src/__tests__/components/EmptyState.test.ts +1 -1
  134. package/src/__tests__/components/ExperimentPopover.test.ts +125 -16
  135. package/src/__tests__/components/ExperimentSelectorModal.test.ts +153 -14
  136. package/src/__tests__/components/FloatingPanelsInModal.test.ts +94 -0
  137. package/src/__tests__/components/GeneratedPluginView.test.ts +23 -2
  138. package/src/__tests__/components/InstrumentStateBadge.test.ts +4 -4
  139. package/src/__tests__/components/JobsStatusTray.test.ts +373 -71
  140. package/src/__tests__/components/PluginWorkspaceView.test.ts +63 -1
  141. package/src/__tests__/components/ResourcePickerListInternal.test.ts +0 -20
  142. package/src/__tests__/components/SampleSelector.test.ts +33 -8
  143. package/src/__tests__/components/SequenceProgressBar.test.ts +12 -0
  144. package/src/__tests__/components/SettingsModal.test.ts +9 -2
  145. package/src/__tests__/components/StepWizard.test.ts +11 -0
  146. package/src/__tests__/components/WellPlate.decoration.test.ts +11 -4
  147. package/src/__tests__/components/deprecationTags.test.ts +1 -1
  148. package/src/__tests__/composables/useAppExperiment.test.ts +27 -1
  149. package/src/__tests__/composables/useCalendarGrid.test.ts +4 -2
  150. package/src/__tests__/composables/useExperimentSelector.test.ts +111 -0
  151. package/src/__tests__/composables/useFileImport.test.ts +36 -0
  152. package/src/__tests__/composables/useFloatingPanel.test.ts +68 -0
  153. package/src/__tests__/composables/useJobCenter.test.ts +262 -3
  154. package/src/__tests__/composables/useOnboarding.test.ts +317 -0
  155. package/src/__tests__/docs/frontendDocsCatalog.test.ts +4 -3
  156. package/src/__tests__/fixtures/table-import/cells.xlsx +0 -0
  157. package/src/__tests__/fixtures/table-import/samples.xlsx +0 -0
  158. package/src/__tests__/stores/settings.test.ts +21 -21
  159. package/src/__tests__/styles/baseControlsRules.test.ts +6 -2
  160. package/src/__tests__/styles/controlRefreshRules.test.ts +0 -6
  161. package/src/__tests__/styles/cssTokens.test.ts +6 -3
  162. package/src/__tests__/styles/menusShellRefresh.test.ts +22 -12
  163. package/src/__tests__/styles/paletteTokens.test.ts +44 -0
  164. package/src/__tests__/styles/v2AuditRules.test.ts +104 -0
  165. package/src/__tests__/utils/plotlyTemplate.test.ts +4 -4
  166. package/src/__tests__/utils/time.test.ts +18 -1
  167. package/src/components/ActionMenu.vue +6 -37
  168. package/src/components/AdductText.story.vue +7 -0
  169. package/src/components/AdductText.vue +1 -1
  170. package/src/components/AlertBox.story.vue +2 -2
  171. package/src/components/AlertBox.vue +18 -16
  172. package/src/components/AppSidebar.story.vue +0 -28
  173. package/src/components/AppTopBar.onboarding.story.vue +69 -0
  174. package/src/components/AppTopBar.story.vue +105 -15
  175. package/src/components/AppTopBar.vue +248 -122
  176. package/src/components/ArtifactSaveDialog.story.vue +23 -0
  177. package/src/components/ArtifactSaveDialog.vue +102 -12
  178. package/src/components/AuditTrail.story.vue +2 -1
  179. package/src/components/AuditTrail.vue +2 -21
  180. package/src/components/Avatar.vue +10 -14
  181. package/src/components/BaseButton.vue +16 -2
  182. package/src/components/BaseModal.vue +2 -1
  183. package/src/components/BaseSelect.multiple.story.vue +13 -2
  184. package/src/components/BaseSelect.vue +4 -3
  185. package/src/components/BatchProgressList.vue +35 -25
  186. package/src/components/ChartContainer.story.vue +2 -1
  187. package/src/components/ChartContainer.vue +8 -6
  188. package/src/components/ConfirmDialog.story.vue +1 -1
  189. package/src/components/EmptyState.vue +1 -1
  190. package/src/components/ExperimentPopover.story.vue +165 -53
  191. package/src/components/ExperimentPopover.vue +169 -187
  192. package/src/components/ExperimentSelectorModal.story.vue +73 -31
  193. package/src/components/ExperimentSelectorModal.vue +144 -111
  194. package/src/components/FormField.story.vue +8 -48
  195. package/src/components/GeneratedPluginView.vue +11 -10
  196. package/src/components/GeneratedResultRenderer.vue +3 -4
  197. package/src/components/InstrumentStateBadge.vue +1 -1
  198. package/src/components/JobsStatusTray.story.vue +97 -43
  199. package/src/components/JobsStatusTray.vue +287 -290
  200. package/src/components/LayoutResizeHandle.story.vue +65 -4
  201. package/src/components/LoadingSpinner.vue +1 -1
  202. package/src/components/NumberInput.vue +3 -6
  203. package/src/components/PluginIcon.story.vue +2 -10
  204. package/src/components/PluginIcon.vue +2 -2
  205. package/src/components/PluginWorkspaceView.props.ts +3 -0
  206. package/src/components/PluginWorkspaceView.story.vue +1 -1
  207. package/src/components/PluginWorkspaceView.vue +17 -0
  208. package/src/components/RuntimeUpdateNotice.vue +1 -1
  209. package/src/components/SampleSelector.story.vue +22 -7
  210. package/src/components/SampleSelector.vue +34 -37
  211. package/src/components/SampleSelectorGroupNode.vue +1 -1
  212. package/src/components/SectionCard.story.vue +35 -2
  213. package/src/components/SequenceProgressBar.story.vue +9 -0
  214. package/src/components/SequenceProgressBar.vue +6 -1
  215. package/src/components/SettingsModal.schema.ts +1 -1
  216. package/src/components/SettingsModal.vue +9 -14
  217. package/src/components/SmartGroupFieldRecipe.story.vue +1 -1
  218. package/src/components/SmartGroupManual.story.vue +1 -1
  219. package/src/components/StepWizard.story.vue +33 -0
  220. package/src/components/StepWizard.vue +8 -1
  221. package/src/components/Tooltip.vue +9 -32
  222. package/src/components/WellPlate.story.vue +3 -1
  223. package/src/components/WellPlate.vue +7 -0
  224. package/src/components/internal/AppTopBarOnboardingInternal.vue +172 -0
  225. package/src/components/internal/AppTopBarPageSelectorInternal.vue +140 -72
  226. package/src/components/internal/AppTopBarPillNavInternal.vue +33 -2
  227. package/src/components/internal/DatePickerDateInternal.vue +23 -85
  228. package/src/components/internal/DatePickerDateTimeInternal.vue +10 -14
  229. package/src/components/internal/DatePickerTimeInternal.vue +13 -9
  230. package/src/components/internal/NumberUnitInputInternal.vue +4 -18
  231. package/src/components/internal/OnboardingLayerInternal.vue +173 -0
  232. package/src/components/internal/ResourcePickerListInternal.vue +45 -85
  233. package/src/components/internal/SelectMultipleInternal.vue +187 -65
  234. package/src/components/internal/SelectSearchableInternal.vue +8 -4
  235. package/src/components/internal/WellEditPopupInternal.vue +11 -22
  236. package/src/components/internal/WorkspaceShellInternal.vue +12 -0
  237. package/src/composables/createClient.ts +2 -0
  238. package/src/composables/index.ts +1 -0
  239. package/src/composables/pluginIdentity.ts +31 -0
  240. package/src/composables/pluginWorkspaceModel.ts +2 -2
  241. package/src/composables/useAppExperiment.ts +20 -0
  242. package/src/composables/useCalendarGrid.ts +4 -3
  243. package/src/composables/useExperimentSelector.ts +77 -31
  244. package/src/composables/useFileImport.ts +51 -1
  245. package/src/composables/useFloatingPanel.ts +107 -0
  246. package/src/composables/useJobCenter.ts +188 -6
  247. package/src/composables/useOnboarding.ts +465 -0
  248. package/src/composables/usePluginWorkspace.ts +1 -1
  249. package/src/data.ts +1 -0
  250. package/src/runtime.ts +2 -1
  251. package/src/shell.ts +17 -0
  252. package/src/stores/index.ts +0 -2
  253. package/src/stores/settings.ts +20 -50
  254. package/src/styles/components/action-menu.css +1 -1
  255. package/src/styles/components/adduct-text.css +13 -5
  256. package/src/styles/components/alert-box.css +39 -58
  257. package/src/styles/components/app-layout.css +10 -6
  258. package/src/styles/components/app-page-selector.css +121 -44
  259. package/src/styles/components/app-pill-nav.css +11 -12
  260. package/src/styles/components/app-sidebar.css +9 -18
  261. package/src/styles/components/app-top-bar.css +219 -55
  262. package/src/styles/components/artifact-save-dialog.css +153 -2
  263. package/src/styles/components/audit-trail.css +16 -19
  264. package/src/styles/components/avatar.css +6 -4
  265. package/src/styles/components/batch-progress-list.css +38 -36
  266. package/src/styles/components/button.css +36 -59
  267. package/src/styles/components/calendar.css +12 -12
  268. package/src/styles/components/chart-container.css +34 -23
  269. package/src/styles/components/checkbox.css +2 -12
  270. package/src/styles/components/collapsible-card.css +27 -29
  271. package/src/styles/components/confirm-dialog.css +13 -14
  272. package/src/styles/components/dataframe.css +13 -5
  273. package/src/styles/components/date-picker.css +84 -94
  274. package/src/styles/components/datetime-picker.css +35 -47
  275. package/src/styles/components/divider.css +20 -20
  276. package/src/styles/components/dose-calculator.css +3 -3
  277. package/src/styles/components/empty-state.css +20 -17
  278. package/src/styles/components/experiment-code-badge.css +2 -0
  279. package/src/styles/components/experiment-popover.css +174 -329
  280. package/src/styles/components/experiment-selector-modal.css +88 -56
  281. package/src/styles/components/file-picker.css +3 -3
  282. package/src/styles/components/file-uploader.css +38 -49
  283. package/src/styles/components/form-builder.css +3 -3
  284. package/src/styles/components/formula-input.css +1 -1
  285. package/src/styles/components/icon-button.css +18 -24
  286. package/src/styles/components/input.css +5 -4
  287. package/src/styles/components/instrument-monitor.css +13 -15
  288. package/src/styles/components/jobs-status-tray.css +140 -296
  289. package/src/styles/components/layout-resize-handle.css +59 -16
  290. package/src/styles/components/modal.css +5 -7
  291. package/src/styles/components/molecule-input.css +2 -2
  292. package/src/styles/components/multi-select.css +65 -117
  293. package/src/styles/components/number-input.css +26 -19
  294. package/src/styles/components/onboarding.css +368 -0
  295. package/src/styles/components/pill.css +3 -2
  296. package/src/styles/components/plate-editor.css +0 -1
  297. package/src/styles/components/plugin-icon.css +26 -23
  298. package/src/styles/components/popover.css +2 -2
  299. package/src/styles/components/presence-avatars.css +1 -1
  300. package/src/styles/components/progress-bar.css +3 -3
  301. package/src/styles/components/protocol-step-editor.css +1 -3
  302. package/src/styles/components/resource-card.css +2 -2
  303. package/src/styles/components/resource-picker-list.css +6 -57
  304. package/src/styles/components/sample-hierarchy-tree.css +4 -4
  305. package/src/styles/components/sample-selector.css +138 -79
  306. package/src/styles/components/schedule-calendar.css +8 -8
  307. package/src/styles/components/searchable-select.css +15 -19
  308. package/src/styles/components/section-card.css +54 -49
  309. package/src/styles/components/segmented-control.css +18 -15
  310. package/src/styles/components/select.css +3 -3
  311. package/src/styles/components/sequence-input.css +2 -2
  312. package/src/styles/components/sequence-progress-bar.css +17 -8
  313. package/src/styles/components/settings-modal.css +7 -2
  314. package/src/styles/components/smart-group.css +18 -18
  315. package/src/styles/components/step-wizard.css +52 -16
  316. package/src/styles/components/tabs.css +2 -2
  317. package/src/styles/components/textarea.css +3 -3
  318. package/src/styles/components/time-picker.css +37 -25
  319. package/src/styles/components/toast.css +13 -11
  320. package/src/styles/components/toggle.css +1 -11
  321. package/src/styles/components/tooltip.css +12 -11
  322. package/src/styles/components/unit-input.css +9 -8
  323. package/src/styles/components/well-edit-popup.css +2 -2
  324. package/src/styles/components/well-plate.css +9 -2
  325. package/src/styles/index.css +1 -0
  326. package/src/styles/variables.css +247 -78
  327. package/src/types/componentWorkflowTypes.ts +2 -0
  328. package/src/types/components.ts +2 -0
  329. package/src/types/generated/platformContext.ts +1 -0
  330. package/src/types/generated-ui.ts +4 -0
  331. package/src/types/index.ts +1 -0
  332. package/src/types/jobs.ts +10 -0
  333. package/src/types/platform.ts +1 -1
  334. package/src/utils/time.ts +21 -0
  335. package/dist/ActionMenu-CScINahU.js.map +0 -1
  336. package/dist/AppSidebar-BmiiPfz9.js.map +0 -1
  337. package/dist/BaseModal-DfRIuVoj.js.map +0 -1
  338. package/dist/ExperimentPopover-C4sGw-is.js +0 -359
  339. package/dist/ExperimentPopover-C4sGw-is.js.map +0 -1
  340. package/dist/ExperimentSelectorModal-D-9z7cNx.js.map +0 -1
  341. package/dist/SettingsModal-CSLE31yg.js.map +0 -1
  342. package/dist/data-C2PwI_dn.js.map +0 -1
  343. package/dist/experiment-43OJtH7R.js.map +0 -1
  344. package/dist/lab-DLwRl_Ql.js.map +0 -1
  345. package/dist/records-BzTcxekG.js.map +0 -1
  346. package/dist/runtime-D6gVat6r.js.map +0 -1
  347. package/dist/useExperimentSamples-CxcnxZ8h.js.map +0 -1
  348. package/dist/useListReorder-CcbEYxX-.js.map +0 -1
  349. package/dist/useRackEditor-BD5NIo5s.js.map +0 -1
  350. package/dist/useRealtime-PalKue84.js +0 -696
  351. package/dist/useRealtime-PalKue84.js.map +0 -1
  352. package/dist/useRequest-D5JDfsgZ.js +0 -76
  353. package/dist/useRequest-D5JDfsgZ.js.map +0 -1
@@ -1,9 +1,15 @@
1
- import { mount } from '@vue/test-utils'
1
+ import { flushPromises, mount } from '@vue/test-utils'
2
2
  import { describe, expect, it, vi } from 'vitest'
3
- import { h } from 'vue'
3
+ import { defineComponent, h, nextTick, shallowRef } from 'vue'
4
4
 
5
+ import BaseModal from '../../components/BaseModal.vue'
5
6
  import JobsStatusTray from '../../components/JobsStatusTray.vue'
6
- import type { PluginJobCenterSource } from '../../composables/useJobCenter'
7
+ import {
8
+ useJobCenter,
9
+ type PluginJobCenterSource,
10
+ type UseJobCenterOptions,
11
+ type UseJobCenterReturn,
12
+ } from '../../composables/useJobCenter'
7
13
  import type { JobState, JobStateInput, JobStatus } from '../../types/jobs'
8
14
 
9
15
  function job(status: JobStatus, id: string = status): JobStateInput {
@@ -43,33 +49,43 @@ function job(status: JobStatus, id: string = status): JobStateInput {
43
49
  async function openTray(options: {
44
50
  source: PluginJobCenterSource
45
51
  slots?: Record<string, (props: { job: JobState }) => unknown>
52
+ confirm?: InstanceType<typeof JobsStatusTray>['$props']['confirm']
53
+ rowJob?: UseJobCenterOptions['rowJob']
54
+ /** Run real transitions, so a leaving dialog stays in the DOM for a frame. */
55
+ transitions?: boolean
46
56
  }) {
47
- const { slots, ...props } = options
48
- const wrapper = mount(JobsStatusTray, {
49
- attachTo: document.body,
50
- props: {
51
- ...props,
52
- loadOnMount: false,
53
- teleportTo: false,
57
+ const Host = defineComponent({
58
+ setup() {
59
+ const center = useJobCenter({ source: options.source, loadOnMount: false, rowJob: options.rowJob })
60
+ return () => [
61
+ h('button', { class: 'opener', onClick: center.toggle }, 'Jobs'),
62
+ h(JobsStatusTray, { center, teleportTo: false, confirm: options.confirm }, options.slots),
63
+ ]
54
64
  },
55
- slots,
56
65
  })
57
- await wrapper.get('.mint-jobs-tray__trigger').trigger('click')
66
+ const wrapper = mount(Host, {
67
+ attachTo: document.body,
68
+ global: options.transitions ? { stubs: { transition: false } } : {},
69
+ })
70
+ const opener = wrapper.get('.opener')
71
+ ;(opener.element as HTMLElement).focus()
72
+ await opener.trigger('click')
58
73
  return wrapper
59
74
  }
60
75
 
61
76
  describe('JobsStatusTray', () => {
62
- it('shows awaiting input as active without default actions', async () => {
77
+ it('should show awaiting input as active without default actions when the job has no capability', async () => {
63
78
  const wrapper = await openTray({ source: { jobs: [job('awaiting_input')] } })
64
79
 
65
80
  expect([
66
- wrapper.text().includes('Waiting for input'),
67
- wrapper.find('[aria-label="Cancel job"]').exists(),
68
- wrapper.find('[aria-label="Delete job"]').exists(),
69
- ]).toEqual([true, false, false])
81
+ wrapper.find('[data-section="active"]').exists(),
82
+ wrapper.find('[role="img"][aria-label="Waiting for input"]').exists(),
83
+ wrapper.find('[aria-label="Cancel Job awaiting_input"]').exists(),
84
+ wrapper.find('[aria-label="Delete Job awaiting_input"]').exists(),
85
+ ]).toEqual([true, true, false, false])
70
86
  })
71
87
 
72
- it('shows only actions supported by both capability and source', async () => {
88
+ it('should show only actions supported by both capability and source when rows render', async () => {
73
89
  const wrapper = await openTray({
74
90
  source: {
75
91
  jobs: [
@@ -82,24 +98,9 @@ describe('JobsStatusTray', () => {
82
98
  })
83
99
 
84
100
  expect([
85
- wrapper.find('[aria-label="Cancel job"]').exists(),
86
- wrapper.find('[aria-label="Delete job"]').exists(),
87
- ]).toEqual([true, false])
88
-
89
- await wrapper.get('[role="tab"][data-tab="history"]').trigger('click')
90
- expect(wrapper.find('[aria-label="Delete job"]').exists()).toBe(true)
91
- })
92
-
93
- it('exposes dialog state and closes on Escape', async () => {
94
- const wrapper = await openTray({ source: { jobs: [job('running')] } })
95
- const trigger = wrapper.get('.mint-jobs-tray__trigger')
96
-
97
- await wrapper.get('[role="dialog"]').trigger('keydown', { key: 'Escape' })
98
-
99
- expect([trigger.attributes('aria-expanded'), wrapper.find('[role="dialog"]').exists()]).toEqual([
100
- 'false',
101
- false,
102
- ])
101
+ wrapper.find('[data-status="awaiting_input"] [aria-label="Cancel Job input"]').exists(),
102
+ wrapper.find('[data-status="completed"] [aria-label="Delete Job done"]').exists(),
103
+ ]).toEqual([true, true])
103
104
  })
104
105
 
105
106
  it('renders an individual delete action for an active capable job', async () => {
@@ -110,83 +111,384 @@ describe('JobsStatusTray', () => {
110
111
  },
111
112
  })
112
113
 
113
- expect(wrapper.find('[aria-label="Delete job"]').exists()).toBe(true)
114
+ expect(wrapper.find('[aria-label="Delete Job awaiting_input"]').exists()).toBe(true)
115
+ })
116
+
117
+ it('should render a non-modal panel that does not trap Tab when open', async () => {
118
+ const wrapper = await openTray({ source: { jobs: [job('running')] } })
119
+ const panel = wrapper.get('.mint-jobs-tray__panel')
120
+ const tab = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, cancelable: true })
121
+ panel.element.dispatchEvent(tab)
122
+
123
+ expect([
124
+ panel.attributes('role'),
125
+ panel.attributes('aria-modal'),
126
+ tab.defaultPrevented,
127
+ ]).toEqual([undefined, undefined, false])
114
128
  })
115
129
 
116
- it('traps forward Tab navigation inside the modal dialog', async () => {
130
+ it('should close and return focus to the opener when Escape is pressed', async () => {
117
131
  const wrapper = await openTray({ source: { jobs: [job('running')] } })
118
- const focusable = wrapper.findAll('button')
119
- const firstInsidePanel = wrapper.get('[aria-label="Close jobs"]')
120
- const lastInsidePanel = focusable[focusable.length - 1]!
121
- lastInsidePanel.element.focus()
132
+ expect(document.activeElement).toBe(wrapper.get('.mint-jobs-tray__panel').element)
122
133
 
123
- await wrapper.get('[role="dialog"]').trigger('keydown', { key: 'Tab' })
134
+ await wrapper.get('.mint-jobs-tray__panel').trigger('keydown', { key: 'Escape' })
124
135
 
125
- expect(document.activeElement).toBe(firstInsidePanel.element)
136
+ expect([
137
+ wrapper.find('.mint-jobs-tray__panel').exists(),
138
+ document.activeElement,
139
+ ]).toEqual([false, wrapper.get('.opener').element])
126
140
  })
127
141
 
128
- it('traps reverse Tab navigation from the dialog container', async () => {
142
+ it('should close when the page outside the panel is clicked', async () => {
129
143
  const wrapper = await openTray({ source: { jobs: [job('running')] } })
130
- const panel = wrapper.get('[role="dialog"]')
131
- const focusable = panel.findAll('button')
132
- const panelElement = panel.element as HTMLElement
133
- panelElement.focus()
134
144
 
135
- await panel.trigger('keydown', { key: 'Tab', shiftKey: true })
145
+ await wrapper.get('.mint-jobs-tray__scrim').trigger('click')
136
146
 
137
- expect(document.activeElement).toBe(focusable[focusable.length - 1]!.element)
147
+ expect(wrapper.find('.mint-jobs-tray__panel').exists()).toBe(false)
138
148
  })
139
149
 
140
- it('exposes job selection through a keyboard-accessible button', async () => {
150
+ it('should emit select from a keyboard-accessible title button when a row is chosen', async () => {
141
151
  const wrapper = await openTray({ source: { jobs: [job('running')] } })
142
152
 
143
- await wrapper.get('.mint-jobs-tray__job-select').trigger('click')
153
+ await wrapper.get('.mint-jobs-tray__row-title').trigger('click')
144
154
 
145
- expect(wrapper.emitted('select')?.[0]?.[0]).toMatchObject({ job_id: 'running' })
155
+ expect(wrapper.findComponent(JobsStatusTray).emitted('select')?.[0]?.[0]).toMatchObject({ job_id: 'running' })
146
156
  })
147
157
 
148
- it('renders the job-actions slot once per row in both tabs with the row job', async () => {
158
+ it('should render each row slot once per row with the row job when slots are given', async () => {
149
159
  const wrapper = await openTray({
150
160
  source: { jobs: [job('running', 'R-1'), job('completed', 'C-1')] },
151
161
  slots: {
152
- 'job-actions': ({ job: row }) =>
153
- h('button', { 'data-testid': 'open', 'data-job': row.job_id }, 'Open'),
162
+ 'job-actions': ({ job: row }) => h('button', { 'data-testid': 'open', 'data-job': row.job_id }, 'Open'),
163
+ 'job-title': ({ job: row }) => h('span', { 'data-testid': 'title' }, `Title ${row.job_id}`),
164
+ 'job-detail': ({ job: row }) => h('span', { 'data-testid': 'detail' }, `Detail ${row.job_id}`),
154
165
  },
155
166
  })
156
167
 
157
- const active = wrapper.findAll('[data-testid="open"]')
158
- expect(active.map((b) => b.attributes('data-job'))).toEqual(['R-1'])
159
-
160
- await wrapper.get('[role="tab"][data-tab="history"]').trigger('click')
161
-
162
- const history = wrapper.findAll('[data-testid="open"]')
163
- expect(history.map((b) => b.attributes('data-job'))).toEqual(['C-1'])
168
+ expect(['open', 'title', 'detail'].map(id => wrapper.findAll(`[data-testid="${id}"]`).map(el => el.attributes('data-job') ?? el.text())))
169
+ .toEqual([['R-1', 'C-1'], ['Title R-1', 'Title C-1'], ['Detail R-1', 'Detail C-1']])
164
170
  })
165
171
 
166
172
  it('should pin awaiting-input jobs to the top when they arrive after other active jobs', async () => {
167
173
  const wrapper = await openTray({
168
174
  source: { jobs: [job('running', 'R-1'), job('queued', 'Q-1'), job('awaiting_input', 'A-1')] },
169
175
  })
170
- expect(wrapper.findAll('.mint-jobs-tray__job-select').map((b) => b.text()))
176
+ expect(wrapper.findAll('.mint-jobs-tray__row-title').map((b) => b.text()))
171
177
  .toEqual(['Job A-1', 'Job R-1', 'Job Q-1'])
172
178
  })
173
179
 
174
180
  it('should tint the awaiting-input row when a job waits for input', async () => {
175
181
  const wrapper = await openTray({ source: { jobs: [job('awaiting_input')] } })
176
- expect(wrapper.get('.mint-jobs-tray__job').classes()).toContain('mint-jobs-tray__job--awaiting-input')
182
+ expect(wrapper.get('.mint-jobs-tray__row').classes()).toContain('mint-jobs-tray__row--awaiting-input')
177
183
  })
178
184
 
179
- it('should show a mono percent next to the status when a job is running', async () => {
185
+ it('should show a mono percent as the row value when a job is running', async () => {
180
186
  const running = job('running')
181
187
  running.progress = { ...running.progress, percent: 62 }
182
188
  const wrapper = await openTray({ source: { jobs: [running] } })
183
- expect(wrapper.get('.mint-jobs-tray__percent').text()).toBe('62%')
189
+ expect([wrapper.get('.mint-jobs-tray__value').text(), wrapper.get('[role="progressbar"]').attributes('aria-label')])
190
+ .toEqual(['62%', 'Job running progress'])
191
+ })
192
+
193
+ it('should show the error in error ink when a job failed', async () => {
194
+ const wrapper = await openTray({ source: { jobs: [{ ...job('failed'), error: 'Peaks not found' }] } })
195
+ const detail = wrapper.get('.mint-jobs-tray__detail')
196
+ expect([detail.text(), detail.classes()]).toEqual(['Peaks not found', expect.arrayContaining(['mint-jobs-tray__detail--error'])])
197
+ })
198
+
199
+ it.each([
200
+ [[job('running', 'R-1'), job('queued', 'Q-1'), job('completed', 'C-1')], '2 active · 1 done', ['Active · 2', 'Finished · 1']],
201
+ [[job('failed', 'F-1'), job('cancelled', 'X-1')], '1 failed', ['Finished · 2']],
202
+ [[], 'No jobs', []],
203
+ ])('should count jobs in the header and sections for %#', async (jobs, count, sections) => {
204
+ const wrapper = await openTray({ source: { jobs } })
205
+ expect({
206
+ count: wrapper.get('.mint-jobs-tray__head-count').text(),
207
+ sections: wrapper.findAll('.mint-jobs-tray__section').map(el => el.text().replace(/\s+/g, ' ')),
208
+ empty: wrapper.find('.mint-jobs-tray__empty').exists(),
209
+ }).toEqual({ count, sections, empty: jobs.length === 0 })
210
+ })
211
+
212
+ it('should clear finished jobs from the Finished section when the source can clear', async () => {
213
+ const clear = vi.fn(async () => 1)
214
+ const wrapper = await openTray({ source: { jobs: [job('running', 'R-1'), job('completed', 'C-1')], clear } })
215
+
216
+ await wrapper.get('[data-section="finished"] .mint-jobs-tray__section-action').trigger('click')
217
+
218
+ expect([wrapper.find('[data-section="active"] .mint-jobs-tray__section-action').exists(), clear.mock.calls.length])
219
+ .toEqual([false, 1])
220
+ })
221
+
222
+ describe('focus and Escape', () => {
223
+ function mountWithChip() {
224
+ let center!: UseJobCenterReturn
225
+ const Host = defineComponent({
226
+ setup() {
227
+ center = useJobCenter({ source: { jobs: [job('running')] }, loadOnMount: false })
228
+ return () => [
229
+ h('button', { class: 'mint-topbar__job' }, 'Jobs'),
230
+ h('button', { class: 'outside' }, 'Elsewhere'),
231
+ h(JobsStatusTray, { center, teleportTo: false }),
232
+ ]
233
+ },
234
+ })
235
+ return { wrapper: mount(Host, { attachTo: document.body }), center: () => center }
236
+ }
237
+
238
+ it.each([
239
+ ['the page body', () => (document.activeElement as HTMLElement | null)?.blur()],
240
+ ['a detached element', () => {
241
+ const gone = document.body.appendChild(document.createElement('button'))
242
+ gone.focus()
243
+ return () => gone.remove()
244
+ }],
245
+ ])('should focus the top-bar jobs control on Escape when the opener was %s', async (_case, focusOpener) => {
246
+ const { wrapper, center } = mountWithChip()
247
+ const detach = focusOpener()
248
+ center().open()
249
+ await nextTick()
250
+ detach?.()
251
+
252
+ await wrapper.get('.mint-jobs-tray__panel').trigger('keydown', { key: 'Escape' })
253
+
254
+ expect(document.activeElement).toBe(wrapper.get('.mint-topbar__job').element)
255
+ wrapper.unmount()
256
+ })
257
+
258
+ it('should close on Escape when focus has left the open panel', async () => {
259
+ const { wrapper, center } = mountWithChip()
260
+ center().open()
261
+ await nextTick()
262
+ ;(wrapper.get('.outside').element as HTMLElement).focus()
263
+
264
+ document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
265
+ await nextTick()
266
+
267
+ expect(wrapper.find('.mint-jobs-tray__panel').exists()).toBe(false)
268
+ wrapper.unmount()
269
+ })
184
270
  })
185
271
 
186
- it('should summarize active jobs by state in the header when jobs are active', async () => {
272
+ it('should draw a row from the center row view when a rowJob option overrides it', async () => {
187
273
  const wrapper = await openTray({
188
- source: { jobs: [job('running', 'R-1'), job('queued', 'Q-1'), job('awaiting_input', 'A-1')] },
274
+ source: { jobs: [{ ...job('awaiting_input', 'A-1'), can_cancel: true }], cancel: vi.fn(async () => undefined) },
275
+ rowJob: row => (row.status === 'awaiting_input' ? { status: 'running', percent: 40, message: 'Decoding in the browser' } : null),
276
+ })
277
+ const row = wrapper.get('.mint-jobs-tray__row')
278
+
279
+ expect({
280
+ section: wrapper.get('[data-section="active"]').text().replace(/\s+/g, ' '),
281
+ status: row.attributes('data-status'),
282
+ tinted: row.classes('mint-jobs-tray__row--awaiting-input'),
283
+ icon: row.get('[role="img"]').attributes('aria-label'),
284
+ value: row.get('.mint-jobs-tray__value').text(),
285
+ progress: row.get('[role="progressbar"]').attributes('aria-valuenow'),
286
+ detail: row.get('.mint-jobs-tray__detail').text(),
287
+ cancel: row.find('[aria-label="Cancel Job A-1"]').exists(),
288
+ }).toEqual({
289
+ section: 'Active · 1',
290
+ status: 'awaiting_input',
291
+ tinted: false,
292
+ icon: 'Running',
293
+ value: '40%',
294
+ progress: '40',
295
+ detail: 'Decoding in the browser',
296
+ cancel: true,
297
+ })
298
+ })
299
+
300
+ it('should mount no confirm dialog when confirm is off', async () => {
301
+ const wrapper = await openTray({ source: { jobs: [job('completed')], delete: vi.fn(async () => true) } })
302
+ expect(wrapper.findComponent(BaseModal).exists()).toBe(false)
303
+ wrapper.unmount()
304
+ })
305
+
306
+ it('should keep the tray open when an inner handler prevents the Escape default', async () => {
307
+ const wrapper = await openTray({
308
+ source: { jobs: [job('running', 'R-1')] },
309
+ slots: {
310
+ 'job-actions': () => h('input', {
311
+ class: 'inner',
312
+ onKeydown: (event: KeyboardEvent) => { if (event.key === 'Escape') event.preventDefault() },
313
+ }),
314
+ },
315
+ })
316
+ const inner = wrapper.get('.inner').element as HTMLElement
317
+ inner.focus()
318
+
319
+ inner.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true, cancelable: true }))
320
+ await nextTick()
321
+
322
+ expect(wrapper.find('.mint-jobs-tray__panel').exists()).toBe(true)
323
+ wrapper.unmount()
324
+ })
325
+
326
+ it('should leave focus on the page when a job update arrives while nothing in the tray is focused', async () => {
327
+ const jobs = shallowRef([job('running', 'R-1')])
328
+ const wrapper = await openTray({ source: { jobs } })
329
+ ;(document.activeElement as HTMLElement).blur()
330
+
331
+ jobs.value = [job('completed', 'R-1')]
332
+ await nextTick()
333
+ await nextTick()
334
+
335
+ expect(document.activeElement).toBe(document.body)
336
+ wrapper.unmount()
337
+ })
338
+
339
+ it('should focus the panel when the focused row leaves the list while the tray is open', async () => {
340
+ const jobs = shallowRef([job('completed', 'C-1'), job('completed', 'C-2')])
341
+ const wrapper = await openTray({ source: { jobs, delete: vi.fn(async () => true) } })
342
+ ;(wrapper.get('[aria-label="Delete Job C-1"]').element as HTMLElement).focus()
343
+
344
+ jobs.value = [job('completed', 'C-2')]
345
+ await nextTick()
346
+ await nextTick()
347
+
348
+ expect(document.activeElement).toBe(wrapper.get('.mint-jobs-tray__panel').element)
349
+ wrapper.unmount()
350
+ })
351
+
352
+ describe('confirmation', () => {
353
+ const dialog = () => document.body.querySelector<HTMLElement>('.mint-modal [role="dialog"], .mint-modal__container')
354
+
355
+ function confirmSource() {
356
+ return {
357
+ jobs: [{ ...job('running', 'R-1'), can_cancel: true }, job('completed', 'C-1'), job('failed', 'F-1')],
358
+ cancel: vi.fn(async () => undefined),
359
+ delete: vi.fn(async () => true),
360
+ clear: vi.fn(async () => 2),
361
+ }
362
+ }
363
+ const triggers = {
364
+ cancel: '[aria-label="Cancel Job R-1"]',
365
+ delete: '[aria-label="Delete Job C-1"]',
366
+ clear: '[data-section="finished"] .mint-jobs-tray__section-action',
367
+ } as const
368
+
369
+ it.each([
370
+ ['cancel', true, 'Job R-1'],
371
+ ['delete', { delete: true }, 'Job C-1'],
372
+ ['clear', { clear: true }, '2 finished jobs'],
373
+ ] as const)('should run %s only after the confirm dialog is confirmed when confirm is %o', async (kind, confirm, named) => {
374
+ const source = confirmSource()
375
+ const wrapper = await openTray({ source, confirm })
376
+
377
+ await wrapper.get(triggers[kind]).trigger('click')
378
+ await nextTick()
379
+ const shown = dialog()
380
+ const before = source[kind].mock.calls.length
381
+ const confirmButton = [...shown!.querySelectorAll('button')].at(-1)!
382
+ confirmButton.click()
383
+ await flushPromises()
384
+
385
+ expect([shown?.textContent?.includes(named), before, source[kind].mock.calls.length]).toEqual([true, 0, 1])
386
+ wrapper.unmount()
387
+ })
388
+
389
+ it.each([
390
+ ['off', undefined],
391
+ ['set for another action', { delete: true }],
392
+ ] as const)('should cancel at once without a dialog when confirm is %s', async (_case, confirm) => {
393
+ const source = confirmSource()
394
+ const wrapper = await openTray({ source, confirm })
395
+
396
+ await wrapper.get(triggers.cancel).trigger('click')
397
+ await flushPromises()
398
+
399
+ expect([dialog(), source.cancel.mock.calls.length]).toEqual([null, 1])
400
+ wrapper.unmount()
401
+ })
402
+
403
+ it('should run the action once when Confirm is clicked twice while the dialog fades out', async () => {
404
+ const source = confirmSource()
405
+ const wrapper = await openTray({ source, confirm: true, transitions: true })
406
+ await wrapper.get(triggers.clear).trigger('click')
407
+ await nextTick()
408
+ const confirmButton = [...dialog()!.querySelectorAll('button')].at(-1)!
409
+
410
+ confirmButton.click()
411
+ confirmButton.click()
412
+ await flushPromises()
413
+
414
+ expect([confirmButton.isConnected, source.clear.mock.calls.length]).toEqual([true, 1])
415
+ await vi.waitFor(() => expect(dialog()).toBeNull())
416
+ wrapper.unmount()
417
+ })
418
+
419
+ it('should close only the dialog when Escape is pressed in it', async () => {
420
+ const source = confirmSource()
421
+ const wrapper = await openTray({ source, confirm: true })
422
+ await wrapper.get(triggers.delete).trigger('click')
423
+ await nextTick()
424
+
425
+ dialog()!.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))
426
+ await flushPromises()
427
+
428
+ expect([dialog(), wrapper.find('.mint-jobs-tray__panel').exists(), source.delete.mock.calls.length])
429
+ .toEqual([null, true, 0])
430
+ wrapper.unmount()
431
+ })
432
+
433
+ it('should ask again with the new count when the finished jobs change before Confirm', async () => {
434
+ const jobs = shallowRef([job('completed', 'C-1'), job('failed', 'F-1')])
435
+ const clear = vi.fn(async () => 3)
436
+ const wrapper = await openTray({ source: { jobs, clear }, confirm: true })
437
+ await wrapper.get(triggers.clear).trigger('click')
438
+ await nextTick()
439
+
440
+ jobs.value = [...jobs.value, job('cancelled', 'X-1')]
441
+ await nextTick()
442
+ ;[...dialog()!.querySelectorAll('button')].at(-1)!.click()
443
+ await flushPromises()
444
+ const asked = [dialog()?.textContent?.includes('3 finished jobs'), clear.mock.calls.length]
445
+ ;[...dialog()!.querySelectorAll('button')].at(-1)!.click()
446
+ await flushPromises()
447
+
448
+ expect([...asked, dialog(), clear.mock.calls.length]).toEqual([true, 0, null, 1])
449
+ wrapper.unmount()
450
+ })
451
+
452
+ it.each([
453
+ ['delete', 'the job is gone', [] as JobStateInput[]],
454
+ ['cancel', 'the job finished', [{ ...job('completed', 'R-1'), can_cancel: true }]],
455
+ ['delete', 'the job is no longer deletable', [{ ...job('completed', 'C-1'), can_delete: false }]],
456
+ ] as const)('should close the %s dialog without acting when %s before Confirm', async (kind, _case, next) => {
457
+ const source = { ...confirmSource(), jobs: shallowRef<JobStateInput[]>(confirmSource().jobs) }
458
+ const wrapper = await openTray({ source, confirm: true })
459
+ await wrapper.get(triggers[kind]).trigger('click')
460
+ await nextTick()
461
+
462
+ source.jobs.value = [...next]
463
+ await nextTick()
464
+ ;[...dialog()!.querySelectorAll('button')].at(-1)!.click()
465
+ await flushPromises()
466
+
467
+ expect([dialog(), source[kind].mock.calls.length]).toEqual([null, 0])
468
+ wrapper.unmount()
469
+ })
470
+
471
+ it('should drop the pending action when the job source changes while the dialog is open', async () => {
472
+ const first = confirmSource()
473
+ const second = confirmSource()
474
+ const current = shallowRef<PluginJobCenterSource>(first)
475
+ const Host = defineComponent({
476
+ setup() {
477
+ const center = useJobCenter({ source: current, loadOnMount: false })
478
+ center.open()
479
+ return () => h(JobsStatusTray, { center, teleportTo: false, confirm: true })
480
+ },
481
+ })
482
+ const wrapper = mount(Host, { attachTo: document.body })
483
+ await nextTick()
484
+ await wrapper.get(triggers.delete).trigger('click')
485
+ await nextTick()
486
+
487
+ current.value = second
488
+ await nextTick()
489
+
490
+ expect([dialog(), first.delete.mock.calls.length, second.delete.mock.calls.length]).toEqual([null, 0, 0])
491
+ wrapper.unmount()
189
492
  })
190
- expect(wrapper.get('.mint-jobs-tray__subtitle').text()).toBe('1 running · 1 queued · 1 waiting')
191
493
  })
192
494
  })
@@ -1,7 +1,7 @@
1
1
  import { flushPromises, mount } from '@vue/test-utils'
2
2
  import type { VueWrapper } from '@vue/test-utils'
3
3
  import { createPinia, setActivePinia } from 'pinia'
4
- import { computed, h, nextTick, ref } from 'vue'
4
+ import { computed, h, nextTick, ref, shallowRef } from 'vue'
5
5
  import { describe, expect, it, vi } from 'vitest'
6
6
  import AppLayout from '../../components/AppLayout.vue'
7
7
  import AppSidebar from '../../components/AppSidebar.vue'
@@ -10,6 +10,7 @@ import ExperimentPopover from '../../components/ExperimentPopover.vue'
10
10
  import ExperimentSelectorModal from '../../components/ExperimentSelectorModal.vue'
11
11
  import PluginWorkspaceView from '../../components/PluginWorkspaceView.vue'
12
12
  import AppToastContainer from '../../components/AppToastContainer.vue'
13
+ import JobsStatusTray from '../../components/JobsStatusTray.vue'
13
14
  import { defineControlModel, defineDoseDesignControlModel } from '../../composables/useControlSchema'
14
15
  import { usePlatform } from '../../composables/usePlatform'
15
16
  import { useExperimentStore } from '../../stores/experiment'
@@ -18,6 +19,7 @@ import type {
18
19
  PillNavItem,
19
20
  SidebarToolSection,
20
21
  } from '../../types'
22
+ import type { JobStateInput, JobStatus } from '../../types/jobs'
21
23
 
22
24
  vi.mock('../../composables/usePlatform', () => ({
23
25
  usePlatform: vi.fn(() => ({
@@ -131,6 +133,13 @@ function mountIntegratedPageSelectorWorkspace(): VueWrapper {
131
133
  }
132
134
 
133
135
  describe('PluginWorkspaceView', () => {
136
+ it('should leave the top bar title unset when the spec has no title', () => {
137
+ // The top bar then shows the plugin identity, not a generic "Workspace".
138
+ const wrapper = mount(PluginWorkspaceView, { props: { spec: {} }, global: globalOptions })
139
+
140
+ expect(wrapper.findComponent(AppTopBar).props('title')).toBeUndefined()
141
+ })
142
+
134
143
  it('should not forward the retired profile-button prop', () => {
135
144
  const wrapper = mount(PluginWorkspaceView, {
136
145
  global: globalOptions,
@@ -736,4 +745,57 @@ describe('PluginWorkspaceView', () => {
736
745
  },
737
746
  })
738
747
  })
748
+
749
+ describe('jobs', () => {
750
+ function workspaceJob(status: JobStatus): JobStateInput {
751
+ return {
752
+ id: 'r', job_id: 'r', kind: 'analysis', name: 'Run', status,
753
+ progress: { current_file: 0, total_files: 0, current_filename: null, percent: 40, message: '', stage: status },
754
+ queue_position: 0, warnings: [], error: null, output_path: null, save_path: null, session_id: null,
755
+ folders: [], folder_path: null, user_id: null, owner_user_id: null, experiment_id: null,
756
+ extraction_result_id: null, metadata: {}, created_at: '', started_at: null, completed_at: null,
757
+ }
758
+ }
759
+ const jobsGlobal = { ...globalOptions, stubs: { ...globalOptions.stubs, Teleport: true } }
760
+
761
+ it('should give the top bar no jobs control when spec.jobs is unset', () => {
762
+ const wrapper = mount(PluginWorkspaceView, { props: { spec: { title: 'Workspace' } }, global: jobsGlobal })
763
+ expect([wrapper.findComponent(AppTopBar).props('job'), wrapper.findComponent(JobsStatusTray).exists()])
764
+ .toEqual([undefined, false])
765
+ })
766
+
767
+ it('should open the jobs tray when the top-bar chip of a running job is clicked', async () => {
768
+ const wrapper = mount(PluginWorkspaceView, {
769
+ props: { spec: { title: 'Workspace', jobs: { jobs: [workspaceJob('running')] } } },
770
+ global: jobsGlobal,
771
+ })
772
+
773
+ await wrapper.get('.mint-topbar__job').trigger('click')
774
+
775
+ const chip = wrapper.get('.mint-topbar__job')
776
+ expect([
777
+ wrapper.findComponent(AppTopBar).props('job'),
778
+ wrapper.find('.mint-jobs-tray__panel').exists(),
779
+ chip.attributes('aria-expanded'),
780
+ chip.attributes('aria-controls') === wrapper.get('.mint-jobs-tray__panel').attributes('id'),
781
+ ]).toEqual([{ status: 'running', label: 'Run running', percent: 40 }, true, 'true', true])
782
+ })
783
+
784
+ it('should emit job-select with the held job when the chip is clicked after the job completed', async () => {
785
+ const jobs = shallowRef([workspaceJob('running')])
786
+ const wrapper = mount(PluginWorkspaceView, {
787
+ props: { spec: { title: 'Workspace', jobs: { jobs } } },
788
+ global: jobsGlobal,
789
+ })
790
+ jobs.value = [workspaceJob('completed')]
791
+ await nextTick()
792
+
793
+ await wrapper.get('.mint-topbar__job').trigger('click')
794
+
795
+ expect([
796
+ wrapper.emitted<[JobStateInput]>('job-select')?.map(([job]) => job.job_id),
797
+ wrapper.find('.mint-jobs-tray__panel').exists(),
798
+ ]).toEqual([['r'], false])
799
+ })
800
+ })
739
801
  })
@@ -107,17 +107,6 @@ describe('ResourcePickerListInternal', () => {
107
107
  expect(wrapper.emitted('update:search')?.[0]).toEqual(['plasma'])
108
108
  })
109
109
 
110
- it('should head each group with its label and a count', () => {
111
- const wrapper = setup({ groups: [{ label: 'Recent', items }] })
112
- expect(wrapper.get('.mint-resource-picker__group').text()).toBe('Recent2')
113
- })
114
-
115
- it('should hide the rows of a collapsed group', async () => {
116
- const wrapper = setup({ groups: [{ label: 'Recent', items }] })
117
- await wrapper.get('.mint-resource-picker__group').trigger('click')
118
- expect(wrapper.findAll('[role="option"]')).toHaveLength(0)
119
- })
120
-
121
110
  it('should show skeleton rows while loading', () => {
122
111
  expect(setup({ loading: true }).findAll('.mint-resource-picker__skeleton-row')).toHaveLength(4)
123
112
  })
@@ -129,13 +118,4 @@ describe('ResourcePickerListInternal', () => {
129
118
  it('should show the empty title when there are no items', () => {
130
119
  expect(setup({ items: [], emptyTitle: 'Nothing here' }).text()).toContain('Nothing here')
131
120
  })
132
-
133
- it('should leave Enter to a group header while a row is highlighted', async () => {
134
- const wrapper = setup({ groups: [{ label: 'Recent', items }] })
135
- await wrapper.findAll('[role="option"]')[0]!.trigger('mouseenter')
136
-
137
- await wrapper.get('.mint-resource-picker__group').trigger('keydown', { key: 'Enter' })
138
-
139
- expect(wrapper.emitted('select')).toBeUndefined()
140
- })
141
121
  })