@morscherlab/mint-sdk 1.1.25 → 1.2.0-beta.10

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 (403) hide show
  1. package/README.md +44 -12
  2. package/dist/{ExperimentPopover-tLwJ7XF4.js → ExperimentPopover-oL0fg_A0.js} +6 -4
  3. package/dist/ExperimentPopover-oL0fg_A0.js.map +1 -0
  4. package/dist/{ExperimentSelectorModal-B6P2msT4.js → ExperimentSelectorModal-BeEOomw_.js} +5 -5
  5. package/dist/ExperimentSelectorModal-BeEOomw_.js.map +1 -0
  6. package/dist/{SettingsModal-BW3Ar-uy.js → SettingsModal-Dphh3RrI.js} +5 -8
  7. package/dist/SettingsModal-Dphh3RrI.js.map +1 -0
  8. package/dist/{EmptyState-p90quq8Z.js → Skeleton-DxtwJ5gt.js} +111 -93
  9. package/dist/Skeleton-DxtwJ5gt.js.map +1 -0
  10. package/dist/__tests__/components/ExperimentTimeline.test.d.ts +1 -0
  11. package/dist/__tests__/components/PluginWorkspaceView.internal.test.d.ts +1 -0
  12. package/dist/__tests__/components/SecretInput.test.d.ts +1 -0
  13. package/dist/__tests__/components/WellEditPopupInternal.test.d.ts +1 -0
  14. package/dist/__tests__/composables/useEventStream.test.d.ts +1 -0
  15. package/dist/__tests__/composables/useListReorder.test.d.ts +1 -0
  16. package/dist/__tests__/styles/cssTokens.test.d.ts +1 -0
  17. package/dist/components/ActionMenu.vue.d.ts +1 -1
  18. package/dist/components/AppAvatarMenu.vue.d.ts +4 -0
  19. package/dist/components/AppPluginSwitcher.vue.d.ts +2 -0
  20. package/dist/components/AppSidebar.vue.d.ts +1 -1
  21. package/dist/components/AuditTrail.vue.d.ts +13 -0
  22. package/dist/components/BatchProgressList.vue.d.ts +17 -0
  23. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
  24. package/dist/components/DataFrame.vue.d.ts +14 -2
  25. package/dist/components/DatePicker.vue.d.ts +1 -1
  26. package/dist/components/DateTimePicker.vue.d.ts +2 -0
  27. package/dist/components/DropdownButton.vue.d.ts +2 -0
  28. package/dist/components/ExperimentDataViewer.vue.d.ts +26 -1
  29. package/dist/components/ExperimentPopover.vue.d.ts +1 -0
  30. package/dist/components/ExperimentTimeline.vue.d.ts +29 -1
  31. package/dist/components/FitPanel.vue.d.ts +1 -1
  32. package/dist/components/LcmsSequenceTable.vue.d.ts +29 -1
  33. package/dist/components/PluginWorkspaceView.props.d.ts +29 -1
  34. package/dist/components/PluginWorkspaceView.vue.d.ts +5 -5
  35. package/dist/components/ProgressBar.vue.d.ts +1 -1
  36. package/dist/components/ProtocolStepEditor.vue.d.ts +1 -0
  37. package/dist/components/ReagentList.vue.d.ts +29 -1
  38. package/dist/components/SampleHierarchyTree.vue.d.ts +29 -1
  39. package/dist/components/SampleSelector.drag.d.ts +13 -0
  40. package/dist/components/SampleSelector.tree.d.ts +2 -0
  41. package/dist/components/SampleSelector.vue.d.ts +34 -1
  42. package/dist/components/ScheduleCalendar.vue.d.ts +20 -0
  43. package/dist/components/SecretInput.vue.d.ts +28 -0
  44. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +2 -1
  45. package/dist/components/TimePicker.vue.d.ts +1 -0
  46. package/dist/components/WellPlate.geometry.d.ts +15 -0
  47. package/dist/components/WellPlate.interaction.d.ts +13 -0
  48. package/dist/components/WellPlate.vue.d.ts +29 -0
  49. package/dist/components/index.d.ts +9 -0
  50. package/dist/components/index.js +8 -8
  51. package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -0
  52. package/dist/components/internal/DataStateInternal.vue.d.ts +43 -0
  53. package/dist/components/internal/WellEditPopupInternal.vue.d.ts +3 -1
  54. package/dist/components/internal/WorkspaceControlsInternal.vue.d.ts +23 -0
  55. package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +70 -0
  56. package/dist/components/internal/WorkspaceSidebarInternal.vue.d.ts +43 -0
  57. package/dist/{components-DQt0tcpY.js → components-CgAUzF8F.js} +2491 -2235
  58. package/dist/components-CgAUzF8F.js.map +1 -0
  59. package/dist/composables/controlSchemaTypes.d.ts +34 -0
  60. package/dist/composables/flattenWorkspaceBindings.d.ts +61 -0
  61. package/dist/composables/index.d.ts +8 -2
  62. package/dist/composables/index.js +10 -10
  63. package/dist/composables/pluginWorkspaceModel.d.ts +164 -0
  64. package/dist/composables/useAsync.d.ts +5 -1
  65. package/dist/composables/useBioTemplateWorkspace.d.ts +4 -20
  66. package/dist/composables/useCommandHistory.d.ts +5 -1
  67. package/dist/composables/useDropdownState.d.ts +18 -2
  68. package/dist/composables/useEventStream.d.ts +52 -0
  69. package/dist/composables/useFocusTrap.d.ts +21 -0
  70. package/dist/composables/useJobsStatusTray.d.ts +1 -1
  71. package/dist/composables/useListReorder.d.ts +90 -0
  72. package/dist/composables/useMenuKeyboard.d.ts +29 -0
  73. package/dist/composables/useOptimisticMutation.d.ts +5 -1
  74. package/dist/composables/usePluginClient.d.ts +4 -17
  75. package/dist/composables/usePluginConfig.d.ts +3 -1
  76. package/dist/composables/usePluginWorkspace.d.ts +51 -0
  77. package/dist/composables/useRequestSyncState.d.ts +42 -2
  78. package/dist/composables/useResourceCrud.d.ts +5 -1
  79. package/dist/composables/useRovingFocus.d.ts +24 -0
  80. package/dist/composables/useScheduleDrag.d.ts +1 -0
  81. package/dist/composables/useWellPainting.d.ts +5 -1
  82. package/dist/composables/useWellPlateAdapter.d.ts +5 -1
  83. package/dist/composables/useWellPlateValidation.d.ts +5 -1
  84. package/dist/composables/workspaceRecipes.d.ts +188 -0
  85. package/dist/{composables-CkXDe7nm.js → composables-B_pia3hO.js} +105 -254
  86. package/dist/composables-B_pia3hO.js.map +1 -0
  87. package/dist/{useApi-CDBq6z23.js → errors-_W0-LbA6.js} +102 -3
  88. package/dist/errors-_W0-LbA6.js.map +1 -0
  89. package/dist/{experiment-Y9Iz7KYi.js → experiment-Cwl9AYxw.js} +3 -8
  90. package/dist/experiment-Cwl9AYxw.js.map +1 -0
  91. package/dist/{experiment-utils-CLnzPPq5.js → experiment-utils-DV3DrvE_.js} +70 -51
  92. package/dist/experiment-utils-DV3DrvE_.js.map +1 -0
  93. package/dist/index.d.ts +1 -0
  94. package/dist/index.js +15 -15
  95. package/dist/install.js +2 -7
  96. package/dist/install.js.map +1 -1
  97. package/dist/permissions.d.ts +42 -1
  98. package/dist/permissions.js +20 -10
  99. package/dist/permissions.js.map +1 -1
  100. package/dist/stores/index.d.ts +1 -1
  101. package/dist/stores/index.js +3 -3
  102. package/dist/stores/settings.d.ts +31 -1
  103. package/dist/styles.css +11247 -29501
  104. package/dist/templates/builderReadUtils.d.ts +1 -1
  105. package/dist/templates/index.js +2 -4
  106. package/dist/{templates-CovvRZ-9.js → templates-Qq113pyZ.js} +303 -106
  107. package/dist/templates-Qq113pyZ.js.map +1 -0
  108. package/dist/types/form-builder.d.ts +1 -1
  109. package/dist/{useControlSchema-BhoJzWzx.js → useControlSchema-aMZlb4Sc.js} +684 -1216
  110. package/dist/useControlSchema-aMZlb4Sc.js.map +1 -0
  111. package/dist/{useDropdownState-DSb3BC3N.js → useDropdownState-DitCtrTT.js} +22 -6
  112. package/dist/useDropdownState-DitCtrTT.js.map +1 -0
  113. package/dist/{useExperimentSelector-D0pjj9WH.js → useExperimentSelector-I8HC8hUu.js} +4 -4
  114. package/dist/{useExperimentSelector-D0pjj9WH.js.map → useExperimentSelector-I8HC8hUu.js.map} +1 -1
  115. package/dist/{useFormBuilder-DiEcDis2.js → useFormBuilder-TCxNnqeb.js} +2 -2
  116. package/dist/{useFormBuilder-DiEcDis2.js.map → useFormBuilder-TCxNnqeb.js.map} +1 -1
  117. package/dist/{useProtocolTemplates-DHwSveE6.js → useProtocolTemplates-BmX_iVBR.js} +660 -197
  118. package/dist/useProtocolTemplates-BmX_iVBR.js.map +1 -0
  119. package/dist/utils/color.d.ts +13 -0
  120. package/dist/utils/errors.d.ts +9 -0
  121. package/dist/utils/pluginSecrets.d.ts +53 -0
  122. package/dist/utils/records.d.ts +4 -0
  123. package/package.json +1 -1
  124. package/src/__tests__/components/ActionMenu.test.ts +94 -0
  125. package/src/__tests__/components/AppAvatarMenu.test.ts +123 -0
  126. package/src/__tests__/components/BaseTabs.test.ts +67 -0
  127. package/src/__tests__/components/DataFrame.test.ts +183 -11
  128. package/src/__tests__/components/DataStates.test.ts +176 -0
  129. package/src/__tests__/components/DatePicker.test.ts +76 -0
  130. package/src/__tests__/components/DateTimePicker.test.ts +36 -0
  131. package/src/__tests__/components/DropdownButton.test.ts +134 -0
  132. package/src/__tests__/components/ExperimentTimeline.test.ts +63 -0
  133. package/src/__tests__/components/GeneratedPluginView.test.ts +0 -5
  134. package/src/__tests__/components/GroupAssigner.test.ts +53 -0
  135. package/src/__tests__/components/JobsStatusTray.test.ts +20 -18
  136. package/src/__tests__/components/LcmsSequenceTable.test.ts +22 -0
  137. package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +230 -0
  138. package/src/__tests__/components/RackEditor.test.ts +35 -0
  139. package/src/__tests__/components/ReagentList.test.ts +64 -2
  140. package/src/__tests__/components/RetiredStyles.test.ts +31 -0
  141. package/src/__tests__/components/SampleSelector.drag.test.ts +51 -1
  142. package/src/__tests__/components/ScheduleCalendar.test.ts +63 -0
  143. package/src/__tests__/components/SecretInput.test.ts +87 -0
  144. package/src/__tests__/components/SegmentedControl.test.ts +88 -0
  145. package/src/__tests__/components/TimePicker.test.ts +61 -0
  146. package/src/__tests__/components/WellEditPopupInternal.test.ts +108 -0
  147. package/src/__tests__/components/WellPlate.interaction.test.ts +59 -0
  148. package/src/__tests__/composables/useAuth.test.ts +25 -0
  149. package/src/__tests__/composables/useDropdownState.test.ts +58 -1
  150. package/src/__tests__/composables/useEventStream.test.ts +140 -0
  151. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +103 -0
  152. package/src/__tests__/composables/useJobsStatusTray.test.ts +25 -161
  153. package/src/__tests__/composables/useListReorder.test.ts +145 -0
  154. package/src/__tests__/composables/usePluginClient.test.ts +148 -13
  155. package/src/__tests__/composables/useRequestSyncState.test.ts +140 -0
  156. package/src/__tests__/docs/frontendDocsCatalog.test.ts +55 -2
  157. package/src/__tests__/stores/settings.test.ts +71 -1
  158. package/src/__tests__/styles/cssTokens.test.ts +131 -0
  159. package/src/__tests__/utils/permissions.test.ts +68 -0
  160. package/src/components/ActionMenu.vue +45 -88
  161. package/src/components/AlertBox.vue +0 -4
  162. package/src/components/AppAvatarMenu.vue +26 -7
  163. package/src/components/AppLayout.vue +0 -4
  164. package/src/components/AppPluginSwitcher.story.vue +87 -0
  165. package/src/components/AppPluginSwitcher.vue +2 -5
  166. package/src/components/AppSidebar.vue +0 -4
  167. package/src/components/AppToastContainer.vue +6 -8
  168. package/src/components/AppTopBar.vue +0 -4
  169. package/src/components/ArtifactSaveDialog.vue +0 -4
  170. package/src/components/ArtifactSelectorModal.vue +0 -4
  171. package/src/components/AuditTrail.story.vue +18 -0
  172. package/src/components/AuditTrail.vue +27 -7
  173. package/src/components/AutoGroupModal.vue +0 -4
  174. package/src/components/Avatar.vue +0 -4
  175. package/src/components/BaseButton.vue +0 -4
  176. package/src/components/BaseCheckbox.vue +0 -4
  177. package/src/components/BaseInput.vue +0 -4
  178. package/src/components/BaseModal.vue +8 -60
  179. package/src/components/BasePill.vue +0 -4
  180. package/src/components/BaseRadioGroup.vue +0 -4
  181. package/src/components/BaseSelect.vue +0 -4
  182. package/src/components/BaseSlider.vue +0 -4
  183. package/src/components/BaseTabs.vue +26 -5
  184. package/src/components/BaseTextarea.vue +0 -4
  185. package/src/components/BaseToggle.vue +0 -4
  186. package/src/components/BatchProgressList.story.vue +18 -0
  187. package/src/components/BatchProgressList.vue +32 -4
  188. package/src/components/BioTemplateExperimentWorkspaceView.vue +10 -21
  189. package/src/components/BioTemplatePackWorkspaceView.vue +11 -15
  190. package/src/components/BioTemplatePresetWorkspaceView.vue +8 -13
  191. package/src/components/Breadcrumb.vue +0 -4
  192. package/src/components/Calendar.vue +0 -4
  193. package/src/components/ChartContainer.story.vue +1 -1
  194. package/src/components/ChartContainer.vue +0 -4
  195. package/src/components/ChemicalFormula.vue +0 -4
  196. package/src/components/CollapsibleCard.vue +0 -4
  197. package/src/components/ColorSlider.vue +0 -4
  198. package/src/components/ComponentBindingRenderer.vue +1 -3
  199. package/src/components/ConcentrationInput.vue +0 -4
  200. package/src/components/ConfirmDialog.vue +0 -4
  201. package/src/components/ControlWorkspaceView.vue +11 -22
  202. package/src/components/DataFrame.story.vue +14 -0
  203. package/src/components/DataFrame.vue +97 -23
  204. package/src/components/DatePicker.vue +73 -27
  205. package/src/components/DateTimePicker.vue +45 -17
  206. package/src/components/Divider.vue +0 -4
  207. package/src/components/DoseCalculator.vue +0 -4
  208. package/src/components/DoseDesignWorkspaceView.vue +6 -25
  209. package/src/components/DropdownButton.vue +24 -6
  210. package/src/components/EmptyState.vue +0 -4
  211. package/src/components/ExperimentCodeBadge.vue +0 -4
  212. package/src/components/ExperimentDataViewer.story.vue +3 -0
  213. package/src/components/ExperimentDataViewer.vue +33 -13
  214. package/src/components/ExperimentPopover.vue +2 -5
  215. package/src/components/ExperimentSelectorModal.vue +0 -4
  216. package/src/components/ExperimentTimeline.story.vue +18 -0
  217. package/src/components/ExperimentTimeline.vue +81 -75
  218. package/src/components/FileUploader.vue +0 -4
  219. package/src/components/FitPanel.vue +0 -4
  220. package/src/components/FormActions.story.vue +96 -0
  221. package/src/components/FormActions.vue +0 -4
  222. package/src/components/FormBuilder.vue +0 -4
  223. package/src/components/FormField.vue +0 -4
  224. package/src/components/FormulaInput.vue +0 -4
  225. package/src/components/GeneratedPathInput.story.vue +68 -0
  226. package/src/components/GeneratedPlotlyResult.story.vue +96 -0
  227. package/src/components/GeneratedPlotlyResult.vue +3 -3
  228. package/src/components/GeneratedPluginView.story.vue +145 -0
  229. package/src/components/GeneratedPluginView.vue +10 -10
  230. package/src/components/GeneratedResultRenderer.story.vue +117 -0
  231. package/src/components/GeneratedResultRenderer.vue +4 -4
  232. package/src/components/GroupAssigner.vue +67 -5
  233. package/src/components/IconButton.vue +0 -4
  234. package/src/components/InstrumentAlertLog.vue +0 -4
  235. package/src/components/InstrumentStateBadge.vue +0 -4
  236. package/src/components/InstrumentStatusCard.vue +0 -4
  237. package/src/components/JobsStatusTray.story.vue +6 -10
  238. package/src/components/JobsStatusTray.vue +0 -4
  239. package/src/components/LcmsSequenceTable.vue +81 -50
  240. package/src/components/LoadingSpinner.vue +0 -4
  241. package/src/components/MobileSupportGate.vue +0 -4
  242. package/src/components/MoleculeInput.vue +0 -4
  243. package/src/components/MultiSelect.vue +0 -4
  244. package/src/components/NumberInput.vue +0 -4
  245. package/src/components/PlateMapEditor.vue +3 -14
  246. package/src/components/PluginIcon.vue +0 -4
  247. package/src/components/PluginWorkspaceView.props.ts +38 -0
  248. package/src/components/PluginWorkspaceView.vue +53 -224
  249. package/src/components/ProgressBar.vue +0 -4
  250. package/src/components/ProtocolStepEditor.vue +2 -4
  251. package/src/components/RackEditor.vue +51 -70
  252. package/src/components/ReagentEditor.vue +0 -4
  253. package/src/components/ReagentList.story.vue +18 -0
  254. package/src/components/ReagentList.vue +127 -82
  255. package/src/components/ResourceCard.vue +0 -4
  256. package/src/components/RuntimeUpdateNotice.story.vue +49 -0
  257. package/src/components/RuntimeUpdateNotice.vue +4 -4
  258. package/src/components/SampleHierarchyTree.story.vue +18 -0
  259. package/src/components/SampleHierarchyTree.vue +32 -9
  260. package/src/components/SampleLegend.vue +2 -8
  261. package/src/components/SampleSelector.drag.ts +51 -0
  262. package/src/components/SampleSelector.story.vue +6 -0
  263. package/src/components/SampleSelector.tree.ts +2 -0
  264. package/src/components/SampleSelector.vue +49 -10
  265. package/src/components/SampleSelectorGroupNode.vue +19 -0
  266. package/src/components/ScheduleCalendar.story.vue +6 -0
  267. package/src/components/ScheduleCalendar.vue +82 -7
  268. package/src/components/ScientificNumber.vue +0 -4
  269. package/src/components/SecretInput.story.vue +79 -0
  270. package/src/components/SecretInput.vue +150 -0
  271. package/src/components/SectionCard.vue +0 -4
  272. package/src/components/SegmentedControl.vue +48 -5
  273. package/src/components/SequenceInput.vue +0 -4
  274. package/src/components/SequenceProgressBar.story.vue +110 -0
  275. package/src/components/SequenceProgressBar.vue +0 -4
  276. package/src/components/SettingsModal.vue +0 -4
  277. package/src/components/Skeleton.vue +0 -4
  278. package/src/components/SmartGroupFieldRecipe.vue +8 -6
  279. package/src/components/SmartGroupManual.vue +0 -4
  280. package/src/components/StatusIndicator.vue +0 -4
  281. package/src/components/StepWizard.vue +0 -4
  282. package/src/components/TagsInput.vue +4 -6
  283. package/src/components/ThemeToggle.vue +0 -4
  284. package/src/components/TimePicker.vue +36 -7
  285. package/src/components/TimeRangeInput.vue +0 -4
  286. package/src/components/Tooltip.story.vue +3 -3
  287. package/src/components/Tooltip.vue +0 -4
  288. package/src/components/UnitInput.vue +0 -4
  289. package/src/components/WellPlate.geometry.ts +60 -1
  290. package/src/components/WellPlate.interaction.ts +44 -0
  291. package/src/components/WellPlate.story.vue +8 -0
  292. package/src/components/WellPlate.vue +53 -7
  293. package/src/components/index.ts +9 -0
  294. package/src/components/internal/AppTopBarPageSelectorInternal.vue +2 -5
  295. package/src/components/internal/AppTopBarPillNavInternal.vue +0 -4
  296. package/src/components/internal/DataStateInternal.vue +74 -0
  297. package/src/components/internal/FormSectionRenderer.vue +0 -4
  298. package/src/components/internal/WellEditPopupInternal.vue +20 -5
  299. package/src/components/internal/WorkspaceControlsInternal.vue +20 -0
  300. package/src/components/internal/WorkspaceShellInternal.vue +125 -0
  301. package/src/components/internal/WorkspaceSidebarInternal.vue +82 -0
  302. package/src/composables/controlSchemaTypes.ts +34 -0
  303. package/src/composables/experimentDesignData.ts +1 -3
  304. package/src/composables/flattenWorkspaceBindings.ts +104 -0
  305. package/src/composables/formBuilderRegistry.ts +5 -0
  306. package/src/composables/index.ts +41 -5
  307. package/src/composables/pluginWorkspaceModel.ts +182 -0
  308. package/src/composables/useAsync.ts +5 -1
  309. package/src/composables/useBioTemplatePackWorkspace.ts +15 -9
  310. package/src/composables/useBioTemplatePresetWorkspace.ts +14 -15
  311. package/src/composables/useBioTemplateWorkspace.ts +18 -43
  312. package/src/composables/useCommandHistory.ts +5 -1
  313. package/src/composables/useDropdownState.ts +35 -5
  314. package/src/composables/useEventStream.ts +326 -0
  315. package/src/composables/useFileImport.ts +2 -7
  316. package/src/composables/useFocusTrap.ts +91 -0
  317. package/src/composables/useGeneratedAnalysis.ts +46 -103
  318. package/src/composables/useJobsStatusTray.ts +1 -1
  319. package/src/composables/useListReorder.story.vue +143 -0
  320. package/src/composables/useListReorder.ts +242 -0
  321. package/src/composables/useMenuKeyboard.ts +90 -0
  322. package/src/composables/useOptimisticMutation.ts +6 -16
  323. package/src/composables/usePluginClient.ts +91 -252
  324. package/src/composables/usePluginConfig.ts +3 -1
  325. package/src/composables/usePluginWorkspace.ts +208 -0
  326. package/src/composables/useRackEditor.ts +2 -11
  327. package/src/composables/useRequestSyncState.ts +98 -28
  328. package/src/composables/useResourceCrud.ts +6 -16
  329. package/src/composables/useRovingFocus.ts +66 -0
  330. package/src/composables/useScheduleDrag.ts +37 -0
  331. package/src/composables/useWellPainting.ts +10 -30
  332. package/src/composables/useWellPlateAdapter.ts +5 -1
  333. package/src/composables/useWellPlateEditor.ts +1 -11
  334. package/src/composables/useWellPlateValidation.ts +5 -1
  335. package/src/composables/workspaceRecipes.ts +267 -0
  336. package/src/index.ts +16 -0
  337. package/src/permissions.ts +48 -9
  338. package/src/stores/experiment.ts +2 -7
  339. package/src/stores/index.ts +9 -1
  340. package/src/stores/settings.ts +93 -1
  341. package/src/styles/components/audit-trail.css +5 -5
  342. package/src/styles/components/batch-progress-list.css +7 -0
  343. package/src/styles/components/color-slider.css +2 -2
  344. package/src/styles/components/data-state.css +30 -0
  345. package/src/styles/components/dataframe.css +29 -35
  346. package/src/styles/components/experiment-data-viewer.css +4 -4
  347. package/src/styles/components/experiment-timeline.css +3 -30
  348. package/src/styles/components/form-builder.css +5 -5
  349. package/src/styles/components/lcms-sequence-table.css +16 -1
  350. package/src/styles/components/multi-select.css +1 -1
  351. package/src/styles/components/reagent-list.css +38 -31
  352. package/src/styles/components/sample-hierarchy-tree.css +5 -19
  353. package/src/styles/components/sample-selector.css +9 -2
  354. package/src/styles/components/schedule-calendar.css +8 -0
  355. package/src/styles/components/scientific-number.css +1 -1
  356. package/src/styles/components/secret-input.css +60 -0
  357. package/src/styles/components/well-plate.css +8 -0
  358. package/src/styles/index.css +9 -0
  359. package/src/styles/variables.css +29 -0
  360. package/src/templates/builderPresetControls.ts +1 -1
  361. package/src/templates/builderReadUtils.ts +1 -3
  362. package/src/templates/componentPlateHelpers.ts +3 -1
  363. package/src/templates/componentTargetResolvers.ts +1 -3
  364. package/src/templates/controlSchemaTargets.ts +1 -3
  365. package/src/templates/doseResponseAdapters.ts +2 -13
  366. package/src/templates/elisaAssayCollectionBuilder.ts +2 -2
  367. package/src/templates/plateMapBuilder.ts +2 -2
  368. package/src/templates/qpcrPlateBuilder.ts +1 -1
  369. package/src/templates/templateValidators.ts +1 -1
  370. package/src/templates/timeCourseBuilder.ts +2 -2
  371. package/src/templates/wellPlateScreenCollectionBuilder.ts +2 -2
  372. package/src/types/form-builder.ts +1 -0
  373. package/src/utils/color.ts +51 -0
  374. package/src/utils/errors.ts +29 -0
  375. package/src/utils/formModelSync.ts +1 -3
  376. package/src/utils/pluginIcon.ts +1 -3
  377. package/src/utils/pluginSecrets.ts +102 -0
  378. package/src/utils/records.ts +9 -0
  379. package/dist/EmptyState-p90quq8Z.js.map +0 -1
  380. package/dist/ExperimentPopover-tLwJ7XF4.js.map +0 -1
  381. package/dist/ExperimentSelectorModal-B6P2msT4.js.map +0 -1
  382. package/dist/SettingsModal-BW3Ar-uy.js.map +0 -1
  383. package/dist/components-DQt0tcpY.js.map +0 -1
  384. package/dist/composables/autoGroupConstants.d.ts +0 -1
  385. package/dist/composables/autoGroupGrouping.d.ts +0 -3
  386. package/dist/composables/useAutoGroupInputSources.d.ts +0 -32
  387. package/dist/composables-CkXDe7nm.js.map +0 -1
  388. package/dist/experiment-Y9Iz7KYi.js.map +0 -1
  389. package/dist/experiment-utils-CLnzPPq5.js.map +0 -1
  390. package/dist/templates/builderDefaults.d.ts +0 -1
  391. package/dist/templates/plateGeometry.d.ts +0 -4
  392. package/dist/templates-CovvRZ-9.js.map +0 -1
  393. package/dist/useApi-CDBq6z23.js.map +0 -1
  394. package/dist/useControlSchema-BhoJzWzx.js.map +0 -1
  395. package/dist/useDropdownState-DSb3BC3N.js.map +0 -1
  396. package/dist/useProtocolTemplates-DHwSveE6.js.map +0 -1
  397. package/src/__tests__/composables/useAutoGroupInputSources.test.ts +0 -127
  398. package/src/composables/autoGroupConstants.ts +0 -4
  399. package/src/composables/autoGroupGrouping.ts +0 -148
  400. package/src/composables/useAutoGroupInputSources.ts +0 -147
  401. package/src/templates/builderDefaults.ts +0 -11
  402. package/src/templates/plateGeometry.ts +0 -62
  403. /package/dist/__tests__/{composables/useAutoGroupInputSources.test.d.ts → components/DataStates.test.d.ts} +0 -0
