@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
@@ -848,6 +848,146 @@ describe('usePluginClient', () => {
848
848
  })
849
849
  })
850
850
 
851
+ describe('secret settings references', () => {
852
+ const secretRef = { $secret: 'sec_0f2a4c6e8a0c2e4a6c8e0a2c4e6a8c0e', set: true }
853
+
854
+ function integratedPluginSettingsHost(): void {
855
+ ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
856
+ isIntegrated: true,
857
+ theme: 'light',
858
+ platformApiUrl: '/api',
859
+ plugin: {
860
+ id: 'drp',
861
+ name: 'drp',
862
+ version: '1.0.0',
863
+ route_prefix: '/drp',
864
+ api_prefix: '/api/drp',
865
+ settings_api: 'plugin',
866
+ },
867
+ }
868
+ nextGetData = {
869
+ settings: { apiKey: secretRef, method: 'first' },
870
+ secret_format: 'ref',
871
+ mode: 'integrated',
872
+ revision: 'revision-1',
873
+ }
874
+ nextPutData = {
875
+ settings: { apiKey: secretRef, method: 'second' },
876
+ secret_format: 'ref',
877
+ mode: 'integrated',
878
+ revision: 'revision-2',
879
+ }
880
+ }
881
+
882
+ it('should round-trip an untouched secret reference unchanged', async () => {
883
+ integratedPluginSettingsHost()
884
+
885
+ const settings = usePluginSettings<{ apiKey?: unknown; method?: string }>({
886
+ pluginName: 'drp',
887
+ })
888
+ await settings.load()
889
+ const saved = await settings.save({ ...settings.values.value, method: 'second' })
890
+
891
+ expect({ saved, body: requestBodies[0] }).toEqual({
892
+ saved: true,
893
+ body: { apiKey: secretRef, method: 'second', secret_format: 'ref' },
894
+ })
895
+ })
896
+
897
+ it('should submit a typed string when the user enters a new secret', async () => {
898
+ integratedPluginSettingsHost()
899
+
900
+ const settings = usePluginSettings<{ apiKey?: unknown; method?: string }>({
901
+ pluginName: 'drp',
902
+ })
903
+ await settings.load()
904
+ await settings.save({ apiKey: 'typed-secret', method: 'first' })
905
+
906
+ expect(requestBodies[0]).toEqual({
907
+ apiKey: 'typed-secret',
908
+ method: 'first',
909
+ secret_format: 'ref',
910
+ })
911
+ })
912
+
913
+ it('should submit null when the user clears a secret', async () => {
914
+ integratedPluginSettingsHost()
915
+
916
+ const settings = usePluginSettings<{ apiKey?: unknown; method?: string }>({
917
+ pluginName: 'drp',
918
+ })
919
+ await settings.load()
920
+ await settings.save({ apiKey: null, method: 'first' })
921
+
922
+ expect(requestBodies[0]).toEqual({
923
+ apiKey: null,
924
+ method: 'first',
925
+ secret_format: 'ref',
926
+ })
927
+ })
928
+
929
+ it('should declare the reference format on platform config patches', async () => {
930
+ ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
931
+ isIntegrated: true,
932
+ theme: 'light',
933
+ platformApiUrl: '/api',
934
+ plugin: {
935
+ id: 'drp',
936
+ name: 'drp',
937
+ version: '1.0.0',
938
+ route_prefix: '/drp',
939
+ api_prefix: '/api/drp',
940
+ },
941
+ }
942
+ nextGetData = {
943
+ plugin_name: 'drp',
944
+ config: { apiKey: secretRef, method: 'first' },
945
+ secret_format: 'ref',
946
+ revision: 'revision-1',
947
+ }
948
+ nextPatchData = {
949
+ plugin_name: 'drp',
950
+ config: { apiKey: secretRef, method: 'second' },
951
+ secret_format: 'ref',
952
+ revision: 'revision-2',
953
+ }
954
+
955
+ const settings = usePluginSettings<{ apiKey?: unknown; method?: string }>({
956
+ pluginName: 'drp',
957
+ })
958
+ await settings.load()
959
+ await settings.save({ apiKey: secretRef, method: 'second' })
960
+
961
+ expect(requestBodies[0]).toEqual({
962
+ config: { method: 'second', secret_format: 'ref' },
963
+ })
964
+ })
965
+
966
+ it('should omit the format marker for a host that does not publish one', async () => {
967
+ ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
968
+ isIntegrated: true,
969
+ theme: 'light',
970
+ platformApiUrl: '/api',
971
+ plugin: {
972
+ id: 'drp',
973
+ name: 'drp',
974
+ version: '1.0.0',
975
+ route_prefix: '/drp',
976
+ api_prefix: '/api/drp',
977
+ settings_api: 'plugin',
978
+ },
979
+ }
980
+ nextGetData = { settings: { method: 'first' }, mode: 'integrated', revision: 'revision-1' }
981
+ nextPutData = { settings: { method: 'second' }, mode: 'integrated', revision: 'revision-2' }
982
+
983
+ const settings = usePluginSettings<{ method?: string }>({ pluginName: 'drp' })
984
+ await settings.load()
985
+ await settings.save({ method: 'second' })
986
+
987
+ expect(requestBodies[0]).toEqual({ method: 'second' })
988
+ })
989
+ })
990
+
851
991
  it('should preserve the contract v2 form schema in settingsConfig', () => {
852
992
  const settings = usePluginSettings({
853
993
  apiBaseUrl: '/api/drp',
@@ -0,0 +1,497 @@
1
+ /**
2
+ * Tests for usePluginConfig.
3
+ *
4
+ * Covers the plugin settings life-cycle used by every plugin's
5
+ * settings pane:
6
+ *
7
+ * - resolvedName prefers explicit pluginName, falls back to platform plugin
8
+ * - load() populates config + savedConfig from /plugins/{name}/config
9
+ * - save() PATCHes the endpoint, updates savedConfig, returns true
10
+ * - isDirty reflects user edits vs last saved state
11
+ * - reset() restores config to savedConfig and clears error
12
+ * - lastLoadedAt / lastSavedAt track successful syncs
13
+ * - error messages bubble up from network failures
14
+ */
15
+
16
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
17
+ import { createPinia, setActivePinia } from 'pinia'
18
+ import axios, { type AxiosRequestConfig } from 'axios'
19
+ import { nextTick } from 'vue'
20
+
21
+ // usePluginConfig calls onMounted(load); happy-dom doesn't run Vue's
22
+ // lifecycle outside a component, so we stub onMounted to call the
23
+ // callback synchronously if and only if the test opts in.
24
+ let autoLoadOnMount = false
25
+ vi.mock('vue', async () => {
26
+ const actual = await vi.importActual<typeof import('vue')>('vue')
27
+ return {
28
+ ...actual,
29
+ onMounted: vi.fn((cb: () => void) => {
30
+ if (autoLoadOnMount) cb()
31
+ }),
32
+ }
33
+ })
34
+
35
+ import { usePluginConfig } from '../../composables/usePluginConfig'
36
+
37
+ type FakeResponse = Record<string, unknown>
38
+
39
+ let getResponse: FakeResponse = { plugin_name: 'drp', config: {} }
40
+ let patchResponse: FakeResponse = { plugin_name: 'drp', config: {} }
41
+ let putResponse: FakeResponse = { plugin_name: 'drp', config: {} }
42
+ let getError: Error | null = null
43
+ let patchError: Error | null = null
44
+ let recordedBaseUrls: Array<string | undefined> = []
45
+ let recordedCalls: {
46
+ method: string
47
+ url: string
48
+ data?: unknown
49
+ headers?: AxiosRequestConfig['headers']
50
+ }[] = []
51
+
52
+ function mockAxios() {
53
+ vi.spyOn(axios.Axios.prototype, 'get').mockImplementation(async function (
54
+ this: unknown,
55
+ url: string,
56
+ config?: AxiosRequestConfig,
57
+ ) {
58
+ recordedBaseUrls.push(config?.baseURL)
59
+ recordedCalls.push({ method: 'GET', url })
60
+ if (getError) throw getError
61
+ return { data: getResponse }
62
+ })
63
+ vi.spyOn(axios.Axios.prototype, 'patch').mockImplementation(async function (
64
+ this: unknown,
65
+ url: string,
66
+ data?: unknown,
67
+ config?: AxiosRequestConfig,
68
+ ) {
69
+ recordedBaseUrls.push(config?.baseURL)
70
+ recordedCalls.push({ method: 'PATCH', url, data })
71
+ if (patchError) throw patchError
72
+ return { data: patchResponse }
73
+ })
74
+ vi.spyOn(axios.Axios.prototype, 'put').mockImplementation(async function (
75
+ this: unknown,
76
+ url: string,
77
+ data?: unknown,
78
+ config?: AxiosRequestConfig,
79
+ ) {
80
+ recordedBaseUrls.push(config?.baseURL)
81
+ recordedCalls.push({ method: 'PUT', url, data, headers: config?.headers })
82
+ return { data: putResponse }
83
+ })
84
+ }
85
+
86
+ describe('usePluginConfig', () => {
87
+ beforeEach(() => {
88
+ setActivePinia(createPinia())
89
+ autoLoadOnMount = false
90
+ getResponse = { plugin_name: 'drp', config: {} }
91
+ patchResponse = { plugin_name: 'drp', config: {} }
92
+ putResponse = { plugin_name: 'drp', config: {} }
93
+ getError = null
94
+ patchError = null
95
+ recordedCalls = []
96
+ recordedBaseUrls = []
97
+ delete (window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__
98
+ mockAxios()
99
+ })
100
+
101
+ afterEach(() => {
102
+ delete (window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__
103
+ vi.restoreAllMocks()
104
+ })
105
+
106
+ it('load fetches config from correct endpoint and populates state', async () => {
107
+ getResponse = {
108
+ plugin_name: 'drp',
109
+ config: { raw_files_path: '/data/rfa', threshold: 0.5 },
110
+ }
111
+ const hook = usePluginConfig('drp')
112
+ await hook.load()
113
+
114
+ expect(recordedCalls).toEqual([
115
+ { method: 'GET', url: '/plugins/drp/config' },
116
+ ])
117
+ expect(hook.config.value).toEqual({
118
+ raw_files_path: '/data/rfa',
119
+ threshold: 0.5,
120
+ })
121
+ expect(hook.isDirty.value).toBe(false)
122
+ expect(hook.error.value).toBeNull()
123
+ expect(hook.lastLoadedAt.value).toBeInstanceOf(Date)
124
+ })
125
+
126
+ it('encodes special characters in plugin name', async () => {
127
+ const hook = usePluginConfig('my/plugin')
128
+ await hook.load()
129
+ expect(recordedCalls[0]!.url).toBe('/plugins/my%2Fplugin/config')
130
+ })
131
+
132
+ it.each([
133
+ 'platform',
134
+ undefined,
135
+ ] as const)(
136
+ 'should use the platform config facade when settings_api is %s',
137
+ async (settingsApi) => {
138
+ ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
139
+ isIntegrated: true,
140
+ theme: 'light',
141
+ platformApiUrl: '/api',
142
+ plugin: {
143
+ id: 'drp',
144
+ name: 'drp',
145
+ version: '1.0.0',
146
+ route_prefix: '/drp',
147
+ api_prefix: '/api/drp',
148
+ ...(settingsApi ? { settings_api: settingsApi } : {}),
149
+ },
150
+ }
151
+ getResponse = {
152
+ plugin_name: 'drp',
153
+ config: { n: 1 },
154
+ revision: 'revision-1',
155
+ }
156
+ const hook = usePluginConfig('drp')
157
+
158
+ await hook.load()
159
+
160
+ expect(recordedCalls).toEqual([
161
+ { method: 'GET', url: '/plugins/drp/config' },
162
+ ])
163
+ },
164
+ )
165
+
166
+ it('should use the plugin settings router for an integrated plugin capability', async () => {
167
+ ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
168
+ isIntegrated: true,
169
+ theme: 'light',
170
+ platformApiUrl: '/api',
171
+ plugin: {
172
+ id: 'drp',
173
+ name: 'drp',
174
+ version: '1.0.0',
175
+ route_prefix: '/drp',
176
+ api_prefix: '/api/drp',
177
+ settings_api: 'plugin',
178
+ },
179
+ }
180
+ getResponse = {
181
+ settings: { n: 1 },
182
+ mode: 'integrated',
183
+ revision: 'revision-1',
184
+ }
185
+ putResponse = {
186
+ settings: { n: 2 },
187
+ mode: 'integrated',
188
+ revision: 'revision-2',
189
+ }
190
+ const hook = usePluginConfig('drp')
191
+ await hook.load()
192
+ hook.config.value = { n: 2 }
193
+
194
+ const saved = await hook.save()
195
+
196
+ expect({
197
+ saved,
198
+ calls: recordedCalls,
199
+ baseUrls: recordedBaseUrls,
200
+ config: hook.config.value,
201
+ }).toEqual({
202
+ saved: true,
203
+ baseUrls: ['/api/drp', '/api/drp'],
204
+ calls: [
205
+ { method: 'GET', url: '/settings' },
206
+ {
207
+ method: 'PUT',
208
+ url: '/settings',
209
+ data: { n: 2 },
210
+ headers: { 'If-Match': '"revision-1"' },
211
+ },
212
+ ],
213
+ config: { n: 2 },
214
+ })
215
+ })
216
+
217
+ it('should use the platform facade for an explicit different plugin name', async () => {
218
+ ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
219
+ isIntegrated: true,
220
+ theme: 'light',
221
+ platformApiUrl: '/api',
222
+ plugin: {
223
+ id: 'current-plugin',
224
+ name: 'current-plugin',
225
+ version: '1.0.0',
226
+ route_prefix: '/current-plugin',
227
+ api_prefix: '/api/current-plugin',
228
+ settings_api: 'plugin',
229
+ },
230
+ }
231
+ getResponse = {
232
+ plugin_name: 'other-plugin',
233
+ config: { n: 1 },
234
+ revision: 'revision-1',
235
+ }
236
+
237
+ const hook = usePluginConfig('other-plugin')
238
+ await hook.load()
239
+
240
+ expect({
241
+ calls: recordedCalls,
242
+ config: hook.config.value,
243
+ }).toEqual({
244
+ calls: [
245
+ { method: 'GET', url: '/plugins/other-plugin/config' },
246
+ ],
247
+ config: { n: 1 },
248
+ })
249
+ })
250
+
251
+ it('should PATCH only dirty typed fields through a versioned platform facade', async () => {
252
+ getResponse = {
253
+ plugin_name: 'drp',
254
+ config: {
255
+ n: 1,
256
+ allowed_experiment_types: ['metabolomics'],
257
+ },
258
+ revision: 'revision-1',
259
+ }
260
+ patchResponse = {
261
+ plugin_name: 'drp',
262
+ config: {
263
+ n: 2,
264
+ allowed_experiment_types: ['metabolomics'],
265
+ },
266
+ revision: 'revision-2',
267
+ }
268
+ const hook = usePluginConfig('drp')
269
+ await hook.load()
270
+ hook.config.value = {
271
+ n: 2,
272
+ allowed_experiment_types: ['metabolomics'],
273
+ }
274
+ await nextTick()
275
+ expect(hook.isDirty.value).toBe(true)
276
+
277
+ const ok = await hook.save()
278
+ expect(ok).toBe(true)
279
+ const patchCall = recordedCalls.find((c) => c.method === 'PATCH')!
280
+ expect(patchCall.url).toBe('/plugins/drp/config')
281
+ expect(patchCall.data).toEqual({ config: { n: 2 } })
282
+ expect(patchCall.headers).toBeUndefined()
283
+ expect(hook.config.value).toEqual({
284
+ n: 2,
285
+ allowed_experiment_types: ['metabolomics'],
286
+ })
287
+ expect(hook.isDirty.value).toBe(false)
288
+ expect(hook.lastSavedAt.value).toBeInstanceOf(Date)
289
+ })
290
+
291
+ it('should split changed platform-owned config from a versioned typed save', async () => {
292
+ getResponse = {
293
+ plugin_name: 'drp',
294
+ config: {
295
+ n: 1,
296
+ allowed_experiment_types: ['metabolomics'],
297
+ },
298
+ revision: 'revision-1',
299
+ }
300
+ patchResponse = {
301
+ plugin_name: 'drp',
302
+ config: {
303
+ n: 2,
304
+ allowed_experiment_types: ['proteomics'],
305
+ },
306
+ revision: 'untrusted-platform-revision',
307
+ }
308
+ const hook = usePluginConfig('drp')
309
+ await hook.load()
310
+ hook.config.value = {
311
+ n: 2,
312
+ allowed_experiment_types: ['proteomics'],
313
+ }
314
+
315
+ const saved = await hook.save()
316
+ patchResponse = {
317
+ plugin_name: 'drp',
318
+ config: {
319
+ n: 3,
320
+ allowed_experiment_types: ['proteomics'],
321
+ },
322
+ revision: 'revision-3',
323
+ }
324
+ hook.config.value = {
325
+ n: 3,
326
+ allowed_experiment_types: ['proteomics'],
327
+ }
328
+ const savedAgain = await hook.save()
329
+
330
+ expect({
331
+ saved,
332
+ savedAgain,
333
+ calls: recordedCalls,
334
+ config: hook.config.value,
335
+ isDirty: hook.isDirty.value,
336
+ }).toEqual({
337
+ saved: true,
338
+ savedAgain: true,
339
+ calls: [
340
+ { method: 'GET', url: '/plugins/drp/config' },
341
+ {
342
+ method: 'PATCH',
343
+ url: '/plugins/drp/config',
344
+ data: { config: { n: 2 } },
345
+ },
346
+ {
347
+ method: 'PATCH',
348
+ url: '/plugins/drp/config',
349
+ data: { config: { allowed_experiment_types: ['proteomics'] } },
350
+ },
351
+ {
352
+ method: 'PATCH',
353
+ url: '/plugins/drp/config',
354
+ data: { config: { n: 3 } },
355
+ },
356
+ ],
357
+ config: {
358
+ n: 3,
359
+ allowed_experiment_types: ['proteomics'],
360
+ },
361
+ isDirty: false,
362
+ })
363
+ })
364
+
365
+ it('should avoid a typed PUT when only the platform-owned config changes', async () => {
366
+ getResponse = {
367
+ plugin_name: 'drp',
368
+ config: {
369
+ n: 1,
370
+ allowed_experiment_types: ['metabolomics'],
371
+ },
372
+ revision: 'revision-1',
373
+ }
374
+ patchResponse = {
375
+ plugin_name: 'drp',
376
+ config: {
377
+ n: 1,
378
+ allowed_experiment_types: ['proteomics'],
379
+ },
380
+ }
381
+ const hook = usePluginConfig('drp')
382
+ await hook.load()
383
+ hook.config.value = {
384
+ n: 1,
385
+ allowed_experiment_types: ['proteomics'],
386
+ }
387
+
388
+ const saved = await hook.save()
389
+
390
+ expect({
391
+ saved,
392
+ calls: recordedCalls,
393
+ config: hook.config.value,
394
+ }).toEqual({
395
+ saved: true,
396
+ calls: [
397
+ { method: 'GET', url: '/plugins/drp/config' },
398
+ {
399
+ method: 'PATCH',
400
+ url: '/plugins/drp/config',
401
+ data: { config: { allowed_experiment_types: ['proteomics'] } },
402
+ },
403
+ ],
404
+ config: {
405
+ n: 1,
406
+ allowed_experiment_types: ['proteomics'],
407
+ },
408
+ })
409
+ })
410
+
411
+ it('should detect nested config edits and restore the saved JSON snapshot', async () => {
412
+ getResponse = {
413
+ plugin_name: 'drp',
414
+ config: {
415
+ rules: {
416
+ thresholds: [1, 2],
417
+ },
418
+ },
419
+ }
420
+ const hook = usePluginConfig('drp')
421
+ await hook.load()
422
+ const rules = hook.config.value.rules as { thresholds: number[] }
423
+ rules.thresholds[0] = 9
424
+ await nextTick()
425
+
426
+ const dirtyAfterMutation = hook.isDirty.value
427
+ hook.reset()
428
+
429
+ expect({
430
+ dirtyAfterMutation,
431
+ config: hook.config.value,
432
+ isDirty: hook.isDirty.value,
433
+ }).toEqual({
434
+ dirtyAfterMutation: true,
435
+ config: {
436
+ rules: {
437
+ thresholds: [1, 2],
438
+ },
439
+ },
440
+ isDirty: false,
441
+ })
442
+ })
443
+
444
+ it('isDirty reflects edits vs last saved snapshot', async () => {
445
+ getResponse = { plugin_name: 'drp', config: { k: 'v' } }
446
+ const hook = usePluginConfig('drp')
447
+ await hook.load()
448
+ expect(hook.isDirty.value).toBe(false)
449
+
450
+ hook.config.value = { k: 'changed' }
451
+ await nextTick()
452
+ expect(hook.isDirty.value).toBe(true)
453
+
454
+ hook.config.value = { k: 'v' }
455
+ await nextTick()
456
+ expect(hook.isDirty.value).toBe(false)
457
+ })
458
+
459
+ it('reset restores saved config and clears error', async () => {
460
+ getResponse = { plugin_name: 'drp', config: { k: 'v' } }
461
+ const hook = usePluginConfig('drp')
462
+ await hook.load()
463
+ hook.config.value = { k: 'edited' }
464
+ hook.error.value = 'something'
465
+
466
+ hook.reset()
467
+ expect(hook.config.value).toEqual({ k: 'v' })
468
+ expect(hook.error.value).toBeNull()
469
+ })
470
+
471
+ it('load surfaces network error message', async () => {
472
+ getError = new Error('network down')
473
+ const hook = usePluginConfig('drp')
474
+ await hook.load()
475
+ expect(hook.error.value).toBe('network down')
476
+ expect(hook.isLoading.value).toBe(false)
477
+ expect(hook.lastLoadedAt.value).toBeNull()
478
+ })
479
+
480
+ it('save returns false and records error on failure', async () => {
481
+ patchError = new Error('403')
482
+ const hook = usePluginConfig('drp')
483
+ await hook.load()
484
+ hook.config.value = { x: 1 }
485
+ const ok = await hook.save()
486
+ expect(ok).toBe(false)
487
+ expect(hook.error.value).toBe('403')
488
+ expect(hook.lastSavedAt.value).toBeNull()
489
+ })
490
+
491
+ it('empty plugin name produces a noop load', async () => {
492
+ const hook = usePluginConfig('')
493
+ await hook.load()
494
+ expect(recordedCalls).toEqual([])
495
+ expect(hook.error.value).toBeNull()
496
+ })
497
+ })