@morscherlab/mint-sdk 1.2.0-beta.1 → 1.2.0-beta.11

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 (408) hide show
  1. package/README.md +35 -5
  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-DyPAUCLw.js → ExperimentSelectorModal-BeEOomw_.js} +5 -5
  5. package/dist/ExperimentSelectorModal-BeEOomw_.js.map +1 -0
  6. package/dist/{SettingsModal-BgKY93zh.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/useListReorder.test.d.ts +1 -0
  15. package/dist/__tests__/composables/usePluginConfig.test.d.ts +15 -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/AutoGroupModal.vue.d.ts +1 -1
  23. package/dist/components/BatchProgressList.vue.d.ts +17 -0
  24. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  25. package/dist/components/DataFrame.vue.d.ts +14 -2
  26. package/dist/components/DatePicker.vue.d.ts +1 -1
  27. package/dist/components/DateTimePicker.vue.d.ts +2 -0
  28. package/dist/components/DropdownButton.vue.d.ts +2 -0
  29. package/dist/components/ExperimentDataViewer.vue.d.ts +26 -1
  30. package/dist/components/ExperimentPopover.vue.d.ts +1 -0
  31. package/dist/components/ExperimentTimeline.vue.d.ts +29 -1
  32. package/dist/components/FitPanel.vue.d.ts +1 -1
  33. package/dist/components/JobsStatusTray.vue.d.ts +9 -2
  34. package/dist/components/LcmsSequenceTable.vue.d.ts +29 -1
  35. package/dist/components/PluginWorkspaceView.props.d.ts +29 -1
  36. package/dist/components/PluginWorkspaceView.vue.d.ts +5 -5
  37. package/dist/components/ProgressBar.vue.d.ts +1 -1
  38. package/dist/components/ProtocolStepEditor.vue.d.ts +1 -0
  39. package/dist/components/ReagentList.vue.d.ts +29 -1
  40. package/dist/components/SampleHierarchyTree.vue.d.ts +29 -1
  41. package/dist/components/SampleLegend.vue.d.ts +1 -1
  42. package/dist/components/SampleSelector.drag.d.ts +13 -0
  43. package/dist/components/SampleSelector.tree.d.ts +2 -0
  44. package/dist/components/SampleSelector.vue.d.ts +35 -2
  45. package/dist/components/ScheduleCalendar.vue.d.ts +20 -0
  46. package/dist/components/SecretInput.vue.d.ts +28 -0
  47. package/dist/components/SettingsModal.vue.d.ts +1 -1
  48. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +2 -1
  49. package/dist/components/TimePicker.vue.d.ts +1 -0
  50. package/dist/components/WellPlate.geometry.d.ts +15 -0
  51. package/dist/components/WellPlate.interaction.d.ts +13 -0
  52. package/dist/components/WellPlate.vue.d.ts +29 -0
  53. package/dist/components/index.d.ts +9 -0
  54. package/dist/components/index.js +8 -8
  55. package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -0
  56. package/dist/components/internal/DataStateInternal.vue.d.ts +43 -0
  57. package/dist/components/internal/WellEditPopupInternal.vue.d.ts +3 -1
  58. package/dist/components/internal/WorkspaceControlsInternal.vue.d.ts +23 -0
  59. package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +70 -0
  60. package/dist/components/internal/WorkspaceSidebarInternal.vue.d.ts +43 -0
  61. package/dist/{components-DkHddRMl.js → components-CgAUzF8F.js} +2497 -2235
  62. package/dist/components-CgAUzF8F.js.map +1 -0
  63. package/dist/composables/controlSchemaTypes.d.ts +34 -0
  64. package/dist/composables/flattenWorkspaceBindings.d.ts +61 -0
  65. package/dist/composables/index.d.ts +9 -2
  66. package/dist/composables/index.js +10 -10
  67. package/dist/composables/pluginWorkspaceModel.d.ts +164 -0
  68. package/dist/composables/useAsync.d.ts +5 -1
  69. package/dist/composables/useBioTemplateWorkspace.d.ts +4 -20
  70. package/dist/composables/useCommandHistory.d.ts +5 -1
  71. package/dist/composables/useDropdownState.d.ts +18 -2
  72. package/dist/composables/useEventStream.d.ts +9 -0
  73. package/dist/composables/useFocusTrap.d.ts +21 -0
  74. package/dist/composables/useGeneratedAnalysis.d.ts +3 -1
  75. package/dist/composables/useJobsStatusTray.d.ts +25 -1
  76. package/dist/composables/useListReorder.d.ts +90 -0
  77. package/dist/composables/useMenuKeyboard.d.ts +29 -0
  78. package/dist/composables/useOptimisticMutation.d.ts +5 -1
  79. package/dist/composables/usePluginClient.d.ts +2 -0
  80. package/dist/composables/usePluginConfig.d.ts +27 -0
  81. package/dist/composables/usePluginWorkspace.d.ts +51 -0
  82. package/dist/composables/useRequestSyncState.d.ts +42 -2
  83. package/dist/composables/useResourceCrud.d.ts +5 -1
  84. package/dist/composables/useRovingFocus.d.ts +24 -0
  85. package/dist/composables/useScheduleDrag.d.ts +1 -0
  86. package/dist/composables/useWellPainting.d.ts +5 -1
  87. package/dist/composables/useWellPlateAdapter.d.ts +5 -1
  88. package/dist/composables/useWellPlateValidation.d.ts +5 -1
  89. package/dist/composables/workspaceRecipes.d.ts +188 -0
  90. package/dist/{composables-C_i90sGS.js → composables-B_pia3hO.js} +209 -82
  91. package/dist/composables-B_pia3hO.js.map +1 -0
  92. package/dist/{useApi-J6zFkHmn.js → errors-_W0-LbA6.js} +103 -3
  93. package/dist/errors-_W0-LbA6.js.map +1 -0
  94. package/dist/{experiment-Cq145oEW.js → experiment-Cwl9AYxw.js} +3 -8
  95. package/dist/experiment-Cwl9AYxw.js.map +1 -0
  96. package/dist/{experiment-utils-CLnzPPq5.js → experiment-utils-DV3DrvE_.js} +70 -51
  97. package/dist/experiment-utils-DV3DrvE_.js.map +1 -0
  98. package/dist/index.d.ts +1 -0
  99. package/dist/index.js +15 -15
  100. package/dist/install.js +2 -7
  101. package/dist/install.js.map +1 -1
  102. package/dist/permissions.d.ts +42 -1
  103. package/dist/permissions.js +20 -10
  104. package/dist/permissions.js.map +1 -1
  105. package/dist/stores/auth.d.ts +6 -0
  106. package/dist/stores/index.d.ts +1 -1
  107. package/dist/stores/index.js +3 -3
  108. package/dist/stores/settings.d.ts +31 -1
  109. package/dist/styles.css +11247 -29501
  110. package/dist/templates/builderReadUtils.d.ts +1 -1
  111. package/dist/templates/index.js +2 -4
  112. package/dist/{templates-DsP51i-S.js → templates-Qq113pyZ.js} +303 -106
  113. package/dist/templates-Qq113pyZ.js.map +1 -0
  114. package/dist/types/auth.d.ts +1 -0
  115. package/dist/types/form-builder.d.ts +1 -1
  116. package/dist/{useControlSchema-BDx2Olha.js → useControlSchema-aMZlb4Sc.js} +462 -1130
  117. package/dist/useControlSchema-aMZlb4Sc.js.map +1 -0
  118. package/dist/{useDropdownState-DSb3BC3N.js → useDropdownState-DitCtrTT.js} +22 -6
  119. package/dist/useDropdownState-DitCtrTT.js.map +1 -0
  120. package/dist/{useExperimentSelector-CV4iNfMS.js → useExperimentSelector-I8HC8hUu.js} +4 -4
  121. package/dist/{useExperimentSelector-CV4iNfMS.js.map → useExperimentSelector-I8HC8hUu.js.map} +1 -1
  122. package/dist/{useFormBuilder-BLxG3BOX.js → useFormBuilder-TCxNnqeb.js} +2 -2
  123. package/dist/{useFormBuilder-BLxG3BOX.js.map → useFormBuilder-TCxNnqeb.js.map} +1 -1
  124. package/dist/{useProtocolTemplates-CS_SQl6U.js → useProtocolTemplates-BmX_iVBR.js} +766 -212
  125. package/dist/useProtocolTemplates-BmX_iVBR.js.map +1 -0
  126. package/dist/utils/color.d.ts +13 -0
  127. package/dist/utils/errors.d.ts +9 -0
  128. package/dist/utils/pluginSecrets.d.ts +53 -0
  129. package/dist/utils/records.d.ts +4 -0
  130. package/package.json +1 -1
  131. package/src/__tests__/components/ActionMenu.test.ts +94 -0
  132. package/src/__tests__/components/AppAvatarMenu.test.ts +123 -0
  133. package/src/__tests__/components/BaseTabs.test.ts +67 -0
  134. package/src/__tests__/components/DataFrame.test.ts +183 -11
  135. package/src/__tests__/components/DataStates.test.ts +176 -0
  136. package/src/__tests__/components/DatePicker.test.ts +76 -0
  137. package/src/__tests__/components/DateTimePicker.test.ts +36 -0
  138. package/src/__tests__/components/DropdownButton.test.ts +134 -0
  139. package/src/__tests__/components/ExperimentTimeline.test.ts +63 -0
  140. package/src/__tests__/components/GroupAssigner.test.ts +53 -0
  141. package/src/__tests__/components/LcmsSequenceTable.test.ts +22 -0
  142. package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +230 -0
  143. package/src/__tests__/components/RackEditor.test.ts +35 -0
  144. package/src/__tests__/components/ReagentList.test.ts +64 -2
  145. package/src/__tests__/components/RetiredStyles.test.ts +31 -0
  146. package/src/__tests__/components/SampleSelector.drag.test.ts +51 -1
  147. package/src/__tests__/components/ScheduleCalendar.test.ts +63 -0
  148. package/src/__tests__/components/SecretInput.test.ts +87 -0
  149. package/src/__tests__/components/SegmentedControl.test.ts +88 -0
  150. package/src/__tests__/components/TimePicker.test.ts +61 -0
  151. package/src/__tests__/components/WellEditPopupInternal.test.ts +108 -0
  152. package/src/__tests__/components/WellPlate.interaction.test.ts +59 -0
  153. package/src/__tests__/composables/useAuth.test.ts +2 -0
  154. package/src/__tests__/composables/useDropdownState.test.ts +58 -1
  155. package/src/__tests__/composables/useListReorder.test.ts +145 -0
  156. package/src/__tests__/composables/usePluginClient.test.ts +140 -0
  157. package/src/__tests__/composables/usePluginConfig.test.ts +497 -0
  158. package/src/__tests__/composables/useRequestSyncState.test.ts +140 -0
  159. package/src/__tests__/docs/frontendDocsCatalog.test.ts +39 -7
  160. package/src/__tests__/stores/settings.test.ts +71 -1
  161. package/src/__tests__/styles/cssTokens.test.ts +131 -0
  162. package/src/__tests__/utils/permissions.test.ts +68 -0
  163. package/src/components/ActionMenu.vue +45 -88
  164. package/src/components/AlertBox.vue +0 -4
  165. package/src/components/AppAvatarMenu.vue +26 -7
  166. package/src/components/AppLayout.vue +0 -4
  167. package/src/components/AppPluginSwitcher.story.vue +87 -0
  168. package/src/components/AppPluginSwitcher.vue +2 -5
  169. package/src/components/AppSidebar.vue +0 -4
  170. package/src/components/AppToastContainer.vue +6 -8
  171. package/src/components/AppTopBar.vue +0 -4
  172. package/src/components/ArtifactSaveDialog.vue +0 -4
  173. package/src/components/ArtifactSelectorModal.vue +0 -4
  174. package/src/components/AuditTrail.story.vue +18 -0
  175. package/src/components/AuditTrail.vue +27 -7
  176. package/src/components/AutoGroupModal.vue +0 -4
  177. package/src/components/Avatar.vue +0 -4
  178. package/src/components/BaseButton.vue +0 -4
  179. package/src/components/BaseCheckbox.vue +0 -4
  180. package/src/components/BaseInput.vue +0 -4
  181. package/src/components/BaseModal.vue +8 -60
  182. package/src/components/BasePill.vue +0 -4
  183. package/src/components/BaseRadioGroup.vue +0 -4
  184. package/src/components/BaseSelect.vue +0 -4
  185. package/src/components/BaseSlider.vue +0 -4
  186. package/src/components/BaseTabs.vue +26 -5
  187. package/src/components/BaseTextarea.vue +0 -4
  188. package/src/components/BaseToggle.vue +0 -4
  189. package/src/components/BatchProgressList.story.vue +18 -0
  190. package/src/components/BatchProgressList.vue +32 -4
  191. package/src/components/BioTemplateExperimentWorkspaceView.vue +10 -21
  192. package/src/components/BioTemplatePackWorkspaceView.vue +11 -15
  193. package/src/components/BioTemplatePresetWorkspaceView.vue +8 -13
  194. package/src/components/Breadcrumb.vue +0 -4
  195. package/src/components/Calendar.vue +0 -4
  196. package/src/components/ChartContainer.story.vue +1 -1
  197. package/src/components/ChartContainer.vue +0 -4
  198. package/src/components/ChemicalFormula.vue +0 -4
  199. package/src/components/CollapsibleCard.vue +0 -4
  200. package/src/components/ColorSlider.vue +0 -4
  201. package/src/components/ComponentBindingRenderer.vue +1 -3
  202. package/src/components/ConcentrationInput.vue +0 -4
  203. package/src/components/ConfirmDialog.vue +0 -4
  204. package/src/components/ControlWorkspaceView.vue +11 -22
  205. package/src/components/DataFrame.story.vue +14 -0
  206. package/src/components/DataFrame.vue +97 -23
  207. package/src/components/DatePicker.vue +73 -27
  208. package/src/components/DateTimePicker.vue +45 -17
  209. package/src/components/Divider.vue +0 -4
  210. package/src/components/DoseCalculator.vue +0 -4
  211. package/src/components/DoseDesignWorkspaceView.vue +6 -25
  212. package/src/components/DropdownButton.vue +24 -6
  213. package/src/components/EmptyState.vue +0 -4
  214. package/src/components/ExperimentCodeBadge.vue +0 -4
  215. package/src/components/ExperimentDataViewer.story.vue +3 -0
  216. package/src/components/ExperimentDataViewer.vue +33 -13
  217. package/src/components/ExperimentPopover.vue +2 -5
  218. package/src/components/ExperimentSelectorModal.vue +0 -4
  219. package/src/components/ExperimentTimeline.story.vue +18 -0
  220. package/src/components/ExperimentTimeline.vue +81 -75
  221. package/src/components/FileUploader.vue +0 -4
  222. package/src/components/FitPanel.vue +0 -4
  223. package/src/components/FormActions.story.vue +96 -0
  224. package/src/components/FormActions.vue +0 -4
  225. package/src/components/FormBuilder.vue +0 -4
  226. package/src/components/FormField.vue +0 -4
  227. package/src/components/FormulaInput.vue +0 -4
  228. package/src/components/GeneratedPathInput.story.vue +68 -0
  229. package/src/components/GeneratedPlotlyResult.story.vue +96 -0
  230. package/src/components/GeneratedPlotlyResult.vue +3 -3
  231. package/src/components/GeneratedPluginView.story.vue +145 -0
  232. package/src/components/GeneratedPluginView.vue +10 -10
  233. package/src/components/GeneratedResultRenderer.story.vue +117 -0
  234. package/src/components/GeneratedResultRenderer.vue +4 -4
  235. package/src/components/GroupAssigner.vue +67 -5
  236. package/src/components/IconButton.vue +0 -4
  237. package/src/components/InstrumentAlertLog.vue +0 -4
  238. package/src/components/InstrumentStateBadge.vue +0 -4
  239. package/src/components/InstrumentStatusCard.vue +0 -4
  240. package/src/components/JobsStatusTray.vue +16 -5
  241. package/src/components/LcmsSequenceTable.vue +81 -50
  242. package/src/components/LoadingSpinner.vue +0 -4
  243. package/src/components/MobileSupportGate.vue +0 -4
  244. package/src/components/MoleculeInput.vue +0 -4
  245. package/src/components/MultiSelect.vue +0 -4
  246. package/src/components/NumberInput.vue +0 -4
  247. package/src/components/PlateMapEditor.vue +3 -14
  248. package/src/components/PluginIcon.vue +0 -4
  249. package/src/components/PluginWorkspaceView.props.ts +38 -0
  250. package/src/components/PluginWorkspaceView.vue +53 -224
  251. package/src/components/ProgressBar.vue +0 -4
  252. package/src/components/ProtocolStepEditor.vue +2 -4
  253. package/src/components/RackEditor.vue +51 -70
  254. package/src/components/ReagentEditor.vue +0 -4
  255. package/src/components/ReagentList.story.vue +18 -0
  256. package/src/components/ReagentList.vue +127 -82
  257. package/src/components/ResourceCard.vue +0 -4
  258. package/src/components/RuntimeUpdateNotice.story.vue +49 -0
  259. package/src/components/RuntimeUpdateNotice.vue +4 -4
  260. package/src/components/SampleHierarchyTree.story.vue +18 -0
  261. package/src/components/SampleHierarchyTree.vue +32 -9
  262. package/src/components/SampleLegend.vue +2 -8
  263. package/src/components/SampleSelector.drag.ts +51 -0
  264. package/src/components/SampleSelector.story.vue +6 -0
  265. package/src/components/SampleSelector.tree.ts +2 -0
  266. package/src/components/SampleSelector.vue +49 -10
  267. package/src/components/SampleSelectorGroupNode.vue +19 -0
  268. package/src/components/ScheduleCalendar.story.vue +6 -0
  269. package/src/components/ScheduleCalendar.vue +82 -7
  270. package/src/components/ScientificNumber.vue +0 -4
  271. package/src/components/SecretInput.story.vue +79 -0
  272. package/src/components/SecretInput.vue +150 -0
  273. package/src/components/SectionCard.vue +0 -4
  274. package/src/components/SegmentedControl.vue +48 -5
  275. package/src/components/SequenceInput.vue +0 -4
  276. package/src/components/SequenceProgressBar.story.vue +110 -0
  277. package/src/components/SequenceProgressBar.vue +0 -4
  278. package/src/components/SettingsModal.vue +0 -4
  279. package/src/components/Skeleton.vue +0 -4
  280. package/src/components/SmartGroupFieldRecipe.vue +8 -6
  281. package/src/components/SmartGroupManual.vue +0 -4
  282. package/src/components/StatusIndicator.vue +0 -4
  283. package/src/components/StepWizard.vue +0 -4
  284. package/src/components/TagsInput.vue +4 -6
  285. package/src/components/ThemeToggle.vue +0 -4
  286. package/src/components/TimePicker.vue +36 -7
  287. package/src/components/TimeRangeInput.vue +0 -4
  288. package/src/components/Tooltip.story.vue +3 -3
  289. package/src/components/Tooltip.vue +0 -4
  290. package/src/components/UnitInput.vue +0 -4
  291. package/src/components/WellPlate.geometry.ts +60 -1
  292. package/src/components/WellPlate.interaction.ts +44 -0
  293. package/src/components/WellPlate.story.vue +8 -0
  294. package/src/components/WellPlate.vue +53 -7
  295. package/src/components/index.ts +9 -0
  296. package/src/components/internal/AppTopBarPageSelectorInternal.vue +2 -5
  297. package/src/components/internal/AppTopBarPillNavInternal.vue +0 -4
  298. package/src/components/internal/DataStateInternal.vue +74 -0
  299. package/src/components/internal/FormSectionRenderer.vue +0 -4
  300. package/src/components/internal/WellEditPopupInternal.vue +20 -5
  301. package/src/components/internal/WorkspaceControlsInternal.vue +20 -0
  302. package/src/components/internal/WorkspaceShellInternal.vue +125 -0
  303. package/src/components/internal/WorkspaceSidebarInternal.vue +82 -0
  304. package/src/composables/controlSchemaTypes.ts +34 -0
  305. package/src/composables/experimentDesignData.ts +1 -3
  306. package/src/composables/flattenWorkspaceBindings.ts +104 -0
  307. package/src/composables/formBuilderRegistry.ts +5 -0
  308. package/src/composables/index.ts +43 -0
  309. package/src/composables/pluginWorkspaceModel.ts +182 -0
  310. package/src/composables/useAsync.ts +5 -1
  311. package/src/composables/useAuth.ts +3 -0
  312. package/src/composables/useBioTemplatePackWorkspace.ts +15 -9
  313. package/src/composables/useBioTemplatePresetWorkspace.ts +14 -15
  314. package/src/composables/useBioTemplateWorkspace.ts +18 -43
  315. package/src/composables/useCommandHistory.ts +5 -1
  316. package/src/composables/useDropdownState.ts +35 -5
  317. package/src/composables/useEventStream.ts +14 -2
  318. package/src/composables/useFileImport.ts +2 -7
  319. package/src/composables/useFocusTrap.ts +91 -0
  320. package/src/composables/useGeneratedAnalysis.ts +22 -10
  321. package/src/composables/useJobsStatusTray.ts +184 -17
  322. package/src/composables/useListReorder.story.vue +143 -0
  323. package/src/composables/useListReorder.ts +242 -0
  324. package/src/composables/useMenuKeyboard.ts +90 -0
  325. package/src/composables/useOptimisticMutation.ts +6 -16
  326. package/src/composables/usePluginClient.ts +47 -4
  327. package/src/composables/usePluginConfig.ts +252 -0
  328. package/src/composables/usePluginWorkspace.ts +208 -0
  329. package/src/composables/useRackEditor.ts +2 -11
  330. package/src/composables/useRequestSyncState.ts +98 -28
  331. package/src/composables/useResourceCrud.ts +6 -16
  332. package/src/composables/useRovingFocus.ts +66 -0
  333. package/src/composables/useScheduleDrag.ts +37 -0
  334. package/src/composables/useWellPainting.ts +10 -30
  335. package/src/composables/useWellPlateAdapter.ts +5 -1
  336. package/src/composables/useWellPlateEditor.ts +1 -11
  337. package/src/composables/useWellPlateValidation.ts +5 -1
  338. package/src/composables/workspaceRecipes.ts +267 -0
  339. package/src/index.ts +16 -0
  340. package/src/permissions.ts +48 -9
  341. package/src/stores/auth.ts +1 -0
  342. package/src/stores/experiment.ts +2 -7
  343. package/src/stores/index.ts +9 -1
  344. package/src/stores/settings.ts +93 -1
  345. package/src/styles/components/audit-trail.css +5 -5
  346. package/src/styles/components/batch-progress-list.css +7 -0
  347. package/src/styles/components/color-slider.css +2 -2
  348. package/src/styles/components/data-state.css +30 -0
  349. package/src/styles/components/dataframe.css +29 -35
  350. package/src/styles/components/experiment-data-viewer.css +4 -4
  351. package/src/styles/components/experiment-timeline.css +3 -30
  352. package/src/styles/components/form-builder.css +5 -5
  353. package/src/styles/components/lcms-sequence-table.css +16 -1
  354. package/src/styles/components/multi-select.css +1 -1
  355. package/src/styles/components/reagent-list.css +38 -31
  356. package/src/styles/components/sample-hierarchy-tree.css +5 -19
  357. package/src/styles/components/sample-selector.css +9 -2
  358. package/src/styles/components/schedule-calendar.css +8 -0
  359. package/src/styles/components/scientific-number.css +1 -1
  360. package/src/styles/components/secret-input.css +60 -0
  361. package/src/styles/components/well-plate.css +8 -0
  362. package/src/styles/index.css +9 -0
  363. package/src/styles/variables.css +29 -0
  364. package/src/templates/builderPresetControls.ts +1 -1
  365. package/src/templates/builderReadUtils.ts +1 -3
  366. package/src/templates/componentPlateHelpers.ts +3 -1
  367. package/src/templates/componentTargetResolvers.ts +1 -3
  368. package/src/templates/controlSchemaTargets.ts +1 -3
  369. package/src/templates/doseResponseAdapters.ts +2 -13
  370. package/src/templates/elisaAssayCollectionBuilder.ts +2 -2
  371. package/src/templates/plateMapBuilder.ts +2 -2
  372. package/src/templates/qpcrPlateBuilder.ts +1 -1
  373. package/src/templates/templateValidators.ts +1 -1
  374. package/src/templates/timeCourseBuilder.ts +2 -2
  375. package/src/templates/wellPlateScreenCollectionBuilder.ts +2 -2
  376. package/src/types/auth.ts +1 -0
  377. package/src/types/form-builder.ts +1 -0
  378. package/src/utils/color.ts +51 -0
  379. package/src/utils/errors.ts +29 -0
  380. package/src/utils/formModelSync.ts +1 -3
  381. package/src/utils/pluginIcon.ts +1 -3
  382. package/src/utils/pluginSecrets.ts +102 -0
  383. package/src/utils/records.ts +9 -0
  384. package/dist/EmptyState-p90quq8Z.js.map +0 -1
  385. package/dist/ExperimentPopover-tLwJ7XF4.js.map +0 -1
  386. package/dist/ExperimentSelectorModal-DyPAUCLw.js.map +0 -1
  387. package/dist/SettingsModal-BgKY93zh.js.map +0 -1
  388. package/dist/components-DkHddRMl.js.map +0 -1
  389. package/dist/composables/autoGroupConstants.d.ts +0 -1
  390. package/dist/composables/autoGroupGrouping.d.ts +0 -3
  391. package/dist/composables/useAutoGroupInputSources.d.ts +0 -32
  392. package/dist/composables-C_i90sGS.js.map +0 -1
  393. package/dist/experiment-Cq145oEW.js.map +0 -1
  394. package/dist/experiment-utils-CLnzPPq5.js.map +0 -1
  395. package/dist/templates/builderDefaults.d.ts +0 -1
  396. package/dist/templates/plateGeometry.d.ts +0 -4
  397. package/dist/templates-DsP51i-S.js.map +0 -1
  398. package/dist/useApi-J6zFkHmn.js.map +0 -1
  399. package/dist/useControlSchema-BDx2Olha.js.map +0 -1
  400. package/dist/useDropdownState-DSb3BC3N.js.map +0 -1
  401. package/dist/useProtocolTemplates-CS_SQl6U.js.map +0 -1
  402. package/src/__tests__/composables/useAutoGroupInputSources.test.ts +0 -127
  403. package/src/composables/autoGroupConstants.ts +0 -4
  404. package/src/composables/autoGroupGrouping.ts +0 -148
  405. package/src/composables/useAutoGroupInputSources.ts +0 -147
  406. package/src/templates/builderDefaults.ts +0 -11
  407. package/src/templates/plateGeometry.ts +0 -62
  408. /package/dist/__tests__/{composables/useAutoGroupInputSources.test.d.ts → components/DataStates.test.d.ts} +0 -0
@@ -0,0 +1,252 @@
1
+ import { ref, computed, onMounted, type Ref, type ComputedRef } from 'vue'
2
+ import { useApi } from './useApi'
3
+ import { usePlatformContext } from './usePlatformContext'
4
+ import { getInjectedPlatformContext } from './platformContextHelpers'
5
+ import { useRequestSyncState } from './useRequestSyncState'
6
+
7
+ const PLATFORM_OWNED_SETTINGS_KEY = 'allowed_experiment_types'
8
+
9
+ function cloneConfig(config: Record<string, unknown>): Record<string, unknown> {
10
+ return JSON.parse(JSON.stringify(config)) as Record<string, unknown>
11
+ }
12
+
13
+ function hasOwnConfig(config: Record<string, unknown>, key: string): boolean {
14
+ return Object.prototype.hasOwnProperty.call(config, key)
15
+ }
16
+
17
+ function changedTopLevelKeys(
18
+ current: Record<string, unknown>,
19
+ saved: Record<string, unknown>,
20
+ ): Set<string> {
21
+ return new Set(
22
+ [...new Set([...Object.keys(current), ...Object.keys(saved)])].filter(
23
+ key => JSON.stringify(current[key]) !== JSON.stringify(saved[key]),
24
+ ),
25
+ )
26
+ }
27
+
28
+ function topLevelConfigPatch(
29
+ current: Record<string, unknown>,
30
+ keys: Iterable<string>,
31
+ ): Record<string, unknown> {
32
+ return Object.fromEntries(
33
+ [...keys].map(key => [
34
+ key,
35
+ hasOwnConfig(current, key) ? current[key] : null,
36
+ ]),
37
+ )
38
+ }
39
+
40
+ function platformOwnedConfigPatch(config: Record<string, unknown>): Record<string, unknown> {
41
+ return {
42
+ [PLATFORM_OWNED_SETTINGS_KEY]: hasOwnConfig(config, PLATFORM_OWNED_SETTINGS_KEY)
43
+ ? config[PLATFORM_OWNED_SETTINGS_KEY]
44
+ : null,
45
+ }
46
+ }
47
+
48
+ function responseConfigPayload(response: {
49
+ config?: Record<string, unknown>
50
+ settings?: Record<string, unknown>
51
+ }): Record<string, unknown> {
52
+ return response.config ?? response.settings ?? {}
53
+ }
54
+
55
+ export interface UsePluginConfigReturn {
56
+ /** Editable plugin configuration values. */
57
+ config: Ref<Record<string, unknown>>
58
+ /** Whether the config is currently loading. */
59
+ isLoading: Ref<boolean>
60
+ /** Whether the config is currently saving. */
61
+ isSaving: Ref<boolean>
62
+ /** Error message from the last failed operation, or null. */
63
+ error: Ref<string | null>
64
+ /** Timestamp of the last successful load, or null. */
65
+ lastLoadedAt: Ref<Date | null>
66
+ /** Timestamp of the last successful save, or null. */
67
+ lastSavedAt: Ref<Date | null>
68
+ /** Whether local config differs from the last loaded or saved snapshot. */
69
+ isDirty: ComputedRef<boolean>
70
+ /** Load plugin configuration from the platform. */
71
+ load: () => Promise<void>
72
+ /** Save plugin configuration to the platform. */
73
+ save: () => Promise<boolean>
74
+ /** Restore the last loaded or saved configuration snapshot. */
75
+ reset: () => void
76
+ }
77
+
78
+ /**
79
+ * @deprecated Unused by the MINT platform and scheduled for removal in MINT 1.3; use `usePluginSettings()` for typed plugin settings.
80
+ */
81
+ export function usePluginConfig(pluginName?: string): UsePluginConfigReturn {
82
+ const injectedContext = getInjectedPlatformContext()
83
+ const { plugin } = usePlatformContext()
84
+ const platformApi = useApi()
85
+ const pluginApi = useApi({
86
+ baseUrl: injectedContext?.plugin?.api_prefix ?? plugin.value?.api_prefix,
87
+ })
88
+
89
+ const resolvedName = computed(
90
+ () => pluginName ?? plugin.value?.name ?? injectedContext?.plugin?.name ?? '',
91
+ )
92
+ const usesPluginSettingsApi = computed(() => {
93
+ const currentPlugin = injectedContext?.plugin ?? plugin.value
94
+ return currentPlugin?.settings_api === 'plugin'
95
+ && resolvedName.value === currentPlugin.name
96
+ })
97
+
98
+ const config = ref<Record<string, unknown>>({})
99
+ const savedConfig = ref<Record<string, unknown>>({})
100
+ const revision = ref<string | null>(null)
101
+ const request = useRequestSyncState('Plugin config request failed.')
102
+ const isLoading = ref(false)
103
+ const isSaving = ref(false)
104
+ const error = request.error
105
+ const lastLoadedAt = request.lastLoadedAt
106
+ const lastSavedAt = request.lastSavedAt
107
+
108
+ const isDirty = computed(() => {
109
+ return JSON.stringify(config.value) !== JSON.stringify(savedConfig.value)
110
+ })
111
+
112
+ async function load(): Promise<void> {
113
+ const name = resolvedName.value
114
+ if (!name) return
115
+
116
+ isLoading.value = true
117
+ try {
118
+ const response = usesPluginSettingsApi.value
119
+ ? await request.run(
120
+ () => pluginApi.get<{
121
+ settings: Record<string, unknown>
122
+ mode?: string
123
+ revision?: string
124
+ }>('/settings'),
125
+ { success: 'load', errorMessage: 'Failed to load plugin config' },
126
+ )
127
+ : await request.run(
128
+ () => platformApi.get<{
129
+ plugin_name: string
130
+ config: Record<string, unknown>
131
+ revision?: string
132
+ }>(
133
+ `/plugins/${encodeURIComponent(name)}/config`,
134
+ ),
135
+ { success: 'load', errorMessage: 'Failed to load plugin config' },
136
+ )
137
+ const responseConfig = responseConfigPayload(response)
138
+ config.value = cloneConfig(responseConfig)
139
+ savedConfig.value = cloneConfig(responseConfig)
140
+ revision.value = response.revision ?? null
141
+ } catch {
142
+ // Error state is handled by request.run().
143
+ } finally {
144
+ isLoading.value = false
145
+ }
146
+ }
147
+
148
+ async function save(): Promise<boolean> {
149
+ const name = resolvedName.value
150
+ if (!name) return false
151
+
152
+ isSaving.value = true
153
+ try {
154
+ const payload = cloneConfig(config.value)
155
+ let response: {
156
+ plugin_name?: string
157
+ config?: Record<string, unknown>
158
+ settings?: Record<string, unknown>
159
+ mode?: string
160
+ revision?: string
161
+ } | undefined
162
+ let nextRevision = revision.value
163
+
164
+ if (usesPluginSettingsApi.value) {
165
+ response = await request.run(
166
+ () => pluginApi.put<{
167
+ settings: Record<string, unknown>
168
+ mode?: string
169
+ revision?: string
170
+ }>(
171
+ '/settings',
172
+ payload,
173
+ revision.value
174
+ ? { headers: { 'If-Match': `"${revision.value}"` } }
175
+ : undefined,
176
+ ),
177
+ { success: 'save', errorMessage: 'Failed to save plugin config' },
178
+ )
179
+ nextRevision = response.revision ?? nextRevision
180
+ } else {
181
+ const dirtyKeys = changedTopLevelKeys(payload, savedConfig.value)
182
+ const platformOwnedChanged = dirtyKeys.has(PLATFORM_OWNED_SETTINGS_KEY)
183
+ const typedKeys = [...dirtyKeys].filter(
184
+ key => key !== PLATFORM_OWNED_SETTINGS_KEY,
185
+ )
186
+
187
+ if (typedKeys.length) {
188
+ response = await request.run(
189
+ () => platformApi.patch<{
190
+ plugin_name: string
191
+ config: Record<string, unknown>
192
+ revision?: string
193
+ }>(
194
+ `/plugins/${encodeURIComponent(name)}/config`,
195
+ { config: topLevelConfigPatch(payload, typedKeys) },
196
+ ),
197
+ { success: 'save', errorMessage: 'Failed to save plugin config' },
198
+ )
199
+ nextRevision = response.revision ?? nextRevision
200
+ }
201
+
202
+ if (platformOwnedChanged) {
203
+ response = await request.run(
204
+ () => platformApi.patch<{
205
+ plugin_name: string
206
+ config: Record<string, unknown>
207
+ revision?: string
208
+ }>(
209
+ `/plugins/${encodeURIComponent(name)}/config`,
210
+ { config: platformOwnedConfigPatch(payload) },
211
+ ),
212
+ { success: 'save', errorMessage: 'Failed to save plugin config' },
213
+ )
214
+ nextRevision = response.revision ?? nextRevision
215
+ }
216
+ }
217
+
218
+ if (!response) return true
219
+ const responseConfig = responseConfigPayload(response)
220
+ config.value = cloneConfig(responseConfig)
221
+ savedConfig.value = cloneConfig(responseConfig)
222
+ revision.value = response.revision ?? nextRevision
223
+ return true
224
+ } catch {
225
+ return false
226
+ } finally {
227
+ isSaving.value = false
228
+ }
229
+ }
230
+
231
+ function reset(): void {
232
+ config.value = cloneConfig(savedConfig.value)
233
+ request.clearError()
234
+ }
235
+
236
+ onMounted(() => {
237
+ load()
238
+ })
239
+
240
+ return {
241
+ config,
242
+ isLoading,
243
+ isSaving,
244
+ error,
245
+ lastLoadedAt,
246
+ lastSavedAt,
247
+ isDirty,
248
+ load,
249
+ save,
250
+ reset,
251
+ }
252
+ }
@@ -0,0 +1,208 @@
1
+ import { computed } from 'vue'
2
+ import type { ComputedRef } from 'vue'
3
+ import type { PageSelectorItem, PillNavItem } from '../types'
4
+ import type {
5
+ PluginWorkspaceViewEmit,
6
+ PluginWorkspaceViewProps,
7
+ } from '../components/PluginWorkspaceView.props'
8
+ import { usePluginWorkspaceControls } from '../components/PluginWorkspaceView.controls'
9
+ import { usePluginWorkspaceNavigation } from '../components/PluginWorkspaceView.navigation'
10
+ import {
11
+ getPluginWorkspaceSidebarSlotNames,
12
+ getPluginWorkspaceTopBarSlotNames,
13
+ hasPluginWorkspaceSidebarSurface,
14
+ } from '../components/PluginWorkspaceView.shell'
15
+ import type {
16
+ PluginWorkspaceControlsModel,
17
+ PluginWorkspaceShellModel,
18
+ PluginWorkspaceSidebarModel,
19
+ } from './pluginWorkspaceModel'
20
+
21
+ /**
22
+ * Loosely-typed view of a component's `useSlots()` result: only slot presence and names are
23
+ * read here, so this accepts any `defineSlots()` shape without fighting Vue's `Slot` render
24
+ * function type. Mirrors the `SlotKeyMap` helper type in PluginWorkspaceView.shell.ts.
25
+ */
26
+ export type PluginWorkspaceSlotsLike = Readonly<Record<string, unknown>>
27
+
28
+ export interface UsePluginWorkspaceOptions {
29
+ /** PluginWorkspaceView's own reactive props object (from `defineProps`). */
30
+ props: PluginWorkspaceViewProps
31
+ /** PluginWorkspaceView's own slots (from `useSlots()`), used to detect provided passthrough slots. */
32
+ slots: PluginWorkspaceSlotsLike
33
+ /** PluginWorkspaceView's own typed `emit` function (from `defineEmits<PluginWorkspaceViewEmits>()`). */
34
+ emit: PluginWorkspaceViewEmit
35
+ }
36
+
37
+ export interface UsePluginWorkspaceReturn {
38
+ /** Model consumed by WorkspaceShellInternal's `shell` prop. */
39
+ shell: ComputedRef<PluginWorkspaceShellModel>
40
+ /** Model consumed by WorkspaceSidebarInternal's `sidebar` prop. */
41
+ sidebar: ComputedRef<PluginWorkspaceSidebarModel>
42
+ /** Model consumed by WorkspaceControlsInternal's `controls` prop, and by the raw `default` slot. */
43
+ controls: ComputedRef<PluginWorkspaceControlsModel>
44
+ /** Whether AppLayout should render a sidebar surface at all. */
45
+ hasSidebarSurface: ComputedRef<boolean>
46
+ /** Applies an AppSidebar `update:model-value` payload to the shared control values and emits `update:modelValue`/`update:values`. */
47
+ handleControlValuesUpdate: (values: Record<string, unknown>) => void
48
+ /** Resolves an AppTopBar `page-selector-select` payload into the active view and emits `page-selector-select`. */
49
+ handlePageSelectorSelect: (page: PageSelectorItem) => void
50
+ /** Resolves an AppTopBar `pill-select` payload into the active view and emits `pill-select`. */
51
+ handlePillSelect: (item: PillNavItem) => void
52
+ }
53
+
54
+ /**
55
+ * Maps PluginWorkspaceView's flat 72-prop surface into the three typed model objects
56
+ * (`shell`, `sidebar`, `controls`) rendered by its internal pieces
57
+ * (WorkspaceShellInternal, WorkspaceSidebarInternal, WorkspaceControlsInternal). Reuses the
58
+ * existing `usePluginWorkspaceControls` / `usePluginWorkspaceNavigation` composables and the
59
+ * PluginWorkspaceView.shell slot-detection helpers, so it changes only how that state is
60
+ * assembled and exposed, not the underlying behavior.
61
+ *
62
+ * @example Recommended
63
+ * ```ts
64
+ * const props = withDefaults(defineProps<PluginWorkspaceViewProps>(), { ... })
65
+ * const emit = defineEmits<PluginWorkspaceViewEmits>()
66
+ * const slots = useSlots()
67
+ * const { shell, sidebar, controls } = usePluginWorkspace({ props, slots, emit })
68
+ * ```
69
+ */
70
+ export function usePluginWorkspace(options: UsePluginWorkspaceOptions): UsePluginWorkspaceReturn {
71
+ const { props, slots, emit } = options
72
+
73
+ const controlsState = usePluginWorkspaceControls({
74
+ model: () => props.model,
75
+ controls: () => props.controls,
76
+ controlOptions: () => props.controlOptions ?? {},
77
+ modelValue: () => props.modelValue,
78
+ values: () => props.values,
79
+ componentBindings: () => props.componentBindings,
80
+ componentProps: () => props.componentProps,
81
+ componentPropsById: () => props.componentPropsById,
82
+ emitValues: (values) => {
83
+ emit('update:modelValue', values)
84
+ emit('update:values', values)
85
+ },
86
+ })
87
+
88
+ const navigationState = usePluginWorkspaceNavigation({
89
+ activeView: () => props.activeView,
90
+ defaultActiveView: () => props.defaultActiveView,
91
+ currentPillId: () => props.currentPillId,
92
+ currentPageSelectorId: () => props.currentPageSelectorId,
93
+ pillNav: () => props.pillNav,
94
+ pageSelector: () => props.pageSelector,
95
+ panelIds: () => Object.keys(props.panels ?? {}),
96
+ generatedPillNav: controlsState.generatedPillNav,
97
+ generatedDefaultView: controlsState.generatedDefaultView,
98
+ emitActiveView: viewId => emit('update:activeView', viewId),
99
+ emitPageSelectorSelect: page => emit('page-selector-select', page),
100
+ emitPillSelect: item => emit('pill-select', item),
101
+ })
102
+
103
+ const topBarSlotNames = computed<string[]>(() => getPluginWorkspaceTopBarSlotNames(slots))
104
+ const sidebarSlotNames = computed<string[]>(() => getPluginWorkspaceSidebarSlotNames(slots))
105
+
106
+ const hasSidebarSurface = computed(() => hasPluginWorkspaceSidebarSurface({
107
+ showSidebar: props.showSidebar ?? true,
108
+ panels: props.panels ?? {},
109
+ forms: props.forms ?? {},
110
+ model: props.model,
111
+ controls: props.controls,
112
+ showSidebarWhenEmpty: props.showSidebarWhenEmpty ?? false,
113
+ sidebarContentId: props.sidebarContentId,
114
+ sidebarTitle: props.sidebarTitle,
115
+ sidebarSubtitle: props.sidebarSubtitle,
116
+ sidebarBadge: props.sidebarBadge,
117
+ hasSidebarSlot: Boolean(slots.sidebar),
118
+ sidebarSlotCount: sidebarSlotNames.value.length,
119
+ }))
120
+
121
+ const controls = computed<PluginWorkspaceControlsModel>(() => ({
122
+ activeView: navigationState.resolvedActiveView.value,
123
+ setActiveView: navigationState.setActiveView,
124
+ values: controlsState.internalControlValues,
125
+ componentBindings: controlsState.resolvedComponentBindings.value,
126
+ componentBindingsById: controlsState.resolvedComponentBindingsById.value,
127
+ componentProps: controlsState.resolvedComponentProps.value,
128
+ componentPropsById: controlsState.resolvedComponentPropsById.value,
129
+ }))
130
+
131
+ const shell = computed<PluginWorkspaceShellModel>(() => ({
132
+ sidebarPosition: props.sidebarPosition ?? 'left',
133
+ sidebarWidth: props.sidebarWidth,
134
+ floating: props.floating ?? true,
135
+ responsiveSidebar: props.responsiveSidebar ?? true,
136
+ sidebarToggleLabel: props.sidebarToggleLabel ?? 'Open workspace sidebar',
137
+ sidebarCloseLabel: props.sidebarCloseLabel ?? 'Close workspace sidebar',
138
+ hasSidebarSurface: hasSidebarSurface.value,
139
+ title: props.title ?? 'Workspace',
140
+ subtitle: props.subtitle,
141
+ topBarVariant: props.topBarVariant ?? 'card',
142
+ homePath: props.homePath ?? '/',
143
+ showLogo: props.showLogo ?? true,
144
+ pageSelector: props.pageSelector,
145
+ currentPageSelectorId: navigationState.resolvedCurrentPageSelectorId.value,
146
+ pluginSwitcher: props.pluginSwitcher,
147
+ pillNav: navigationState.resolvedPillNav.value,
148
+ currentPillId: navigationState.resolvedCurrentPillId.value,
149
+ showThemeToggle: props.showThemeToggle ?? true,
150
+ showSettings: props.showSettings ?? true,
151
+ settingsConfig: props.settingsConfig,
152
+ showStandaloneLabel: props.showStandaloneLabel ?? false,
153
+ standaloneLabel: props.standaloneLabel ?? 'Standalone',
154
+ accountMenu: props.accountMenu,
155
+ showNotifications: props.showNotifications ?? false,
156
+ hasNotificationDot: props.hasNotificationDot ?? false,
157
+ showAdmin: props.showAdmin ?? false,
158
+ adminPath: props.adminPath ?? '/admin',
159
+ userName: props.userName,
160
+ userInitial: props.userInitial,
161
+ userEmail: props.userEmail,
162
+ topBarSlotNames: topBarSlotNames.value,
163
+ controls: controls.value,
164
+ }))
165
+
166
+ const sidebar = computed<PluginWorkspaceSidebarModel>(() => ({
167
+ title: props.sidebarTitle,
168
+ subtitle: props.sidebarSubtitle,
169
+ badge: props.sidebarBadge,
170
+ variant: props.sidebarVariant ?? 'analysis',
171
+ panels: props.panels ?? {},
172
+ activeView: navigationState.resolvedActiveView.value,
173
+ floating: false,
174
+ dense: props.dense ?? true,
175
+ width: props.sidebarWidth,
176
+ side: props.sidebarPosition ?? 'left',
177
+ forms: props.forms ?? {},
178
+ contentId: props.sidebarContentId,
179
+ showWhenEmpty: (props.showSidebarWhenEmpty ?? false) || props.sidebarContentId !== undefined,
180
+ model: props.model,
181
+ controlSchema: props.controls,
182
+ controlOptions: props.controlOptions ?? {},
183
+ values: controlsState.internalControlValues,
184
+ formEnhancements: props.formEnhancements,
185
+ showFormActions: props.showFormActions ?? false,
186
+ formLoading: props.formLoading ?? false,
187
+ formDisabled: props.formDisabled ?? false,
188
+ formReadonly: props.formReadonly ?? false,
189
+ formSize: props.formSize ?? 'sm',
190
+ collapsible: props.sidebarCollapsible,
191
+ collapsed: props.sidebarCollapsed,
192
+ defaultCollapsed: props.defaultSidebarCollapsed ?? false,
193
+ collapsedWidth: props.sidebarCollapsedWidth ?? '3rem',
194
+ collapseButtonLabel: props.sidebarCollapseButtonLabel ?? 'Collapse sidebar',
195
+ expandButtonLabel: props.sidebarExpandButtonLabel ?? 'Expand sidebar',
196
+ slotNames: sidebarSlotNames.value,
197
+ }))
198
+
199
+ return {
200
+ shell,
201
+ sidebar,
202
+ controls,
203
+ hasSidebarSurface,
204
+ handleControlValuesUpdate: controlsState.handleControlValuesUpdate,
205
+ handlePageSelectorSelect: navigationState.handlePageSelectorSelect,
206
+ handlePillSelect: navigationState.handlePillSelect,
207
+ }
208
+ }
@@ -1,5 +1,6 @@
1
1
  import { ref, computed, type Ref, type ComputedRef } from 'vue'
2
2
  import type { Rack, SlotPosition, WellPlateFormat, Well } from '../types'
3
+ import { PLATE_CONFIGS, rowLabel as plateRowLabel } from '../components/WellPlate.geometry'
3
4
 
4
5
  export interface UseRackEditorOptions {
5
6
  defaultFormat?: WellPlateFormat
@@ -31,15 +32,6 @@ export interface UseRackEditorReturn {
31
32
 
32
33
  const SLOT_CYCLE: SlotPosition[] = ['R', 'G', 'B', 'Y']
33
34
 
34
- const PLATE_CONFIGS: Record<WellPlateFormat, { rows: number; cols: number }> = {
35
- 6: { rows: 2, cols: 3 },
36
- 12: { rows: 3, cols: 4 },
37
- 24: { rows: 4, cols: 6 },
38
- 48: { rows: 6, cols: 8 },
39
- 54: { rows: 6, cols: 9 },
40
- 96: { rows: 8, cols: 12 },
41
- 384: { rows: 16, cols: 24 },
42
- }
43
35
 
44
36
  let globalIdCounter = 0
45
37
 
@@ -156,8 +148,7 @@ export function useRackEditor(
156
148
  let counter = 1
157
149
  for (let row = 0; row < config.rows; row++) {
158
150
  for (let col = 0; col < config.cols; col++) {
159
- const rowLabel = String.fromCharCode(65 + row)
160
- const wellId = `${rowLabel}${col + 1}`
151
+ const wellId = `${plateRowLabel(row)}${col + 1}`
161
152
  // Only fill empty wells
162
153
  if (!rack.wells[wellId]?.sampleType) {
163
154
  const paddedNum = String(counter).padStart(3, '0')
@@ -1,10 +1,28 @@
1
1
  import { ref, type Ref } from 'vue'
2
+ import { readErrorMessage as readSharedErrorMessage } from '../utils/errors'
2
3
 
3
4
  export type RequestSyncSuccessKind = 'load' | 'save' | 'run'
4
5
 
6
+ /**
7
+ * The unit of work `run()` drives.
8
+ *
9
+ * It receives an `AbortSignal` that fires when the run is superseded by a
10
+ * newer `run()` or by `cancel()`. Zero-argument operations stay valid and
11
+ * simply ignore it.
12
+ */
13
+ export type RequestSyncOperation<T> = (signal: AbortSignal) => Promise<T>
14
+
5
15
  export interface RequestSyncRunOptions {
6
16
  success?: RequestSyncSuccessKind
7
17
  errorMessage?: string
18
+ /**
19
+ * Abort the previously started run when this one starts. Default `true`;
20
+ * pass `false` to let overlapping runs finish their own work (the stale
21
+ * guard on shared state still applies either way).
22
+ */
23
+ abortPrevious?: boolean
24
+ /** Caller-owned signal; aborting it aborts this run's signal too. */
25
+ signal?: AbortSignal
8
26
  }
9
27
 
10
28
  export interface UseRequestSyncStateReturn {
@@ -30,8 +48,32 @@ export interface UseRequestSyncStateReturn {
30
48
  markSaved: (date?: Date) => void
31
49
  /** Mark the operation as successfully run. */
32
50
  markRun: (date?: Date) => void
33
- /** Run a request with shared loading/error handling and optional sync marking. */
34
- run: <T>(operation: () => Promise<T>, options?: RequestSyncRunOptions) => Promise<T>
51
+ /**
52
+ * Run a request with shared loading/error handling and optional sync marking.
53
+ *
54
+ * ## Stale-response semantics
55
+ *
56
+ * Every call takes a monotonically increasing token. Starting a run
57
+ * supersedes any run started before it, and a superseded run writes **no**
58
+ * shared state when it finally settles: it does not clear `loading`, does not
59
+ * set `error`, and does not move `lastLoadedAt` / `lastSavedAt` / `lastRunAt`.
60
+ * Only the newest run owns those.
61
+ *
62
+ * The caller's own promise is untouched by the guard — a superseded run still
63
+ * resolves with its value or rejects with its own error, exactly as the
64
+ * operation did. The guard never invents a rejection, so a caller awaiting
65
+ * `run()` never sees a "superseded" error it did not cause.
66
+ *
67
+ * The operation additionally receives an `AbortSignal` that fires when its
68
+ * run is superseded or `cancel()` is called, so an operation that forwards
69
+ * the signal (`fetch(url, { signal })`) stops work instead of racing.
70
+ */
71
+ run: <T>(operation: RequestSyncOperation<T>, options?: RequestSyncRunOptions) => Promise<T>
72
+ /**
73
+ * Abort the in-flight run and drop `loading`. The aborted run's completion
74
+ * is treated as stale and writes no state.
75
+ */
76
+ cancel: () => void
35
77
  }
36
78
 
37
79
  /** Shared loading/error/timestamp state for generated plugin request helpers. */
@@ -49,22 +91,7 @@ export function useRequestSyncState(
49
91
  }
50
92
 
51
93
  function readErrorMessage(value: unknown, fallback = defaultErrorMessage): string {
52
- if (value instanceof Error && value.message) {
53
- return value.message
54
- }
55
- if (typeof value === 'string' && value.trim()) {
56
- return value
57
- }
58
- if (
59
- typeof value === 'object'
60
- && value !== null
61
- && 'message' in value
62
- && typeof (value as { message?: unknown }).message === 'string'
63
- && (value as { message: string }).message
64
- ) {
65
- return (value as { message: string }).message
66
- }
67
- return fallback
94
+ return readSharedErrorMessage(value, fallback)
68
95
  }
69
96
 
70
97
  function setError(value: unknown, fallback?: string): string {
@@ -85,27 +112,69 @@ export function useRequestSyncState(
85
112
  lastRunAt.value = date
86
113
  }
87
114
 
115
+ // Monotonic token guarding every write to the shared loading/error/timestamp
116
+ // state. Overlapping runs settle in any order, and a superseded one must not
117
+ // clear a newer run's loading flag or overwrite its error.
118
+ let runToken = 0
119
+ let activeController: AbortController | null = null
120
+
121
+ function cancel(): void {
122
+ runToken += 1
123
+ activeController?.abort()
124
+ activeController = null
125
+ loading.value = false
126
+ }
127
+
88
128
  async function run<T>(
89
- operation: () => Promise<T>,
129
+ operation: RequestSyncOperation<T>,
90
130
  options: RequestSyncRunOptions = {},
91
131
  ): Promise<T> {
132
+ if (options.abortPrevious !== false) {
133
+ activeController?.abort()
134
+ }
135
+
136
+ const token = (runToken += 1)
137
+ const controller = new AbortController()
138
+ activeController = controller
139
+
140
+ const callerSignal = options.signal
141
+ if (callerSignal) {
142
+ if (callerSignal.aborted) controller.abort(callerSignal.reason)
143
+ else callerSignal.addEventListener(
144
+ 'abort',
145
+ () => controller.abort(callerSignal.reason),
146
+ { once: true },
147
+ )
148
+ }
149
+
150
+ const isCurrent = (): boolean => token === runToken
151
+
92
152
  loading.value = true
93
153
  clearError()
94
154
  try {
95
- const response = await operation()
96
- if (options.success === 'load') {
97
- markLoaded()
98
- } else if (options.success === 'save') {
99
- markSaved()
100
- } else if (options.success === 'run') {
101
- markRun()
155
+ const response = await operation(controller.signal)
156
+ if (isCurrent()) {
157
+ if (options.success === 'load') {
158
+ markLoaded()
159
+ } else if (options.success === 'save') {
160
+ markSaved()
161
+ } else if (options.success === 'run') {
162
+ markRun()
163
+ }
102
164
  }
103
165
  return response
104
166
  } catch (err) {
105
- setError(err, options.errorMessage)
167
+ if (isCurrent()) {
168
+ setError(err, options.errorMessage)
169
+ }
106
170
  throw err
107
171
  } finally {
108
- loading.value = false
172
+ if (isCurrent()) {
173
+ loading.value = false
174
+ }
175
+ if (activeController === controller) {
176
+ activeController = null
177
+ }
109
178
  }
110
179
  }
111
180
 
@@ -122,5 +191,6 @@ export function useRequestSyncState(
122
191
  markSaved,
123
192
  markRun,
124
193
  run,
194
+ cancel,
125
195
  }
126
196
  }