@@ -0,0 +1,140 @@
1
+ import { effectScope } from 'vue'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+
4
+ import { useEventStream } from '../../composables/useEventStream'
5
+
6
+ function pendingStreamResponse(): {
7
+ response: Response
8
+ close: () => void
9
+ } {
10
+ let close!: () => void
11
+ const body = new ReadableStream<Uint8Array>({
12
+ start(controller) {
13
+ close = () => {
14
+ try {
15
+ controller.close()
16
+ } catch {
17
+ // stop() may already have cancelled the reader.
18
+ }
19
+ }
20
+ },
21
+ })
22
+ return { response: new Response(body), close }
23
+ }
24
+
25
+ describe('useEventStream', () => {
26
+ afterEach(() => {
27
+ vi.useRealTimers()
28
+ })
29
+
30
+ it('should decode chunked UTF-8 and dispatch an unterminated multiline tail frame', async () => {
31
+ const encoded = new TextEncoder().encode(
32
+ ': keepalive\nevent: result\nid: event-7\ndata: first\ndata: 2 µmol',
33
+ )
34
+ const split = encoded.indexOf(0xc2) + 1
35
+ const body = new ReadableStream<Uint8Array>({
36
+ start(controller) {
37
+ controller.enqueue(encoded.slice(0, split))
38
+ controller.enqueue(encoded.slice(split))
39
+ controller.close()
40
+ },
41
+ })
42
+ const onMessage = vi.fn()
43
+ const scope = effectScope()
44
+ const stream = scope.run(() => useEventStream('/events', {
45
+ transport: vi.fn(async () => new Response(body)),
46
+ reconnect: false,
47
+ onMessage,
48
+ }))!
49
+
50
+ await vi.waitFor(() => expect(onMessage).toHaveBeenCalledOnce())
51
+
52
+ expect(stream.lastMessage.value).toEqual({
53
+ event: 'result',
54
+ data: 'first\n2 µmol',
55
+ id: 'event-7',
56
+ retry: undefined,
57
+ raw: ': keepalive\nevent: result\nid: event-7\ndata: first\ndata: 2 µmol',
58
+ })
59
+ scope.stop()
60
+ })
61
+
62
+ it('should keep server retry delay local and send Last-Event-ID on reconnect', async () => {
63
+ vi.useFakeTimers()
64
+ const first = new Response('id: event-1\nretry: 7\ndata: first\n\n')
65
+ const second = pendingStreamResponse()
66
+ const transport = vi.fn()
67
+ .mockResolvedValueOnce(first)
68
+ .mockResolvedValueOnce(second.response)
69
+ const options = {
70
+ transport,
71
+ reconnectDelayMs: 1_000,
72
+ }
73
+ const scope = effectScope()
74
+ const stream = scope.run(() => useEventStream('/events', options))!
75
+
76
+ await vi.waitFor(() => expect(stream.lastEventId.value).toBe('event-1'))
77
+ await vi.advanceTimersByTimeAsync(7)
78
+
79
+ const secondHeaders = new Headers(transport.mock.calls[1]?.[1]?.headers)
80
+ expect({
81
+ attempts: transport.mock.calls.length,
82
+ lastEventId: secondHeaders.get('Last-Event-ID'),
83
+ configuredDelay: options.reconnectDelayMs,
84
+ }).toEqual({
85
+ attempts: 2,
86
+ lastEventId: 'event-1',
87
+ configuredDelay: 1_000,
88
+ })
89
+ stream.stop()
90
+ second.close()
91
+ scope.stop()
92
+ })
93
+
94
+ it('should apply control-only frames without dispatching a data message', async () => {
95
+ vi.useFakeTimers()
96
+ const second = pendingStreamResponse()
97
+ const transport = vi.fn()
98
+ .mockResolvedValueOnce(new Response('id: event-2\nretry: 7\n\n'))
99
+ .mockResolvedValueOnce(second.response)
100
+ const onMessage = vi.fn()
101
+ const scope = effectScope()
102
+ const stream = scope.run(() => useEventStream('/events', {
103
+ transport,
104
+ reconnectDelayMs: 1_000,
105
+ onMessage,
106
+ }))!
107
+
108
+ await vi.waitFor(() => expect(stream.lastEventId.value).toBe('event-2'))
109
+ await vi.advanceTimersByTimeAsync(7)
110
+
111
+ expect({
112
+ messages: onMessage.mock.calls.length,
113
+ reconnectCursor: new Headers(transport.mock.calls[1]?.[1]?.headers).get('Last-Event-ID'),
114
+ }).toEqual({ messages: 0, reconnectCursor: 'event-2' })
115
+ stream.stop()
116
+ second.close()
117
+ scope.stop()
118
+ })
119
+
120
+ it('should reconnect after an ordinary stream close', async () => {
121
+ vi.useFakeTimers()
122
+ const second = pendingStreamResponse()
123
+ const transport = vi.fn()
124
+ .mockResolvedValueOnce(new Response('data: first\n\n'))
125
+ .mockResolvedValueOnce(second.response)
126
+ const scope = effectScope()
127
+ const stream = scope.run(() => useEventStream('/events', {
128
+ transport,
129
+ reconnectDelayMs: 5,
130
+ }))!
131
+
132
+ await vi.waitFor(() => expect(stream.lastMessage.value?.data).toBe('first'))
133
+ await vi.advanceTimersByTimeAsync(5)
134
+
135
+ expect(transport).toHaveBeenCalledTimes(2)
136
+ stream.stop()
137
+ second.close()
138
+ scope.stop()
139
+ })
140
+ })
@@ -126,6 +126,28 @@ function activeJob(
126
126
  }
127
127
  }
128
128
 
129
+ function streamingJobTransport(): ReturnType<typeof vi.fn<GeneratedAnalysisTransport>> {
130
+ return vi.fn<GeneratedAnalysisTransport>(async (input, init) => {
131
+ const url = String(input)
132
+ if (url.endsWith('/generated/manifest')) return response(manifest)
133
+ if (url.endsWith('/jobs/sessions') && init?.method === 'GET') {
134
+ return response({ sessions: [] })
135
+ }
136
+ if (url.endsWith('/jobs/sessions') && init?.method === 'POST') {
137
+ return response({
138
+ id: 'session-1',
139
+ tab_id: 'tab-1',
140
+ created_at: '',
141
+ last_seen_at: '',
142
+ active_job_ids: [],
143
+ })
144
+ }
145
+ if (url.endsWith('/jobs/events')) return eventStream()
146
+ if (url.endsWith('/jobs')) return response({ jobs: [activeJob('job-1')] })
147
+ throw new Error(`Unexpected request: ${url}`)
148
+ })
149
+ }
150
+
129
151
  describe('useGeneratedAnalysis', () => {
130
152
  it('is directly usable as a job center source', () => {
131
153
  const scope = effectScope()
@@ -283,6 +305,87 @@ describe('useGeneratedAnalysis', () => {
283
305
  }
284
306
  })
285
307
 
308
+ it('should poll active jobs while a closed stream waits to reconnect', async () => {
309
+ vi.useFakeTimers()
310
+ const transport = streamingJobTransport()
311
+ const scope = effectScope()
312
+ const generated = scope.run(() => useGeneratedAnalysis({
313
+ baseUrl: '/api/demo',
314
+ transport,
315
+ storage: memoryStorage(),
316
+ createId: () => 'tab-1',
317
+ pollInterval: 5,
318
+ streamReconnectDelay: 1_000,
319
+ }))!
320
+
321
+ try {
322
+ await generated.initialize()
323
+ await vi.advanceTimersByTimeAsync(5)
324
+
325
+ expect(transport.mock.calls.filter(([input]) => String(input).endsWith('/jobs'))).toHaveLength(2)
326
+ } finally {
327
+ generated.dispose()
328
+ scope.stop()
329
+ vi.useRealTimers()
330
+ }
331
+ })
332
+
333
+ it('should not poll or reconnect a paused job stream', async () => {
334
+ vi.useFakeTimers()
335
+ const transport = streamingJobTransport()
336
+ const scope = effectScope()
337
+ const generated = scope.run(() => useGeneratedAnalysis({
338
+ baseUrl: '/api/demo',
339
+ transport,
340
+ storage: memoryStorage(),
341
+ createId: () => 'tab-1',
342
+ pollInterval: 5,
343
+ streamReconnectDelay: 10,
344
+ }))!
345
+
346
+ try {
347
+ await generated.initialize()
348
+ await vi.advanceTimersByTimeAsync(0)
349
+ generated.pause()
350
+ await vi.advanceTimersByTimeAsync(20)
351
+
352
+ expect({
353
+ jobLists: transport.mock.calls.filter(([input]) => String(input).endsWith('/jobs')).length,
354
+ streams: transport.mock.calls.filter(([input]) => String(input).endsWith('/jobs/events')).length,
355
+ }).toEqual({ jobLists: 1, streams: 1 })
356
+ } finally {
357
+ generated.dispose()
358
+ scope.stop()
359
+ vi.useRealTimers()
360
+ }
361
+ })
362
+
363
+ it('should not poll or reconnect a disposed job stream', async () => {
364
+ vi.useFakeTimers()
365
+ const transport = streamingJobTransport()
366
+ const scope = effectScope()
367
+ const generated = scope.run(() => useGeneratedAnalysis({
368
+ baseUrl: '/api/demo',
369
+ transport,
370
+ storage: memoryStorage(),
371
+ createId: () => 'tab-1',
372
+ pollInterval: 5,
373
+ streamReconnectDelay: 10,
374
+ }))!
375
+
376
+ await generated.initialize()
377
+ await vi.advanceTimersByTimeAsync(0)
378
+ generated.dispose()
379
+ await vi.advanceTimersByTimeAsync(20)
380
+
381
+ expect({
382
+ jobLists: transport.mock.calls.filter(([input]) => String(input).endsWith('/jobs')).length,
383
+ streams: transport.mock.calls.filter(([input]) => String(input).endsWith('/jobs/events')).length,
384
+ }).toEqual({ jobLists: 1, streams: 1 })
385
+ scope.stop()
386
+ vi.useRealTimers()
387
+ })
388
+
286
389
  it('does not let a late submit response regress a terminal stream update', async () => {
287
390
  const terminal = completedJob('fast-service-job')
288
391
  const lateQueued = activeJob('fast-service-job')
@@ -1,14 +1,13 @@
1
1
  import { mount } from '@vue/test-utils'
2
- import { defineComponent, nextTick, ref, shallowRef } from 'vue'
2
+ import { defineComponent, nextTick, shallowRef } from 'vue'
3
3
  import { describe, expect, it, vi } from 'vitest'
4
4
 
5
5
  import {
6
6
  combinePluginJobSources,
7
- useJobsStatusTray,
8
7
  usePluginJobCenter,
9
- type UseJobsStatusTrayOptions,
8
+ type UsePluginJobCenterOptions,
10
9
  } from '../../composables/useJobsStatusTray'
11
- import type { JobState, JobStateInput, JobStatus } from '../../types/jobs'
10
+ import type { JobStateInput, JobStatus } from '../../types/jobs'
12
11
 
13
12
  function job(status: JobStatus, id: string = status): JobStateInput {
14
13
  return {
@@ -44,29 +43,18 @@ function job(status: JobStatus, id: string = status): JobStateInput {
44
43
  }
45
44
  }
46
45
 
47
- function mountComposable(options: UseJobsStatusTrayOptions) {
48
- let tray!: ReturnType<typeof useJobsStatusTray>
46
+ function mountComposable(options: UsePluginJobCenterOptions) {
47
+ let tray!: ReturnType<typeof usePluginJobCenter>
49
48
  const wrapper = mount(defineComponent({
50
49
  setup() {
51
- tray = useJobsStatusTray(options)
50
+ tray = usePluginJobCenter(options)
52
51
  return () => null
53
52
  },
54
53
  }))
55
54
  return { tray, wrapper }
56
55
  }
57
56
 
58
- function deferred<T>(): {
59
- promise: Promise<T>
60
- resolve: (value: T) => void
61
- } {
62
- let resolve!: (value: T) => void
63
- const promise = new Promise<T>((done) => {
64
- resolve = done
65
- })
66
- return { promise, resolve }
67
- }
68
-
69
- describe('useJobsStatusTray', () => {
57
+ describe('usePluginJobCenter', () => {
70
58
  it('combines live source jobs and fans out or routes source actions', async () => {
71
59
  const firstJobs = shallowRef([job('running', 'first')])
72
60
  const secondJobs = shallowRef([job('completed', 'second')])
@@ -242,37 +230,20 @@ describe('useJobsStatusTray', () => {
242
230
 
243
231
  it('keeps awaiting input jobs in the active group', () => {
244
232
  const { tray } = mountComposable({
245
- jobs: [job('awaiting_input')],
233
+ source: { jobs: [job('awaiting_input')] },
246
234
  loadOnMount: false,
247
235
  })
248
236
 
249
237
  expect(tray.activeJobs.value.map(item => item.job_id)).toEqual(['awaiting_input'])
250
238
  })
251
239
 
252
- it('replaces stale jobs when an SSE snapshot arrives', () => {
253
- const { tray } = mountComposable({ jobs: [job('running', 'old')], loadOnMount: false })
254
-
255
- tray.applyStreamMessage({
256
- event: 'snapshot',
257
- data: { jobs: [job('queued', 'new')] },
258
- })
259
-
260
- expect(tray.jobs.value.map(item => item.job_id)).toEqual(['new'])
261
- })
262
-
263
- it('moves an SSE terminal update into history', () => {
264
- const { tray } = mountComposable({ jobs: [job('running', 'fit')], loadOnMount: false })
265
-
266
- tray.applyStreamMessage({ event: 'terminal', data: job('completed', 'fit') })
267
-
268
- expect(tray.historyJobs.value.map(item => item.job_id)).toEqual(['fit'])
269
- })
270
-
271
- it('does not call a cancel adapter when the capability is disabled', async () => {
240
+ it('does not call a source cancel action when the capability is disabled', async () => {
272
241
  const cancelJob = vi.fn()
273
242
  const { tray } = mountComposable({
274
- jobs: [{ ...job('running'), can_cancel: false }],
275
- adapter: { cancelJob },
243
+ source: {
244
+ jobs: [{ ...job('running'), can_cancel: false }],
245
+ cancelJob,
246
+ },
276
247
  loadOnMount: false,
277
248
  })
278
249
 
@@ -281,139 +252,32 @@ describe('useJobsStatusTray', () => {
281
252
  expect(cancelJob).not.toHaveBeenCalled()
282
253
  })
283
254
 
284
- it('does not let a stale list response overwrite an SSE update', async () => {
285
- let resolveList!: (jobs: JobStateInput[]) => void
286
- const listJobs = vi.fn(() => new Promise<JobStateInput[]>((resolve) => {
287
- resolveList = resolve
288
- }))
289
- const { tray } = mountComposable({
290
- adapter: { listJobs },
291
- loadOnMount: false,
255
+ it('refreshes through the source without mirroring its jobs', async () => {
256
+ const sourceJobs = shallowRef([job('running', 'old')])
257
+ const refreshJobs = vi.fn(async () => {
258
+ sourceJobs.value = [job('queued', 'new')]
259
+ return sourceJobs.value
292
260
  })
293
-
294
- const refresh = tray.refresh()
295
- tray.applyStreamMessage({ event: 'job', data: job('running', 'live') })
296
- resolveList([job('queued', 'stale')])
297
- await refresh
298
- await nextTick()
299
-
300
- expect(tray.jobs.value.map(item => item.job_id)).toEqual(['live'])
301
- })
302
-
303
- it('passes only explicitly deletable jobs to the bulk clear adapter', async () => {
304
- const clearFinishedJobs = vi.fn(async (_jobs: readonly JobState[]) => undefined)
305
261
  const { tray } = mountComposable({
306
- jobs: [
307
- job('completed', 'legacy-deletable'),
308
- { ...job('failed', 'retained'), can_delete: false },
309
- ],
310
- adapter: { clearFinishedJobs },
262
+ source: { jobs: sourceJobs, refreshJobs },
311
263
  loadOnMount: false,
312
264
  })
313
265
 
314
- await tray.clearFinished()
266
+ await tray.refresh()
267
+ await nextTick()
315
268
 
316
- expect(clearFinishedJobs.mock.calls[0]![0].map(item => item.job_id)).toEqual([
317
- 'legacy-deletable',
269
+ expect([refreshJobs.mock.calls.length, tray.jobs.value.map(item => item.job_id)]).toEqual([
270
+ 1,
271
+ ['new'],
318
272
  ])
319
273
  })
320
274
 
321
275
  it('does not include active deletable jobs in bulk clear targets', () => {
322
276
  const { tray } = mountComposable({
323
- jobs: [{ ...job('awaiting_input'), can_delete: true }],
277
+ source: { jobs: [{ ...job('awaiting_input'), can_delete: true }] },
324
278
  loadOnMount: false,
325
279
  })
326
280
 
327
281
  expect(tray.deletableJobs.value).toEqual([])
328
282
  })
329
-
330
- it('does not let a late cancel response overwrite a newer SSE state', async () => {
331
- const pending = deferred<JobStateInput>()
332
- const { tray } = mountComposable({
333
- jobs: [job('running', 'fit')],
334
- adapter: { cancelJob: async () => pending.promise },
335
- loadOnMount: false,
336
- })
337
-
338
- const cancellation = tray.cancelJob(tray.jobs.value[0]!)
339
- tray.applyStreamMessage({ event: 'terminal', data: job('completed', 'fit') })
340
- pending.resolve(job('cancelled', 'fit'))
341
- await cancellation
342
-
343
- expect(tray.jobs.value[0]!.status).toBe('completed')
344
- })
345
-
346
- it('applies a cancel response after an unrelated job update', async () => {
347
- const pending = deferred<JobStateInput>()
348
- const { tray } = mountComposable({
349
- jobs: [job('running', 'fit'), job('running', 'export')],
350
- adapter: { cancelJob: async () => pending.promise },
351
- loadOnMount: false,
352
- })
353
-
354
- const cancellation = tray.cancelJob(tray.jobs.value[0]!)
355
- tray.applyStreamMessage({ event: 'job', data: job('running', 'export') })
356
- pending.resolve(job('cancelled', 'fit'))
357
- await cancellation
358
-
359
- expect(tray.jobs.value.find(item => item.job_id === 'fit')?.status).toBe('cancelled')
360
- })
361
-
362
- it('refreshes instead of removing every target after a partial bulk clear', async () => {
363
- const listJobs = vi.fn(async () => [job('completed', 'retained')])
364
- const clearFinishedJobs = vi.fn(async () => 1)
365
- const { tray } = mountComposable({
366
- jobs: [job('completed', 'deleted'), job('completed', 'retained')],
367
- adapter: { listJobs, clearFinishedJobs },
368
- loadOnMount: false,
369
- })
370
-
371
- const cleared = await tray.clearFinished()
372
-
373
- expect([
374
- cleared,
375
- listJobs.mock.calls.length,
376
- tray.jobs.value.map(item => item.job_id),
377
- ]).toEqual([false, 1, ['retained']])
378
- })
379
-
380
- it('uses a replacement reactive adapter for later actions', async () => {
381
- const first = vi.fn(async () => undefined)
382
- const second = vi.fn(async () => undefined)
383
- const adapter = shallowRef({ cancelJob: first })
384
- const { tray } = mountComposable({
385
- jobs: [job('running')],
386
- adapter,
387
- loadOnMount: false,
388
- })
389
- adapter.value = { cancelJob: second }
390
-
391
- await tray.cancelJob(tray.jobs.value[0]!)
392
-
393
- expect([first.mock.calls.length, second.mock.calls.length]).toEqual([0, 1])
394
- })
395
-
396
- it('applies the latest message from a replacement stream immediately', async () => {
397
- const eventStream = shallowRef({
398
- isConnected: ref(true),
399
- isConnecting: ref(false),
400
- error: ref<string | null>(null),
401
- lastMessage: shallowRef({
402
- event: 'snapshot',
403
- data: { jobs: [job('queued', 'replacement')] },
404
- raw: '',
405
- }),
406
- lastEventId: ref<string | null>(null),
407
- start: vi.fn(),
408
- stop: vi.fn(),
409
- })
410
-
411
- const { tray } = mountComposable({
412
- eventStream,
413
- loadOnMount: false,
414
- })
415
- await nextTick()
416
-
417
- expect(tray.jobs.value.map(item => item.job_id)).toEqual(['replacement'])
418
- })
419
283
  })
@@ -0,0 +1,145 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { describe, expect, it, vi } from 'vitest'
3
+ import { defineComponent, nextTick, ref } from 'vue'
4
+ import {
5
+ reorderList,
6
+ useListReorder,
7
+ useReorderAnnouncer,
8
+ type ListReorderMove,
9
+ } from '../../composables/useListReorder'
10
+
11
+ interface Row { id: string; name: string }
12
+
13
+ function mountHarness(onReorder: (move: ListReorderMove<Row>) => void, disabled = false) {
14
+ const items = ref<Row[]>([
15
+ { id: 'a', name: 'Alpha' },
16
+ { id: 'b', name: 'Beta' },
17
+ { id: 'c', name: 'Gamma' },
18
+ ])
19
+
20
+ const wrapper = mount(defineComponent({
21
+ setup() {
22
+ const reorder = useListReorder<Row>({
23
+ items: () => items.value,
24
+ getId: (row) => row.id,
25
+ getLabel: (row) => row.name,
26
+ disabled: () => disabled,
27
+ itemNoun: 'Row',
28
+ onReorder,
29
+ })
30
+ return { items, ...reorder }
31
+ },
32
+ template: `
33
+ <ul>
34
+ <li v-for="(row, index) in items" :key="row.id" :data-id="row.id"
35
+ @keydown="onKeydown($event, row, index)"
36
+ @dragstart="onDragStart($event, row, index)"
37
+ @dragover="onDragOver($event, row, index)"
38
+ @drop="onDrop($event, row, index)">{{ row.name }}</li>
39
+ </ul>
40
+ <p class="announcement">{{ announcement }}</p>
41
+ `,
42
+ }), { attachTo: document.body })
43
+
44
+ return { wrapper, items }
45
+ }
46
+
47
+ describe('reorderList', () => {
48
+ it('returns a copy with the item moved, leaving the source untouched', () => {
49
+ const source = ['a', 'b', 'c']
50
+
51
+ expect(reorderList(source, 0, 2)).toEqual(['b', 'c', 'a'])
52
+ expect(source).toEqual(['a', 'b', 'c'])
53
+ })
54
+ })
55
+
56
+ describe('useReorderAnnouncer', () => {
57
+ it('publishes messages and re-announces an identical message', async () => {
58
+ const { announcement, announce } = useReorderAnnouncer()
59
+
60
+ announce('Alpha moved.')
61
+ await nextTick()
62
+ expect(announcement.value).toBe('Alpha moved.')
63
+
64
+ announce('Alpha moved.')
65
+ expect(announcement.value).toBe('')
66
+ await nextTick()
67
+ expect(announcement.value).toBe('Alpha moved.')
68
+ })
69
+ })
70
+
71
+ describe('useListReorder', () => {
72
+ it('moves an item one position later on Alt+ArrowDown and announces it', async () => {
73
+ const onReorder = vi.fn()
74
+ const { wrapper } = mountHarness(onReorder)
75
+
76
+ await wrapper.get('[data-id="a"]').trigger('keydown', { key: 'ArrowDown', altKey: true })
77
+
78
+ expect(onReorder).toHaveBeenCalledTimes(1)
79
+ const move = onReorder.mock.calls[0][0] as ListReorderMove<Row>
80
+ expect(move.fromIndex).toBe(0)
81
+ expect(move.toIndex).toBe(1)
82
+ expect(move.source).toBe('keyboard')
83
+ expect(move.items.map(row => row.id)).toEqual(['b', 'a', 'c'])
84
+
85
+ await nextTick()
86
+ expect(wrapper.get('.announcement').text()).toBe('Alpha moved to position 2 of 3.')
87
+
88
+ wrapper.unmount()
89
+ })
90
+
91
+ it('moves an item one position earlier on Alt+ArrowUp and Alt+ArrowLeft', async () => {
92
+ const onReorder = vi.fn()
93
+ const { wrapper } = mountHarness(onReorder)
94
+
95
+ await wrapper.get('[data-id="c"]').trigger('keydown', { key: 'ArrowUp', altKey: true })
96
+ await wrapper.get('[data-id="c"]').trigger('keydown', { key: 'ArrowLeft', altKey: true })
97
+
98
+ expect(onReorder.mock.calls.map(call => (call[0] as ListReorderMove<Row>).toIndex)).toEqual([1, 1])
99
+
100
+ wrapper.unmount()
101
+ })
102
+
103
+ it('ignores arrow keys without Alt and moves past either end', async () => {
104
+ const onReorder = vi.fn()
105
+ const { wrapper } = mountHarness(onReorder)
106
+
107
+ await wrapper.get('[data-id="a"]').trigger('keydown', { key: 'ArrowDown' })
108
+ await wrapper.get('[data-id="a"]').trigger('keydown', { key: 'ArrowUp', altKey: true })
109
+ await wrapper.get('[data-id="c"]').trigger('keydown', { key: 'ArrowDown', altKey: true })
110
+
111
+ expect(onReorder).not.toHaveBeenCalled()
112
+
113
+ wrapper.unmount()
114
+ })
115
+
116
+ it('reorders through a drag and clears its drag state', async () => {
117
+ const onReorder = vi.fn()
118
+ const { wrapper } = mountHarness(onReorder)
119
+
120
+ const dataTransfer = { effectAllowed: '', dropEffect: '', setData: vi.fn() }
121
+ await wrapper.get('[data-id="a"]').trigger('dragstart', { dataTransfer })
122
+ await wrapper.get('[data-id="c"]').trigger('dragover', { dataTransfer })
123
+ await wrapper.get('[data-id="c"]').trigger('drop', { dataTransfer })
124
+
125
+ const move = onReorder.mock.calls[0][0] as ListReorderMove<Row>
126
+ expect(move).toMatchObject({ fromIndex: 0, toIndex: 2, source: 'drag' })
127
+ expect(move.items.map(row => row.id)).toEqual(['b', 'c', 'a'])
128
+ expect(dataTransfer.setData).toHaveBeenCalledWith('text/plain', 'a')
129
+
130
+ wrapper.unmount()
131
+ })
132
+
133
+ it('blocks every reorder while disabled', async () => {
134
+ const onReorder = vi.fn()
135
+ const { wrapper } = mountHarness(onReorder, true)
136
+
137
+ await wrapper.get('[data-id="a"]').trigger('keydown', { key: 'ArrowDown', altKey: true })
138
+ await wrapper.get('[data-id="a"]').trigger('dragstart', { dataTransfer: { setData: vi.fn() } })
139
+ await wrapper.get('[data-id="b"]').trigger('drop', { dataTransfer: { setData: vi.fn() } })
140
+
141
+ expect(onReorder).not.toHaveBeenCalled()
142
+
143
+ wrapper.unmount()
144
+ })
145
+ })