@morscherlab/mint-sdk 1.0.0-beta.2 → 1.0.0-beta.4

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 (427) hide show
  1. package/README.md +225 -6
  2. package/dist/__tests__/components/ActionItem.test.d.ts +1 -0
  3. package/dist/__tests__/components/AppAvatarMenu.test.d.ts +1 -0
  4. package/dist/__tests__/components/AppPageSelector.test.d.ts +1 -0
  5. package/dist/__tests__/components/AppPillNav.test.d.ts +1 -0
  6. package/dist/__tests__/components/AppPluginSwitcher.test.d.ts +1 -0
  7. package/dist/__tests__/components/AppToastContainer.test.d.ts +1 -0
  8. package/dist/__tests__/components/BaseRadioGroup.test.d.ts +1 -0
  9. package/dist/__tests__/components/BaseSelect.test.d.ts +1 -0
  10. package/dist/__tests__/components/BaseTabs.test.d.ts +1 -0
  11. package/dist/__tests__/components/BatchProgressList.test.d.ts +1 -0
  12. package/dist/__tests__/components/BioTemplateExperimentWorkspaceView.test.d.ts +1 -0
  13. package/dist/__tests__/components/BioTemplatePackWorkspaceView.test.d.ts +1 -0
  14. package/dist/__tests__/components/BioTemplatePresetWorkspaceView.test.d.ts +1 -0
  15. package/dist/__tests__/components/BioTemplateRenderer.test.d.ts +1 -0
  16. package/dist/__tests__/components/Breadcrumb.test.d.ts +1 -0
  17. package/dist/__tests__/components/CalendarGridPanel.test.d.ts +1 -0
  18. package/dist/__tests__/components/ComponentBindingRenderer.test.d.ts +1 -0
  19. package/dist/__tests__/components/ConcentrationInput.test.d.ts +1 -0
  20. package/dist/__tests__/components/ControlWorkspaceView.test.d.ts +1 -0
  21. package/dist/__tests__/components/DatePicker.test.d.ts +1 -0
  22. package/dist/__tests__/components/DateTimePicker.test.d.ts +1 -0
  23. package/dist/__tests__/components/DoseDesignWorkspaceView.test.d.ts +1 -0
  24. package/dist/__tests__/components/EmptyState.test.d.ts +1 -0
  25. package/dist/__tests__/components/ExperimentPopover.test.d.ts +1 -0
  26. package/dist/__tests__/components/FormBuilder.test.d.ts +1 -0
  27. package/dist/__tests__/components/GroupAssigner.test.d.ts +1 -0
  28. package/dist/__tests__/components/MultiSelect.test.d.ts +1 -0
  29. package/dist/__tests__/components/PluginWorkspaceView.test.d.ts +1 -0
  30. package/dist/__tests__/components/ProtocolStepEditor.test.d.ts +1 -0
  31. package/dist/__tests__/components/ReagentList.test.d.ts +1 -0
  32. package/dist/__tests__/components/SampleHierarchyTree.test.d.ts +1 -0
  33. package/dist/__tests__/components/SampleSelector.test.d.ts +1 -0
  34. package/dist/__tests__/components/SegmentedControl.test.d.ts +1 -0
  35. package/dist/__tests__/components/SettingsModal.test.d.ts +1 -0
  36. package/dist/__tests__/components/TagsInput.test.d.ts +1 -0
  37. package/dist/__tests__/components/ThemeToggle.test.d.ts +1 -0
  38. package/dist/__tests__/components/TimePicker.test.d.ts +1 -0
  39. package/dist/__tests__/composables/experiment-utils.test.d.ts +1 -0
  40. package/dist/__tests__/composables/useApi.test.d.ts +1 -0
  41. package/dist/__tests__/composables/useBioTemplatePackWorkspace.test.d.ts +1 -0
  42. package/dist/__tests__/composables/useBioTemplatePresetWorkspace.test.d.ts +1 -0
  43. package/dist/__tests__/composables/useBioTemplateWorkspace.test.d.ts +1 -0
  44. package/dist/__tests__/composables/useCalendarGrid.test.d.ts +1 -0
  45. package/dist/__tests__/composables/useControlSchema.test.d.ts +1 -0
  46. package/dist/__tests__/composables/useDebouncedWatch.test.d.ts +1 -0
  47. package/dist/__tests__/composables/useDropdownState.test.d.ts +1 -0
  48. package/dist/__tests__/composables/useEventListener.test.d.ts +1 -0
  49. package/dist/__tests__/composables/useExpansionSet.test.d.ts +1 -0
  50. package/dist/__tests__/composables/useExperimentData.test.d.ts +1 -0
  51. package/dist/__tests__/composables/useExperimentSelector.test.d.ts +1 -0
  52. package/dist/__tests__/composables/useGroupAssignment.test.d.ts +1 -0
  53. package/dist/__tests__/composables/useListSelection.test.d.ts +1 -0
  54. package/dist/__tests__/composables/usePluginClient.test.d.ts +1 -0
  55. package/dist/__tests__/composables/usePluginConfig.test.d.ts +1 -0
  56. package/dist/__tests__/composables/useRequestSyncState.test.d.ts +1 -0
  57. package/dist/__tests__/composables/useSampleGroups.test.d.ts +1 -0
  58. package/dist/__tests__/composables/useSelectionLimit.test.d.ts +1 -0
  59. package/dist/__tests__/composables/useSortedItems.test.d.ts +1 -0
  60. package/dist/__tests__/composables/useTemplateCollection.test.d.ts +1 -0
  61. package/dist/__tests__/composables/useTextSearch.test.d.ts +1 -0
  62. package/dist/__tests__/composables/useTheme.test.d.ts +1 -0
  63. package/dist/__tests__/composables/useTimeUtils.test.d.ts +1 -0
  64. package/dist/__tests__/docs/frontendDocsCatalog.test.d.ts +1 -0
  65. package/dist/__tests__/templates/templates.test.d.ts +1 -0
  66. package/dist/{auth-DsI0rQ7_.js → auth-QQj2kkze.js} +12 -5
  67. package/dist/auth-QQj2kkze.js.map +1 -0
  68. package/dist/components/AppAvatarMenu.vue.d.ts +2 -7
  69. package/dist/components/AppContainer.vue.d.ts +1 -1
  70. package/dist/components/AppLayout.vue.d.ts +20 -1
  71. package/dist/components/AppSidebar.vue.d.ts +111 -6
  72. package/dist/components/AppTopBar.vue.d.ts +35 -22
  73. package/dist/components/BaseButton.vue.d.ts +1 -1
  74. package/dist/components/BaseCheckbox.vue.d.ts +1 -1
  75. package/dist/components/BaseInput.vue.d.ts +2 -2
  76. package/dist/components/BasePill.vue.d.ts +2 -2
  77. package/dist/components/BaseRadioGroup.vue.d.ts +3 -3
  78. package/dist/components/BaseSelect.vue.d.ts +3 -3
  79. package/dist/components/BaseTabs.vue.d.ts +2 -2
  80. package/dist/components/BaseTextarea.vue.d.ts +1 -1
  81. package/dist/components/BaseToggle.vue.d.ts +1 -1
  82. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +119 -0
  83. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +93 -0
  84. package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +87 -0
  85. package/dist/components/BioTemplateRenderer.vue.d.ts +29 -0
  86. package/dist/components/Breadcrumb.vue.d.ts +2 -2
  87. package/dist/components/Calendar.vue.d.ts +1 -1
  88. package/dist/components/CollapsibleCard.vue.d.ts +1 -1
  89. package/dist/components/ComponentBindingRenderer.vue.d.ts +44 -0
  90. package/dist/components/ConcentrationInput.vue.d.ts +2 -2
  91. package/dist/components/ConfirmDialog.vue.d.ts +2 -2
  92. package/dist/components/ControlWorkspaceView.vue.d.ts +147 -0
  93. package/dist/components/DatePicker.vue.d.ts +1 -1
  94. package/dist/components/DateTimePicker.vue.d.ts +3 -3
  95. package/dist/components/Divider.vue.d.ts +1 -1
  96. package/dist/components/DoseDesignWorkspaceView.vue.d.ts +149 -0
  97. package/dist/components/DropdownButton.vue.d.ts +3 -3
  98. package/dist/components/EmptyState.vue.d.ts +1 -2
  99. package/dist/components/ExperimentDataViewer.vue.d.ts +1 -1
  100. package/dist/components/ExperimentTimeline.vue.d.ts +2 -2
  101. package/dist/components/FileUploader.vue.d.ts +1 -1
  102. package/dist/components/FitPanel.vue.d.ts +1 -1
  103. package/dist/components/FormActions.vue.d.ts +4 -4
  104. package/dist/components/FormBuilder.vue.d.ts +31 -17
  105. package/dist/components/FormulaInput.vue.d.ts +2 -2
  106. package/dist/components/MoleculeInput.vue.d.ts +2 -2
  107. package/dist/components/MultiSelect.vue.d.ts +3 -3
  108. package/dist/components/NumberInput.vue.d.ts +1 -1
  109. package/dist/components/PlateMapEditor.vue.d.ts +1 -1
  110. package/dist/components/PluginWorkspaceView.vue.d.ts +310 -0
  111. package/dist/components/ProgressBar.vue.d.ts +1 -1
  112. package/dist/components/ProtocolStepEditor.vue.d.ts +3 -1
  113. package/dist/components/RackEditor.vue.d.ts +2 -2
  114. package/dist/components/SampleLegend.vue.d.ts +2 -2
  115. package/dist/components/ScheduleCalendar.vue.d.ts +2 -2
  116. package/dist/components/SegmentedControl.vue.d.ts +2 -2
  117. package/dist/components/SequenceInput.vue.d.ts +3 -3
  118. package/dist/components/SettingsModal.vue.d.ts +14 -6
  119. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  120. package/dist/components/TagsInput.vue.d.ts +3 -2
  121. package/dist/components/TimePicker.vue.d.ts +3 -3
  122. package/dist/components/TimeRangeInput.vue.d.ts +1 -1
  123. package/dist/components/UnitInput.vue.d.ts +2 -2
  124. package/dist/components/WellPlate.vue.d.ts +6 -6
  125. package/dist/components/index.d.ts +9 -8
  126. package/dist/components/index.js +3 -3
  127. package/dist/components/{SettingsButton.vue.d.ts → internal/ActionItemInternal.vue.d.ts} +11 -9
  128. package/dist/components/{AppPageSelector.vue.d.ts → internal/AppPageSelectorInternal.vue.d.ts} +3 -6
  129. package/dist/components/{AppPillNav.vue.d.ts → internal/AppPillNavInternal.vue.d.ts} +4 -2
  130. package/dist/components/internal/CalendarGridPanelInternal.vue.d.ts +25 -0
  131. package/dist/components/{FormFieldRenderer.vue.d.ts → internal/FormFieldRendererInternal.vue.d.ts} +2 -2
  132. package/dist/components/{FormSection.vue.d.ts → internal/FormSectionRenderer.vue.d.ts} +7 -7
  133. package/dist/components/{WellEditPopup.vue.d.ts → internal/WellEditPopupInternal.vue.d.ts} +1 -1
  134. package/dist/{components-_XqPEhP9.js → components-BkGF4B4y.js} +9760 -8471
  135. package/dist/components-BkGF4B4y.js.map +1 -0
  136. package/dist/composables/experiment-utils.d.ts +8 -0
  137. package/dist/composables/index.d.ts +22 -5
  138. package/dist/composables/index.js +4 -3
  139. package/dist/composables/platformContextHelpers.d.ts +14 -0
  140. package/dist/composables/useAppExperiment.d.ts +31 -2
  141. package/dist/composables/useBioTemplateComponents.d.ts +22 -0
  142. package/dist/composables/useBioTemplateControls.d.ts +6 -0
  143. package/dist/composables/useBioTemplatePackWorkspace.d.ts +46 -0
  144. package/dist/composables/useBioTemplatePresetWorkspace.d.ts +75 -0
  145. package/dist/composables/useBioTemplateWorkspace.d.ts +51 -0
  146. package/dist/composables/useCalendarGrid.d.ts +26 -0
  147. package/dist/composables/useControlSchema.d.ts +343 -0
  148. package/dist/composables/useDebouncedWatch.d.ts +20 -0
  149. package/dist/composables/useDropdownState.d.ts +19 -0
  150. package/dist/composables/useEventListener.d.ts +13 -0
  151. package/dist/composables/useExpansionSet.d.ts +21 -0
  152. package/dist/composables/useExperimentData.d.ts +10 -0
  153. package/dist/composables/useExperimentSave.d.ts +31 -2
  154. package/dist/composables/useExperimentSelector.d.ts +20 -0
  155. package/dist/composables/useForm.d.ts +2 -0
  156. package/dist/composables/useGroupAssignment.d.ts +31 -0
  157. package/dist/composables/useListSelection.d.ts +35 -0
  158. package/dist/composables/usePlatformContext.d.ts +21 -3
  159. package/dist/composables/usePluginClient.d.ts +112 -0
  160. package/dist/composables/usePluginConfig.d.ts +12 -0
  161. package/dist/composables/useRequestSyncState.d.ts +34 -0
  162. package/dist/composables/useSampleGroups.d.ts +32 -0
  163. package/dist/composables/useSelectionLimit.d.ts +17 -0
  164. package/dist/composables/useSortedItems.d.ts +32 -0
  165. package/dist/composables/useTemplateCollection.d.ts +58 -0
  166. package/dist/composables/useTextSearch.d.ts +18 -0
  167. package/dist/composables/useTimeUtils.d.ts +8 -0
  168. package/dist/{composables-tiZqLu1M.js → composables-CHsME9H1.js} +240 -146
  169. package/dist/composables-CHsME9H1.js.map +1 -0
  170. package/dist/index.d.ts +6 -4
  171. package/dist/index.js +6 -5
  172. package/dist/install.d.ts +7 -2
  173. package/dist/install.js +2 -2
  174. package/dist/install.js.map +1 -1
  175. package/dist/stores/index.js +1 -1
  176. package/dist/stores/settings.d.ts +4 -1
  177. package/dist/styles.css +4746 -5514
  178. package/dist/templates/adapters.d.ts +43 -0
  179. package/dist/templates/builders.d.ts +63 -0
  180. package/dist/templates/catalog.d.ts +188 -0
  181. package/dist/templates/componentBindings.d.ts +71 -0
  182. package/dist/templates/controlSchemas.d.ts +25 -0
  183. package/dist/templates/index.d.ts +15 -0
  184. package/dist/templates/index.js +2 -0
  185. package/dist/templates/lookup.d.ts +4 -0
  186. package/dist/templates/packs.d.ts +18 -0
  187. package/dist/templates/presets.d.ts +90 -0
  188. package/dist/templates/types.d.ts +531 -0
  189. package/dist/templates-B5jmTWuk.js +9388 -0
  190. package/dist/templates-B5jmTWuk.js.map +1 -0
  191. package/dist/types/components.d.ts +26 -23
  192. package/dist/types/form-builder.d.ts +6 -8
  193. package/dist/types/index.d.ts +2 -2
  194. package/dist/types/platform.d.ts +7 -1
  195. package/dist/useScheduleDrag-BgzpQT53.js +4414 -0
  196. package/dist/useScheduleDrag-BgzpQT53.js.map +1 -0
  197. package/dist/utils/formModelSync.d.ts +5 -0
  198. package/dist/utils/items.d.ts +8 -0
  199. package/dist/utils/options.d.ts +6 -0
  200. package/dist/utils/pluginIcon.d.ts +9 -0
  201. package/package.json +7 -2
  202. package/src/__tests__/components/ActionItem.test.ts +99 -0
  203. package/src/__tests__/components/AppAvatarMenu.test.ts +27 -0
  204. package/src/__tests__/components/AppLayout.test.ts +44 -0
  205. package/src/__tests__/components/AppPageSelector.test.ts +134 -0
  206. package/src/__tests__/components/AppPillNav.test.ts +125 -0
  207. package/src/__tests__/components/AppPluginSwitcher.test.ts +44 -0
  208. package/src/__tests__/components/AppSidebar.test.ts +496 -0
  209. package/src/__tests__/components/AppToastContainer.test.ts +37 -0
  210. package/src/__tests__/components/AppTopBar.test.ts +455 -9
  211. package/src/__tests__/components/BaseRadioGroup.test.ts +25 -0
  212. package/src/__tests__/components/BaseSelect.test.ts +21 -0
  213. package/src/__tests__/components/BaseTabs.test.ts +25 -0
  214. package/src/__tests__/components/BatchProgressList.test.ts +52 -0
  215. package/src/__tests__/components/BioTemplateExperimentWorkspaceView.test.ts +159 -0
  216. package/src/__tests__/components/BioTemplatePackWorkspaceView.test.ts +175 -0
  217. package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +306 -0
  218. package/src/__tests__/components/BioTemplateRenderer.test.ts +71 -0
  219. package/src/__tests__/components/Breadcrumb.test.ts +23 -0
  220. package/src/__tests__/components/CalendarGridPanel.test.ts +36 -0
  221. package/src/__tests__/components/ComponentBindingRenderer.test.ts +161 -0
  222. package/src/__tests__/components/ConcentrationInput.test.ts +45 -0
  223. package/src/__tests__/components/ControlWorkspaceView.test.ts +1102 -0
  224. package/src/__tests__/components/DataFrame.test.ts +11 -0
  225. package/src/__tests__/components/DatePicker.test.ts +45 -0
  226. package/src/__tests__/components/DateTimePicker.test.ts +48 -0
  227. package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +185 -0
  228. package/src/__tests__/components/DropdownButton.test.ts +23 -0
  229. package/src/__tests__/components/EmptyState.test.ts +23 -0
  230. package/src/__tests__/components/ExperimentPopover.test.ts +56 -0
  231. package/src/__tests__/components/FormBuilder.test.ts +296 -0
  232. package/src/__tests__/components/GroupAssigner.test.ts +30 -0
  233. package/src/__tests__/components/MultiSelect.test.ts +48 -0
  234. package/src/__tests__/components/PluginWorkspaceView.test.ts +548 -0
  235. package/src/__tests__/components/ProtocolStepEditor.test.ts +33 -0
  236. package/src/__tests__/components/ReagentList.test.ts +82 -0
  237. package/src/__tests__/components/SampleHierarchyTree.test.ts +53 -0
  238. package/src/__tests__/components/SampleSelector.test.ts +60 -0
  239. package/src/__tests__/components/SegmentedControl.test.ts +24 -0
  240. package/src/__tests__/components/SettingsModal.test.ts +296 -0
  241. package/src/__tests__/components/TagsInput.test.ts +75 -0
  242. package/src/__tests__/components/ThemeToggle.test.ts +47 -0
  243. package/src/__tests__/components/TimePicker.test.ts +38 -0
  244. package/src/__tests__/composables/experiment-utils.test.ts +30 -0
  245. package/src/__tests__/composables/useApi.test.ts +30 -0
  246. package/src/__tests__/composables/useAppExperiment.test.ts +100 -1
  247. package/src/__tests__/composables/useBioTemplatePackWorkspace.test.ts +125 -0
  248. package/src/__tests__/composables/useBioTemplatePresetWorkspace.test.ts +199 -0
  249. package/src/__tests__/composables/useBioTemplateWorkspace.test.ts +104 -0
  250. package/src/__tests__/composables/useCalendarGrid.test.ts +38 -0
  251. package/src/__tests__/composables/useControlSchema.test.ts +1033 -0
  252. package/src/__tests__/composables/useDebouncedWatch.test.ts +93 -0
  253. package/src/__tests__/composables/useDropdownState.test.ts +95 -0
  254. package/src/__tests__/composables/useEventListener.test.ts +116 -0
  255. package/src/__tests__/composables/useExpansionSet.test.ts +62 -0
  256. package/src/__tests__/composables/useExperimentData.test.ts +4 -0
  257. package/src/__tests__/composables/useExperimentSave.test.ts +203 -8
  258. package/src/__tests__/composables/useExperimentSelector.test.ts +164 -0
  259. package/src/__tests__/composables/useForm.test.ts +58 -0
  260. package/src/__tests__/composables/useFormBuilder.test.ts +77 -0
  261. package/src/__tests__/composables/useGroupAssignment.test.ts +73 -0
  262. package/src/__tests__/composables/useListSelection.test.ts +66 -0
  263. package/src/__tests__/composables/usePluginClient.test.ts +541 -0
  264. package/src/__tests__/composables/usePluginConfig.test.ts +5 -0
  265. package/src/__tests__/composables/useRequestSyncState.test.ts +92 -0
  266. package/src/__tests__/composables/useSampleGroups.test.ts +66 -0
  267. package/src/__tests__/composables/useSelectionLimit.test.ts +41 -0
  268. package/src/__tests__/composables/useSortedItems.test.ts +87 -0
  269. package/src/__tests__/composables/useTemplateCollection.test.ts +147 -0
  270. package/src/__tests__/composables/useTextSearch.test.ts +55 -0
  271. package/src/__tests__/composables/useTheme.test.ts +91 -0
  272. package/src/__tests__/composables/useTimeUtils.test.ts +35 -0
  273. package/src/__tests__/docs/frontendDocsCatalog.test.ts +324 -0
  274. package/src/__tests__/fixtures/templates/dose-response.json +81 -0
  275. package/src/__tests__/fixtures/templates/plate-map.json +54 -0
  276. package/src/__tests__/fixtures/templates/qpcr-plate.json +96 -0
  277. package/src/__tests__/fixtures/templates/sample-sheet.json +71 -0
  278. package/src/__tests__/templates/templates.test.ts +1055 -0
  279. package/src/components/AppAvatarMenu.vue +15 -69
  280. package/src/components/AppLayout.story.vue +64 -25
  281. package/src/components/AppLayout.vue +83 -2
  282. package/src/components/AppPluginSwitcher.vue +41 -145
  283. package/src/components/AppSidebar.story.vue +203 -1
  284. package/src/components/AppSidebar.vue +320 -25
  285. package/src/components/{ToastNotification.story.vue → AppToastContainer.story.vue} +6 -6
  286. package/src/components/{ToastNotification.vue → AppToastContainer.vue} +1 -1
  287. package/src/components/AppTopBar.story.vue +7 -33
  288. package/src/components/AppTopBar.vue +104 -300
  289. package/src/components/BaseModal.vue +3 -5
  290. package/src/components/BaseRadioGroup.vue +7 -3
  291. package/src/components/BaseSelect.vue +11 -7
  292. package/src/components/BaseTabs.vue +6 -4
  293. package/src/components/BatchProgressList.vue +5 -8
  294. package/src/components/BioTemplateExperimentWorkspaceView.story.vue +123 -0
  295. package/src/components/BioTemplateExperimentWorkspaceView.vue +343 -0
  296. package/src/components/BioTemplatePackWorkspaceView.story.vue +107 -0
  297. package/src/components/BioTemplatePackWorkspaceView.vue +177 -0
  298. package/src/components/BioTemplatePresetWorkspaceView.story.vue +163 -0
  299. package/src/components/BioTemplatePresetWorkspaceView.vue +401 -0
  300. package/src/components/BioTemplateRenderer.story.vue +57 -0
  301. package/src/components/BioTemplateRenderer.vue +57 -0
  302. package/src/components/Breadcrumb.vue +14 -8
  303. package/src/components/ComponentBindingRenderer.story.vue +57 -0
  304. package/src/components/ComponentBindingRenderer.vue +308 -0
  305. package/src/components/ConcentrationInput.vue +27 -64
  306. package/src/components/ControlWorkspaceView.story.vue +347 -0
  307. package/src/components/ControlWorkspaceView.vue +378 -0
  308. package/src/components/DataFrame.vue +34 -50
  309. package/src/components/DatePicker.vue +59 -192
  310. package/src/components/DateTimePicker.vue +50 -171
  311. package/src/components/DoseDesignWorkspaceView.story.vue +77 -0
  312. package/src/components/DoseDesignWorkspaceView.vue +255 -0
  313. package/src/components/DropdownButton.vue +14 -32
  314. package/src/components/EmptyState.vue +4 -2
  315. package/src/components/ExperimentPopover.vue +7 -28
  316. package/src/components/ExperimentSelectorModal.vue +6 -5
  317. package/src/components/FormBuilder.story.vue +190 -0
  318. package/src/components/FormBuilder.vue +124 -27
  319. package/src/components/GroupAssigner.vue +24 -56
  320. package/src/components/MultiSelect.vue +17 -12
  321. package/src/components/PlateMapEditor.vue +3 -8
  322. package/src/components/PluginIcon.vue +2 -22
  323. package/src/components/PluginWorkspaceView.story.vue +334 -0
  324. package/src/components/PluginWorkspaceView.vue +708 -0
  325. package/src/components/ProtocolStepEditor.vue +13 -22
  326. package/src/components/ReagentList.vue +25 -33
  327. package/src/components/SampleHierarchyTree.vue +12 -23
  328. package/src/components/SampleSelector.vue +42 -122
  329. package/src/components/SegmentedControl.vue +7 -3
  330. package/src/components/SettingsModal.story.vue +88 -1
  331. package/src/components/SettingsModal.vue +120 -29
  332. package/src/components/TagsInput.vue +29 -14
  333. package/src/components/ThemeToggle.vue +9 -7
  334. package/src/components/TimePicker.vue +19 -41
  335. package/src/components/Tooltip.vue +7 -12
  336. package/src/components/WellPlate.vue +6 -12
  337. package/src/components/index.ts +9 -8
  338. package/src/components/internal/ActionItemInternal.vue +82 -0
  339. package/src/components/internal/AppPageSelectorInternal.vue +128 -0
  340. package/src/components/internal/AppPillNavInternal.vue +194 -0
  341. package/src/components/internal/CalendarGridPanelInternal.vue +120 -0
  342. package/src/components/{FormFieldRenderer.vue → internal/FormFieldRendererInternal.vue} +4 -12
  343. package/src/components/{FormSection.vue → internal/FormSectionRenderer.vue} +6 -18
  344. package/src/components/{WellEditPopup.vue → internal/WellEditPopupInternal.vue} +5 -10
  345. package/src/composables/experiment-utils.ts +26 -0
  346. package/src/composables/index.ts +229 -3
  347. package/src/composables/platformContextHelpers.ts +74 -0
  348. package/src/composables/useApi.ts +9 -2
  349. package/src/composables/useAppExperiment.ts +85 -13
  350. package/src/composables/useBioTemplateComponents.ts +105 -0
  351. package/src/composables/useBioTemplateControls.ts +41 -0
  352. package/src/composables/useBioTemplatePackWorkspace.ts +185 -0
  353. package/src/composables/useBioTemplatePresetWorkspace.ts +326 -0
  354. package/src/composables/useBioTemplateWorkspace.ts +141 -0
  355. package/src/composables/useCalendarGrid.ts +140 -0
  356. package/src/composables/useControlSchema.ts +1362 -0
  357. package/src/composables/useDebouncedWatch.ts +119 -0
  358. package/src/composables/useDropdownState.ts +83 -0
  359. package/src/composables/useEventListener.ts +111 -0
  360. package/src/composables/useExpansionSet.ts +117 -0
  361. package/src/composables/useExperimentData.ts +20 -11
  362. package/src/composables/useExperimentSave.ts +202 -50
  363. package/src/composables/useExperimentSelector.ts +86 -72
  364. package/src/composables/useForm.ts +49 -4
  365. package/src/composables/useFormBuilder.ts +93 -42
  366. package/src/composables/useGroupAssignment.ts +148 -0
  367. package/src/composables/useListSelection.ts +158 -0
  368. package/src/composables/usePluginClient.ts +466 -0
  369. package/src/composables/usePluginConfig.ts +34 -13
  370. package/src/composables/useRequestSyncState.ts +126 -0
  371. package/src/composables/useSampleGroups.ts +126 -0
  372. package/src/composables/useSelectionLimit.ts +57 -0
  373. package/src/composables/useSortedItems.ts +118 -0
  374. package/src/composables/useTemplateCollection.ts +229 -0
  375. package/src/composables/useTextSearch.ts +60 -0
  376. package/src/composables/useTheme.ts +2 -28
  377. package/src/composables/useTimeUtils.ts +26 -2
  378. package/src/composables/useWellPlateEditor.ts +13 -9
  379. package/src/index.ts +11 -348
  380. package/src/install.ts +11 -4
  381. package/src/stores/settings.ts +13 -9
  382. package/src/styles/components/app-layout.css +82 -0
  383. package/src/styles/components/app-page-selector.css +23 -0
  384. package/src/styles/components/app-pill-nav.css +77 -0
  385. package/src/styles/components/app-sidebar.css +119 -0
  386. package/src/styles/components/app-top-bar.css +0 -201
  387. package/src/styles/components/concentration-input.css +3 -142
  388. package/src/styles/components/empty-state.css +0 -16
  389. package/src/styles/components/theme-toggle.css +3 -66
  390. package/src/styles/index.css +0 -2
  391. package/src/templates/adapters.ts +785 -0
  392. package/src/templates/builders.ts +2149 -0
  393. package/src/templates/catalog.ts +245 -0
  394. package/src/templates/componentBindings.ts +653 -0
  395. package/src/templates/controlSchemas.ts +718 -0
  396. package/src/templates/index.ts +318 -0
  397. package/src/templates/lookup.ts +18 -0
  398. package/src/templates/packs.ts +156 -0
  399. package/src/templates/presets.ts +146 -0
  400. package/src/templates/types.ts +668 -0
  401. package/src/types/components.ts +39 -27
  402. package/src/types/form-builder.ts +7 -2
  403. package/src/types/index.ts +13 -3
  404. package/src/types/platform.ts +7 -1
  405. package/src/utils/formModelSync.ts +52 -0
  406. package/src/utils/items.ts +28 -0
  407. package/src/utils/options.ts +23 -0
  408. package/src/utils/pluginIcon.ts +30 -0
  409. package/dist/__tests__/composables/usePluginApi.test.d.ts +0 -13
  410. package/dist/auth-DsI0rQ7_.js.map +0 -1
  411. package/dist/components/GroupingModal.vue.d.ts +0 -12
  412. package/dist/components-_XqPEhP9.js.map +0 -1
  413. package/dist/composables/usePluginApi.d.ts +0 -29
  414. package/dist/composables-tiZqLu1M.js.map +0 -1
  415. package/dist/useScheduleDrag-CA9sGNJG.js +0 -7181
  416. package/dist/useScheduleDrag-CA9sGNJG.js.map +0 -1
  417. package/src/__tests__/composables/usePluginApi.test.ts +0 -81
  418. package/src/components/AppPageSelector.vue +0 -159
  419. package/src/components/AppPillNav.vue +0 -66
  420. package/src/components/GroupingModal.story.vue +0 -52
  421. package/src/components/GroupingModal.vue +0 -422
  422. package/src/components/SettingsButton.story.vue +0 -58
  423. package/src/components/SettingsButton.vue +0 -76
  424. package/src/composables/usePluginApi.ts +0 -39
  425. package/src/styles/components/grouping-modal.css +0 -323
  426. package/src/styles/components/settings-button.css +0 -94
  427. /package/dist/components/{ToastNotification.vue.d.ts → AppToastContainer.vue.d.ts} +0 -0
@@ -0,0 +1,1055 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ assertTemplateEnvelope,
4
+ bioTemplatePresetControlValuesToOptions,
5
+ createBioTemplatePackCollection,
6
+ createBioTemplatePresetCollection,
7
+ createBioTemplatePresetCollectionFromControls,
8
+ createAssayMatrixTemplate,
9
+ createCalibrationCurveTemplate,
10
+ createDoseResponseTemplate,
11
+ createElisaAssayCollection,
12
+ createFlowCytometryAssayCollection,
13
+ createFlowCytometryPanelTemplate,
14
+ createInstrumentRunTemplate,
15
+ createLcmsBatchCollection,
16
+ createPlateMapTemplate,
17
+ createProtocolStepsTemplate,
18
+ createQpcrExpressionCollection,
19
+ createQpcrPlateTemplate,
20
+ createReagentListTemplate,
21
+ createSampleSheetTemplate,
22
+ createSamplePrepTemplate,
23
+ createDefaultBioTemplate,
24
+ createTemplateCollection,
25
+ createTimeCourseTemplate,
26
+ createWellPlateScreenCollection,
27
+ createWesternBlotAssayCollection,
28
+ ensureTemplateEnvelope,
29
+ ensureTemplateFromCollection,
30
+ extractTemplateCollection,
31
+ getBioTemplateComponentBindings,
32
+ getBioTemplateComponentProps,
33
+ getBioTemplateControlDefaults,
34
+ getBioTemplateControlSchema,
35
+ getBioTemplatePackInfo,
36
+ getBioTemplatePresetInfo,
37
+ getBioTemplateInfo,
38
+ listBioTemplateCatalog,
39
+ listBioTemplateComponentBindings,
40
+ listBioTemplateControlSchemas,
41
+ listBioTemplatePacks,
42
+ listBioTemplatePresets,
43
+ searchBioTemplatePacks,
44
+ searchBioTemplateCatalog,
45
+ searchBioTemplatePresets,
46
+ toBioTemplateComponentBindings,
47
+ toBioTemplateComponentBindingsById,
48
+ toBioTemplateComponentImports,
49
+ toBioTemplateComponentProps,
50
+ toBioTemplateComponentPropsByComponent,
51
+ toBioTemplateComponentPropsById,
52
+ toBioTemplateComponentSnippets,
53
+ toBioTemplateComponentUsage,
54
+ toAssayMatrixColumns,
55
+ toAssayMatrixDataFrame,
56
+ toAssayMatrixRows,
57
+ toCalibrationCurveDataFrame,
58
+ toCalibrationCurveRows,
59
+ toDoseConditions,
60
+ toDoseLayoutState,
61
+ toFlowPanelDataFrame,
62
+ toFlowPanelRows,
63
+ toInstrumentRunDataFrame,
64
+ toInstrumentRunRows,
65
+ toPlateMapEditorState,
66
+ toProtocolDataFrame,
67
+ toProtocolSteps,
68
+ toQpcrDataFrame,
69
+ toQpcrRows,
70
+ toQpcrWellPlateWells,
71
+ toReagentDataFrame,
72
+ toReagentListItems,
73
+ toSampleColumns,
74
+ toSampleDataFrame,
75
+ toSampleOptions,
76
+ toSamplePrepDataFrame,
77
+ toSamplePrepRows,
78
+ toSampleRows,
79
+ toTemplateDataFrame,
80
+ toTimeCourseDataFrame,
81
+ toTimeCourseRows,
82
+ toTimeCourseSteps,
83
+ toWellPlateWells,
84
+ TEMPLATE_COLLECTION_KEY,
85
+ } from '../../templates'
86
+ import bioTemplateCatalogContract from '../../../../bio-template-catalog.contract.json'
87
+ import bioTemplatePacksContract from '../../../../bio-template-packs.contract.json'
88
+ import bioTemplatePresetsContract from '../../../../bio-template-presets.contract.json'
89
+ import type {
90
+ DoseResponseTemplate,
91
+ FlowCytometryPanelTemplate,
92
+ InstrumentRunTemplate,
93
+ PlateMapTemplate,
94
+ ProtocolStepsTemplate,
95
+ QpcrPlateTemplate,
96
+ ReagentListTemplate,
97
+ SampleSheetTemplate,
98
+ } from '../../templates'
99
+ import doseResponseFixture from '../fixtures/templates/dose-response.json'
100
+ import plateMapFixture from '../fixtures/templates/plate-map.json'
101
+ import qpcrPlateFixture from '../fixtures/templates/qpcr-plate.json'
102
+ import sampleSheetFixture from '../fixtures/templates/sample-sheet.json'
103
+
104
+ describe('bio data templates', () => {
105
+ it('exposes a discoverable template catalog', () => {
106
+ const catalog = listBioTemplateCatalog()
107
+
108
+ expect(catalog.map(entry => entry.name)).toEqual([
109
+ 'plate-map',
110
+ 'sample-sheet',
111
+ 'sample-prep',
112
+ 'dose-response',
113
+ 'calibration-curve',
114
+ 'time-course',
115
+ 'protocol-steps',
116
+ 'assay-matrix',
117
+ 'reagent-list',
118
+ 'flow-cytometry-panel',
119
+ 'instrument-run',
120
+ 'qpcr-plate',
121
+ ])
122
+ expect(getBioTemplateInfo('wellplate')?.template_id).toBe('plate-map')
123
+ expect(getBioTemplateInfo('sample preparation')?.template_id).toBe('sample-prep')
124
+ expect(getBioTemplateInfo('dose design')?.template_id).toBe('dose-response')
125
+ expect(getBioTemplateInfo('dose-design')?.template_id).toBe('dose-response')
126
+ expect(getBioTemplateInfo('dose calculator')?.template_id).toBe('dose-response')
127
+ expect(getBioTemplateInfo('dose_calculator')?.template_id).toBe('dose-response')
128
+ expect(getBioTemplateInfo('protocol')?.template_id).toBe('protocol-steps')
129
+ expect(getBioTemplateInfo('standard curve')?.template_id).toBe('calibration-curve')
130
+ expect(getBioTemplateInfo('omics matrix')?.template_id).toBe('assay-matrix')
131
+ expect(getBioTemplateInfo('antibodies')?.template_id).toBe('reagent-list')
132
+ expect(getBioTemplateInfo('facs panel')?.template_id).toBe('flow-cytometry-panel')
133
+ expect(getBioTemplateInfo('run queue')?.template_id).toBe('instrument-run')
134
+ expect(getBioTemplateInfo('gene expression')?.template_id).toBe('qpcr-plate')
135
+ expect(searchBioTemplateCatalog('DoseCalculator')[0].name).toBe('dose-response')
136
+ })
137
+
138
+ it('matches the shared Python/frontend catalog contract', () => {
139
+ expect(listBioTemplateCatalog()).toEqual(bioTemplateCatalogContract)
140
+ })
141
+
142
+ it('exposes discoverable template collection packs', () => {
143
+ const packs = listBioTemplatePacks()
144
+
145
+ expect(packs.map(entry => entry.name)).toEqual([
146
+ 'cell-culture-screen',
147
+ 'omics-assay',
148
+ 'longitudinal-study',
149
+ 'molecular-assay',
150
+ ])
151
+ expect(getBioTemplatePackInfo('cell culture')?.templates).toEqual([
152
+ 'sample-sheet',
153
+ 'sample-prep',
154
+ 'plate-map',
155
+ 'reagent-list',
156
+ 'protocol-steps',
157
+ 'dose-response',
158
+ 'flow-cytometry-panel',
159
+ ])
160
+ expect(getBioTemplatePackInfo('metabolomics')?.name).toBe('omics-assay')
161
+ expect(getBioTemplatePackInfo('longitudinal')?.init_command).toBe('mint init --template longitudinal-study')
162
+ expect(getBioTemplatePackInfo('gene expression')?.templates.at(-1)).toBe('qpcr-plate')
163
+ expect(getBioTemplatePackInfo('gene-expression')?.templates.at(-1)).toBe('qpcr-plate')
164
+ expect(searchBioTemplatePacks('WellPlate').map(entry => entry.name)).toEqual([
165
+ 'cell-culture-screen',
166
+ 'omics-assay',
167
+ 'molecular-assay',
168
+ ])
169
+ })
170
+
171
+ it('matches the shared Python/frontend pack contract', () => {
172
+ expect(listBioTemplatePacks()).toEqual(bioTemplatePacksContract)
173
+ })
174
+
175
+ it('creates default template collections from curated packs', () => {
176
+ const plate = createDefaultBioTemplate('wellplate') as PlateMapTemplate
177
+ const pack = createBioTemplatePackCollection('cell culture')
178
+ const templates = extractTemplateCollection(pack)
179
+
180
+ expect(plate.template_id).toBe('plate-map')
181
+ expect(pack.metadata?.pack).toBe('cell-culture-screen')
182
+ expect(Object.keys(templates)).toEqual([
183
+ 'sample-sheet',
184
+ 'sample-prep',
185
+ 'plate-map',
186
+ 'reagent-list',
187
+ 'protocol-steps',
188
+ 'dose-response',
189
+ 'flow-cytometry-panel',
190
+ ])
191
+ expect(toPlateMapEditorState(templates['plate-map'] as PlateMapTemplate).samples).toHaveLength(2)
192
+ expect(toDoseConditions(templates['dose-response'] as DoseResponseTemplate)[0].label).toBe('Compound A')
193
+ expect(getBioTemplateControlSchema('cell-culture-screen')?.sampleSheetSampleCount.default).toBe(2)
194
+ expect(getBioTemplateComponentBindings('cell-culture-screen').map(binding => binding.component)).toContain('DoseCalculator')
195
+ expect(() => createBioTemplatePackCollection('missing-pack')).toThrow("Unknown template pack 'missing-pack'.")
196
+ })
197
+
198
+ it('exposes discoverable template collection presets', () => {
199
+ const presets = listBioTemplatePresets()
200
+
201
+ expect(presets.map(entry => entry.name)).toEqual([
202
+ 'wellplate-screen',
203
+ 'qpcr-expression',
204
+ 'lcms-batch',
205
+ 'elisa-assay',
206
+ 'flow-cytometry-assay',
207
+ 'western-blot-assay',
208
+ ])
209
+ expect(getBioTemplatePresetInfo('wellplate')?.name).toBe('wellplate-screen')
210
+ expect(getBioTemplatePresetInfo('dose planner')?.name).toBe('wellplate-screen')
211
+ expect(getBioTemplatePresetInfo('dose-planner')?.name).toBe('wellplate-screen')
212
+ expect(getBioTemplatePresetInfo('gene expression')?.templates).toEqual([
213
+ 'sample-sheet',
214
+ 'qpcr-plate',
215
+ ])
216
+ expect(searchBioTemplatePresets('run queue')[0].name).toBe('lcms-batch')
217
+ expect(getBioTemplatePresetInfo('immunoassay')?.templates).toEqual([
218
+ 'plate-map',
219
+ 'sample-sheet',
220
+ 'calibration-curve',
221
+ 'assay-matrix',
222
+ ])
223
+ expect(getBioTemplatePresetInfo('facs')?.templates).toEqual([
224
+ 'sample-sheet',
225
+ 'flow-cytometry-panel',
226
+ 'assay-matrix',
227
+ ])
228
+ expect(getBioTemplatePresetInfo('immunoblot')?.templates).toEqual([
229
+ 'sample-sheet',
230
+ 'reagent-list',
231
+ 'protocol-steps',
232
+ 'assay-matrix',
233
+ ])
234
+ })
235
+
236
+ it('matches the shared Python/frontend preset contract', () => {
237
+ expect(listBioTemplatePresets()).toEqual(bioTemplatePresetsContract)
238
+ })
239
+
240
+ it('prefers generic dataframe adapters in catalog entries', () => {
241
+ const dataframeTemplates = listBioTemplateCatalog().filter(entry =>
242
+ entry.components.includes('DataFrame')
243
+ )
244
+
245
+ expect(dataframeTemplates.map(entry => entry.name)).toEqual([
246
+ 'sample-sheet',
247
+ 'sample-prep',
248
+ 'calibration-curve',
249
+ 'time-course',
250
+ 'protocol-steps',
251
+ 'assay-matrix',
252
+ 'reagent-list',
253
+ 'flow-cytometry-panel',
254
+ 'instrument-run',
255
+ 'qpcr-plate',
256
+ ])
257
+ for (const entry of dataframeTemplates) {
258
+ expect(entry.frontend_adapters[0]).toBe('toTemplateDataFrame')
259
+ expect(entry.frontend_import).toContain('toTemplateDataFrame')
260
+ }
261
+ })
262
+
263
+ it('exposes control schemas for built-in templates and presets', () => {
264
+ const schemas = listBioTemplateControlSchemas()
265
+ const plateSchema = getBioTemplateControlSchema('wellplate')
266
+ const presetSchema = getBioTemplateControlSchema('wellplate-screen')
267
+ const defaults = getBioTemplateControlDefaults('lcms-batch')
268
+ const elisaDefaults = getBioTemplateControlDefaults('elisa-assay')
269
+ const flowDefaults = getBioTemplateControlDefaults('flow-cytometry-assay')
270
+ const westernDefaults = getBioTemplateControlDefaults('western-blot-assay')
271
+
272
+ expect(Object.keys(schemas)).toContain('plate-map')
273
+ expect(plateSchema?.plateFormat.default).toBe(96)
274
+ expect(presetSchema?.replicates.default).toBe(3)
275
+ expect(defaults.instrument).toBe('LC-MS')
276
+ expect(defaults.featureNames).toEqual(['Glucose', 'Lactate'])
277
+ expect(elisaDefaults.analyte).toBe('Analyte')
278
+ expect(elisaDefaults.standardConcentrations).toEqual(['1000', '100', '10', '1'])
279
+ expect(flowDefaults.markers).toEqual(['CD3', 'CD4', 'CD8'])
280
+ expect(flowDefaults.includeDefaultControls).toBe(true)
281
+ expect(westernDefaults.targets).toEqual(['Target protein'])
282
+ expect(westernDefaults.loadingControl).toBe('ACTB')
283
+ })
284
+
285
+ it('exposes component bindings for template-driven frontend rendering', () => {
286
+ const bindings = listBioTemplateComponentBindings()
287
+ const wellplateBindings = getBioTemplateComponentBindings('wellplate-screen')
288
+ const qpcrBindings = getBioTemplateComponentBindings('gene expression')
289
+ const elisaBindings = getBioTemplateComponentBindings('elisa-assay')
290
+ const flowBindings = getBioTemplateComponentBindings('flow-cytometry-assay')
291
+ const westernBindings = getBioTemplateComponentBindings('western-blot-assay')
292
+ const wellplateImports = toBioTemplateComponentImports('wellplate-screen')
293
+
294
+ expect(bindings['plate-map'].map(binding => binding.component)).toEqual(['PlateMapEditor', 'WellPlate'])
295
+ expect(wellplateBindings.map(binding => binding.component)).toContain('DoseCalculator')
296
+ expect(wellplateBindings.map(binding => binding.component)).toContain('WellPlate')
297
+ expect(qpcrBindings.map(binding => binding.component)).toContain('WellPlate')
298
+ expect(qpcrBindings.map(binding => binding.component)).toContain('DataFrame')
299
+ expect(elisaBindings.map(binding => binding.component)).toContain('PlateMapEditor')
300
+ expect(elisaBindings.map(binding => binding.component)).toContain('DataFrame')
301
+ expect(flowBindings.map(binding => binding.template_id)).toEqual([
302
+ 'sample-sheet',
303
+ 'sample-sheet',
304
+ 'flow-cytometry-panel',
305
+ 'assay-matrix',
306
+ ])
307
+ expect(westernBindings.map(binding => binding.component)).toContain('ReagentList')
308
+ expect(westernBindings.map(binding => binding.component)).toContain('ExperimentTimeline')
309
+ expect(wellplateImports).toEqual([
310
+ {
311
+ importPath: '@morscherlab/mint-sdk',
312
+ components: ['PlateMapEditor', 'WellPlate', 'DataFrame', 'SampleSelector', 'DoseCalculator'],
313
+ statement: "import { PlateMapEditor, WellPlate, DataFrame, SampleSelector, DoseCalculator } from '@morscherlab/mint-sdk'",
314
+ },
315
+ ])
316
+ })
317
+
318
+ it('creates plate-map envelopes and adapts them to plate editor state', () => {
319
+ const template = createPlateMapTemplate({
320
+ name: 'Drug screen plate',
321
+ samples: ['DMSO', 'Drug A'],
322
+ })
323
+ const state = toPlateMapEditorState(template)
324
+
325
+ expect(template.template_id).toBe('plate-map')
326
+ expect(state.activePlateId).toBe('plate-1')
327
+ expect(state.plates[0].format).toBe(96)
328
+ expect(state.samples[1].id).toBe('drug-a')
329
+ })
330
+
331
+ it('loads the shared plate-map fixture', () => {
332
+ const state = toPlateMapEditorState(plateMapFixture as PlateMapTemplate)
333
+ const wells = toWellPlateWells(plateMapFixture as PlateMapTemplate)
334
+ const componentProps = toBioTemplateComponentProps(plateMapFixture as PlateMapTemplate)
335
+ const componentBindings = toBioTemplateComponentBindings(plateMapFixture as PlateMapTemplate)
336
+ const componentPropsById = toBioTemplateComponentPropsById(plateMapFixture as PlateMapTemplate)
337
+ const componentBindingsById = toBioTemplateComponentBindingsById(plateMapFixture as PlateMapTemplate)
338
+ const wellPlate = componentProps.find(binding => binding.component === 'WellPlate')
339
+ const wellPlateBinding = componentBindings.find(binding => binding.component === 'WellPlate')
340
+
341
+ expect(state.plates[0].name).toBe('Drug screen plate')
342
+ expect(wells.A2.sampleType).toBe('drug-a')
343
+ expect(wellPlate?.propsObject.format).toBe(96)
344
+ expect((wellPlate?.propsObject.wells as Record<string, { sampleType?: string }>).A2.sampleType).toBe('drug-a')
345
+ expect(wellPlateBinding?.props.format).toBe(96)
346
+ expect(wellPlateBinding?.propNames).toContain('wells')
347
+ expect(componentPropsById['plate-map:WellPlate']).toEqual(wellPlate?.propsObject)
348
+ expect(componentBindingsById['plate-map:WellPlate'].component).toBe('WellPlate')
349
+ expect(componentBindingsById['plate-map:WellPlate'].props).toEqual(wellPlate?.propsObject)
350
+ expect((componentPropsById['plate-map:WellPlate'].wells as Record<string, { sampleType?: string }>).A2.sampleType).toBe('drug-a')
351
+ })
352
+
353
+ it('returns component props keyed by stable template component id', () => {
354
+ const collection = createWellPlateScreenCollection({
355
+ samples: ['Control', 'Treatment'],
356
+ compounds: { 'Drug A': [10, 1] },
357
+ })
358
+ const propsById = toBioTemplateComponentPropsById(collection)
359
+ const bindingsById = toBioTemplateComponentBindingsById(collection)
360
+ const propsByComponent = toBioTemplateComponentPropsByComponent(collection)
361
+ const doseCalculatorProps = getBioTemplateComponentProps(collection, 'DoseCalculator')
362
+ const doseWellPlateProps = getBioTemplateComponentProps(collection, 'WellPlate', {
363
+ templateId: 'dose-response',
364
+ })
365
+
366
+ expect(Object.keys(propsById)).toEqual([
367
+ 'plate-map:PlateMapEditor',
368
+ 'plate-map:WellPlate',
369
+ 'sample-sheet:DataFrame',
370
+ 'sample-sheet:SampleSelector',
371
+ 'dose-response:DoseCalculator',
372
+ 'dose-response:WellPlate',
373
+ ])
374
+ expect(propsById['dose-response:DoseCalculator'].mode).toBe('serial')
375
+ expect(bindingsById['dose-response:DoseCalculator'].props.mode).toBe('serial')
376
+ expect(bindingsById['dose-response:DoseCalculator'].propNames).toContain('mode')
377
+ expect(propsById['plate-map:WellPlate'].wells).toBeDefined()
378
+ expect(propsById['dose-response:WellPlate'].wells).toBeDefined()
379
+ expect(propsByComponent.WellPlate).toHaveLength(2)
380
+ expect(propsByComponent.DoseCalculator[0].mode).toBe('serial')
381
+ expect(doseCalculatorProps?.mode).toBe('serial')
382
+ expect(doseWellPlateProps?.wells).toBeDefined()
383
+ expect(getBioTemplateComponentProps(collection, 'MissingWidget')).toBeUndefined()
384
+ })
385
+
386
+ it('creates ELISA assay preset collections with plate, standards, and readout matrix', () => {
387
+ const collection = createElisaAssayCollection({
388
+ samples: ['Control', 'Treatment'],
389
+ analyte: 'IL-6',
390
+ standardConcentrations: [1000, 100, 10, 1],
391
+ unit: 'pg/mL',
392
+ })
393
+ const templates = extractTemplateCollection(collection)
394
+ const componentPropsById = toBioTemplateComponentPropsById(collection)
395
+
396
+ expect(Object.keys(templates)).toEqual([
397
+ 'plate-map',
398
+ 'sample-sheet',
399
+ 'calibration-curve',
400
+ 'assay-matrix',
401
+ ])
402
+ expect(collection.metadata?.preset).toBe('elisa-assay')
403
+ expect(templates['calibration-curve'].data).toMatchObject({
404
+ analyte: 'IL-6',
405
+ model: 'four-parameter-logistic',
406
+ xUnit: 'pg/mL',
407
+ responseUnit: 'OD450',
408
+ })
409
+ expect(templates['assay-matrix'].data).toMatchObject({
410
+ features: [{ id: 'il-6', name: 'IL-6', type: 'protein', unit: 'OD450', metadata: {} }],
411
+ })
412
+ expect(componentPropsById['plate-map:WellPlate'].wells).toBeDefined()
413
+ expect(componentPropsById['calibration-curve:DataFrame'].data).toBeDefined()
414
+ expect(componentPropsById['assay-matrix:DataFrame'].columns).toBeDefined()
415
+ })
416
+
417
+ it('generates Vue snippets for concrete template component props', () => {
418
+ const snippets = toBioTemplateComponentSnippets(plateMapFixture as PlateMapTemplate, {
419
+ propsVariable: 'bindings',
420
+ })
421
+
422
+ expect(snippets.map(snippet => snippet.component)).toEqual(['PlateMapEditor', 'WellPlate'])
423
+ expect(snippets.map(snippet => snippet.propsName)).toEqual([
424
+ 'plateMapPlateMapEditorProps',
425
+ 'plateMapWellPlateProps',
426
+ ])
427
+ expect(snippets[0].propsExpression).toBe('bindings[0].propsObject')
428
+ expect(snippets[0].template).toBe('<PlateMapEditor v-bind="bindings[0].propsObject" />')
429
+ expect(snippets[1].template).toBe('<WellPlate v-bind="bindings[1].propsObject" />')
430
+ })
431
+
432
+ it('generates a Vue usage bundle for concrete template component props', () => {
433
+ const usage = toBioTemplateComponentUsage(plateMapFixture as PlateMapTemplate, {
434
+ propsVariable: 'bindings',
435
+ targetExpression: 'plateMap',
436
+ })
437
+
438
+ expect(usage.imports[0].statement).toBe(
439
+ "import { PlateMapEditor, WellPlate } from '@morscherlab/mint-sdk'"
440
+ )
441
+ expect(usage.componentProps).toHaveLength(2)
442
+ expect(usage.propsDeclarations).toEqual([
443
+ 'const plateMapPlateMapEditorProps = bindings[0].propsObject',
444
+ 'const plateMapWellPlateProps = bindings[1].propsObject',
445
+ ])
446
+ expect(usage.scriptSetup).toBe([
447
+ 'const bindings = toBioTemplateComponentProps(plateMap)',
448
+ 'const plateMapPlateMapEditorProps = bindings[0].propsObject',
449
+ 'const plateMapWellPlateProps = bindings[1].propsObject',
450
+ ].join('\n'))
451
+ expect(usage.template).toBe([
452
+ '<PlateMapEditor v-bind="plateMapPlateMapEditorProps" />',
453
+ '<WellPlate v-bind="plateMapWellPlateProps" />',
454
+ ].join('\n'))
455
+ })
456
+
457
+ it('generates a complete Vue SFC for concrete template component props', () => {
458
+ const usage = toBioTemplateComponentUsage(plateMapFixture as PlateMapTemplate)
459
+
460
+ expect(usage.scriptSetup).toContain(
461
+ 'const props = defineProps<{ templateCollection: BioTemplateEnvelope<unknown> | TemplateCollectionEnvelope }>()'
462
+ )
463
+ expect(usage.sfc).toContain("import { PlateMapEditor, WellPlate } from '@morscherlab/mint-sdk'")
464
+ expect(usage.sfc).toContain("import { toBioTemplateComponentProps } from '@morscherlab/mint-sdk/templates'")
465
+ expect(usage.sfc).toContain('defineProps<{ templateCollection: BioTemplateEnvelope<unknown> | TemplateCollectionEnvelope }>()')
466
+ expect(usage.sfc).toContain('<PlateMapEditor v-bind="plateMapPlateMapEditorProps" />')
467
+ expect(usage.sfc).toContain('<WellPlate v-bind="plateMapWellPlateProps" />')
468
+ expect(usage.sfc).toMatch(/^<script setup lang="ts">[\s\S]*<\/template>$/)
469
+ })
470
+
471
+ it('rejects mismatched template envelopes', () => {
472
+ expect(() => assertTemplateEnvelope(plateMapFixture, 'sample-sheet')).toThrow(
473
+ "Expected template_id 'sample-sheet'"
474
+ )
475
+ })
476
+
477
+ it('returns validated typed template envelopes', () => {
478
+ const template = ensureTemplateEnvelope<PlateMapTemplate>(plateMapFixture, 'plate-map')
479
+
480
+ expect(template.data.plates[0].id).toBe('plate-1')
481
+ expect(() => ensureTemplateEnvelope<SampleSheetTemplate>(plateMapFixture, 'sample-sheet')).toThrow(
482
+ "Expected template_id 'sample-sheet'"
483
+ )
484
+ })
485
+
486
+ it('builds and reads template collections', () => {
487
+ const plate = createPlateMapTemplate({
488
+ name: 'Drug screen plate',
489
+ samples: ['DMSO', 'Drug A'],
490
+ })
491
+ const sampleSheet = createSampleSheetTemplate({
492
+ samples: [
493
+ { sampleId: 'S001', name: 'Control 1', group: 'Control' },
494
+ ],
495
+ })
496
+ const collection = createTemplateCollection([plate, sampleSheet], { source: 'unit-test' })
497
+ const extracted = extractTemplateCollection(collection)
498
+ const typedPlate = ensureTemplateFromCollection<PlateMapTemplate>(collection, 'plate-map')
499
+
500
+ expect(Object.keys(collection[TEMPLATE_COLLECTION_KEY])).toEqual(['plate-map', 'sample-sheet'])
501
+ expect(collection.metadata?.source).toBe('unit-test')
502
+ expect(extracted['sample-sheet'].template_id).toBe('sample-sheet')
503
+ expect(typedPlate.data.plates[0].name).toBe('Drug screen plate')
504
+ expect(extractTemplateCollection(plate)['plate-map'].template_id).toBe('plate-map')
505
+ expect(() => ensureTemplateFromCollection<SampleSheetTemplate>(collection, 'sample-prep')).toThrow(
506
+ "Template 'sample-prep' was not found"
507
+ )
508
+ expect(() => extractTemplateCollection({ templates: { 'sample-sheet': plate } })).toThrow(
509
+ 'does not match envelope template_id'
510
+ )
511
+ expect(() => createTemplateCollection([plate, plate])).toThrow('Duplicate template_id')
512
+ })
513
+
514
+ it('creates ready-to-save template preset collections', () => {
515
+ const wellplate = createWellPlateScreenCollection({
516
+ samples: ['Control', 'Treatment'],
517
+ compounds: { 'Drug A': [10, 1, 0.1] },
518
+ unit: 'uM',
519
+ })
520
+ const qpcr = createQpcrExpressionCollection({
521
+ samples: [{ sampleId: 'S001', name: 'Control', group: 'Control' }],
522
+ targets: ['ACTB'],
523
+ })
524
+ const lcms = createLcmsBatchCollection({
525
+ samples: ['S001', 'S002'],
526
+ features: ['Glucose'],
527
+ instrument: 'LC-MS',
528
+ })
529
+ const flow = createFlowCytometryAssayCollection({
530
+ samples: ['Control', 'Treatment'],
531
+ markers: ['CD3', 'CD4'],
532
+ instrument: 'BD LSRFortessa',
533
+ })
534
+ const western = createWesternBlotAssayCollection({
535
+ samples: ['Control', 'Treatment'],
536
+ targets: ['pERK', 'ERK'],
537
+ loadingControl: 'ACTB',
538
+ })
539
+
540
+ const wellplateTemplates = extractTemplateCollection(wellplate)
541
+ const qpcrTemplates = extractTemplateCollection(qpcr)
542
+ const lcmsTemplates = extractTemplateCollection(lcms)
543
+ const flowTemplates = extractTemplateCollection(flow)
544
+ const westernTemplates = extractTemplateCollection(western)
545
+ const wellplateComponentProps = toBioTemplateComponentProps(wellplate)
546
+
547
+ expect(Object.keys(wellplateTemplates)).toEqual(['plate-map', 'sample-sheet', 'dose-response'])
548
+ expect(wellplate.metadata?.preset).toBe('wellplate-screen')
549
+ expect(toPlateMapEditorState(wellplateTemplates['plate-map'] as PlateMapTemplate).samples[1].id).toBe('treatment')
550
+ expect(toDoseConditions(wellplateTemplates['dose-response'] as DoseResponseTemplate)[0].label).toBe('Drug A')
551
+ expect(wellplateComponentProps.map(binding => binding.component)).toContain('PlateMapEditor')
552
+ expect(wellplateComponentProps.map(binding => binding.component)).toContain('DoseCalculator')
553
+ expect(Object.keys(qpcrTemplates)).toEqual(['sample-sheet', 'qpcr-plate'])
554
+ expect(toQpcrRows(qpcrTemplates['qpcr-plate'] as QpcrPlateTemplate)[0].sampleId).toBe('S001')
555
+ expect(Object.keys(lcmsTemplates)).toEqual(['sample-sheet', 'instrument-run', 'assay-matrix'])
556
+ expect(toInstrumentRunRows(lcmsTemplates['instrument-run'] as InstrumentRunTemplate)[0].kind).toBe('blank')
557
+ expect(toInstrumentRunRows(lcmsTemplates['instrument-run'] as InstrumentRunTemplate)[2].sampleId).toBe('s001')
558
+ expect(toTemplateDataFrame(lcmsTemplates['assay-matrix']).columns.map(column => column.key)).toContain('glucose')
559
+ expect(Object.keys(flowTemplates)).toEqual(['sample-sheet', 'flow-cytometry-panel', 'assay-matrix'])
560
+ expect(flow.metadata?.preset).toBe('flow-cytometry-assay')
561
+ expect(flowTemplates['flow-cytometry-panel'].data).toMatchObject({
562
+ instrument: 'BD LSRFortessa',
563
+ markers: [
564
+ { id: 'cd3', marker: 'CD3' },
565
+ { id: 'cd4', marker: 'CD4' },
566
+ ],
567
+ })
568
+ expect(toTemplateDataFrame(flowTemplates['assay-matrix']).columns.map(column => column.key)).toContain('cd4')
569
+ expect(Object.keys(westernTemplates)).toEqual(['sample-sheet', 'reagent-list', 'protocol-steps', 'assay-matrix'])
570
+ expect(western.metadata?.preset).toBe('western-blot-assay')
571
+ expect(toReagentDataFrame(westernTemplates['reagent-list'] as ReagentListTemplate).data[0].kind).toBe('antibody')
572
+ expect(toProtocolDataFrame(westernTemplates['protocol-steps'] as ProtocolStepsTemplate).data[2].name).toBe('Transfer to membrane')
573
+ expect(toTemplateDataFrame(westernTemplates['assay-matrix']).columns.map(column => column.key)).toContain('actb')
574
+
575
+ const aliasCollection = createBioTemplatePresetCollection('wellplate', {
576
+ samples: ['Control', 'Treatment'],
577
+ compounds: { 'Drug A': [10, 1] },
578
+ })
579
+ expect(aliasCollection.metadata?.preset).toBe('wellplate-screen')
580
+ expect(Object.keys(extractTemplateCollection(aliasCollection))).toEqual(['plate-map', 'sample-sheet', 'dose-response'])
581
+ expect(createBioTemplatePresetCollection('facs').metadata?.preset).toBe('flow-cytometry-assay')
582
+ expect(createBioTemplatePresetCollection('immunoblot').metadata?.preset).toBe('western-blot-assay')
583
+ expect(() => createBioTemplatePresetCollection('unknown-preset')).toThrow("Unknown template preset 'unknown-preset'.")
584
+ })
585
+
586
+ it('maps preset control values into template collection previews', () => {
587
+ const wellplateOptions = bioTemplatePresetControlValuesToOptions('wellplate-screen', {
588
+ sampleNames: 'Vehicle, Drug A, Drug B',
589
+ plateName: 'Custom screen',
590
+ plateFormat: '384',
591
+ unit: 'nM',
592
+ replicates: '2',
593
+ })
594
+ const wellplate = createBioTemplatePresetCollectionFromControls('wellplate-screen', {
595
+ sampleNames: 'Vehicle, Drug A, Drug B',
596
+ plateName: 'Custom screen',
597
+ plateFormat: '384',
598
+ unit: 'nM',
599
+ replicates: '2',
600
+ })
601
+ const wellplateTemplates = extractTemplateCollection(wellplate)
602
+ const plate = wellplateTemplates['plate-map'] as PlateMapTemplate
603
+ const dose = wellplateTemplates['dose-response'] as DoseResponseTemplate
604
+
605
+ expect(wellplateOptions.samples).toEqual(['Vehicle', 'Drug A', 'Drug B'])
606
+ expect(wellplateOptions.plateFormat).toBe(384)
607
+ expect(wellplateOptions.replicates).toBe(2)
608
+ expect(plate.data.plates[0].name).toBe('Custom screen')
609
+ expect(plate.data.plates[0].format).toBe(384)
610
+ expect(plate.data.samples.map(sample => sample.id)).toEqual(['vehicle', 'drug-a', 'drug-b'])
611
+ expect(dose.data.unit).toBe('nM')
612
+ expect(dose.data.replicates).toBe(2)
613
+
614
+ const qpcrOptions = bioTemplatePresetControlValuesToOptions('qpcr-expression', {
615
+ sampleNames: ['Control', 'Knockdown'],
616
+ targets: 'ACTB, MYC',
617
+ chemistry: 'taqman',
618
+ plateFormat: 384,
619
+ includeNoTemplateControls: false,
620
+ replicates: 2,
621
+ instrument: 'QuantStudio',
622
+ })
623
+ const qpcr = ensureTemplateFromCollection<QpcrPlateTemplate>(
624
+ createBioTemplatePresetCollectionFromControls('qpcr-expression', {
625
+ sampleNames: ['Control', 'Knockdown'],
626
+ targets: 'ACTB, MYC',
627
+ chemistry: 'taqman',
628
+ plateFormat: 384,
629
+ includeNoTemplateControls: false,
630
+ replicates: 2,
631
+ instrument: 'QuantStudio',
632
+ }),
633
+ 'qpcr-plate',
634
+ )
635
+
636
+ expect(qpcrOptions.targets).toEqual(['ACTB', 'MYC'])
637
+ expect(qpcrOptions.includeNoTemplateControls).toBe(false)
638
+ expect(qpcr.data.chemistry).toBe('taqman')
639
+ expect(qpcr.data.format).toBe(384)
640
+ expect(qpcr.data.instrument).toBe('QuantStudio')
641
+ expect(qpcr.data.reactions).toHaveLength(8)
642
+ expect(qpcr.data.reactions.some(reaction => reaction.controlKind === 'no-template')).toBe(false)
643
+
644
+ const lcmsOptions = bioTemplatePresetControlValuesToOptions('lcms-batch', {
645
+ sampleNames: 'S001, S002',
646
+ featureNames: 'Glucose, Pyruvate',
647
+ instrument: 'Orbitrap',
648
+ method: 'Polar metabolites',
649
+ includeQc: false,
650
+ })
651
+ const lcmsTemplates = extractTemplateCollection(
652
+ createBioTemplatePresetCollectionFromControls('lcms-batch', {
653
+ sampleNames: 'S001, S002',
654
+ featureNames: 'Glucose, Pyruvate',
655
+ instrument: 'Orbitrap',
656
+ method: 'Polar metabolites',
657
+ includeQc: false,
658
+ }),
659
+ )
660
+ const run = lcmsTemplates['instrument-run'] as InstrumentRunTemplate
661
+
662
+ expect(lcmsOptions.features).toEqual(['Glucose', 'Pyruvate'])
663
+ expect(lcmsOptions.includeQc).toBe(false)
664
+ expect(run.data.instrument).toBe('Orbitrap')
665
+ expect(run.data.methods[0].name).toBe('Polar metabolites')
666
+ expect(run.data.items.map(item => item.kind)).toEqual(['blank', 'sample', 'sample'])
667
+ expect(toTemplateDataFrame(lcmsTemplates['assay-matrix']).columns.map(column => column.key)).toContain('pyruvate')
668
+
669
+ const flowOptions = bioTemplatePresetControlValuesToOptions('flow-cytometry-assay', {
670
+ sampleNames: 'Control, Treatment',
671
+ markers: 'CD3, CD19',
672
+ featureNames: 'T cells, B cells',
673
+ instrument: 'Cytek Aurora',
674
+ includeDefaultControls: false,
675
+ })
676
+ const flowTemplates = extractTemplateCollection(
677
+ createBioTemplatePresetCollectionFromControls('flow-cytometry-assay', {
678
+ sampleNames: 'Control, Treatment',
679
+ markers: 'CD3, CD19',
680
+ featureNames: 'T cells, B cells',
681
+ instrument: 'Cytek Aurora',
682
+ includeDefaultControls: false,
683
+ }),
684
+ )
685
+ const flowPanel = flowTemplates['flow-cytometry-panel'] as FlowCytometryPanelTemplate
686
+
687
+ expect(flowOptions.markers).toEqual(['CD3', 'CD19'])
688
+ expect(flowOptions.features).toEqual(['T cells', 'B cells'])
689
+ expect(flowPanel.data.instrument).toBe('Cytek Aurora')
690
+ expect(flowPanel.data.controls).toEqual([])
691
+ expect(toTemplateDataFrame(flowTemplates['assay-matrix']).columns.map(column => column.key)).toContain('b-cells')
692
+
693
+ const westernOptions = bioTemplatePresetControlValuesToOptions('western-blot-assay', {
694
+ sampleNames: 'Control, Treatment',
695
+ targets: 'pERK, ERK',
696
+ loadingControl: 'ACTB',
697
+ })
698
+ const westernTemplates = extractTemplateCollection(
699
+ createBioTemplatePresetCollectionFromControls('western-blot-assay', {
700
+ sampleNames: 'Control, Treatment',
701
+ targets: 'pERK, ERK',
702
+ loadingControl: 'ACTB',
703
+ }),
704
+ )
705
+
706
+ expect(westernOptions.targets).toEqual(['pERK', 'ERK'])
707
+ expect(westernOptions.loadingControl).toBe('ACTB')
708
+ expect(toProtocolSteps(westernTemplates['protocol-steps'] as ProtocolStepsTemplate).map(step => step.name)).toContain('Run SDS-PAGE')
709
+ expect(toTemplateDataFrame(westernTemplates['assay-matrix']).columns.map(column => column.key)).toContain('actb')
710
+ })
711
+
712
+ it('creates sample-sheet templates and adapts rows, columns, and options', () => {
713
+ const template = createSampleSheetTemplate({
714
+ samples: [
715
+ {
716
+ sampleId: 'S001',
717
+ name: 'Control 1',
718
+ group: 'Control',
719
+ metadata: { cell_line: 'HCT116' },
720
+ },
721
+ {
722
+ sampleId: 'S002',
723
+ name: 'Control 2',
724
+ group: 'Control',
725
+ },
726
+ ],
727
+ columns: [
728
+ { id: 'sampleId', label: 'Sample ID', type: 'string', required: true, groupable: false },
729
+ { id: 'cell_line', label: 'Cell Line', type: 'string', required: false, groupable: true },
730
+ ],
731
+ })
732
+
733
+ expect(toSampleRows(template)[0].cell_line).toBe('HCT116')
734
+ expect(template.data.samples[1].metadata).toEqual({})
735
+ expect(toSampleColumns(template)[0].key).toBe('sampleId')
736
+ expect(toSampleColumns(template)[1].key).toBe('cell_line')
737
+ const sampleFrame = toSampleDataFrame(template)
738
+ expect(sampleFrame.rowKey).toBe('sampleId')
739
+ expect(sampleFrame.data[0].cell_line).toBe('HCT116')
740
+ expect(sampleFrame.columns.map(column => column.key)).toEqual(['sampleId', 'cell_line'])
741
+ expect(toTemplateDataFrame(template).rowKey).toBe('sampleId')
742
+ expect(toSampleOptions(template)[0].label).toBe('Control 1')
743
+ })
744
+
745
+ it('loads the shared sample-sheet fixture', () => {
746
+ const rows = toSampleRows(sampleSheetFixture as SampleSheetTemplate)
747
+ const columns = toSampleColumns(sampleSheetFixture as SampleSheetTemplate)
748
+ const options = toSampleOptions(sampleSheetFixture as SampleSheetTemplate)
749
+
750
+ expect(rows[1].group).toBe('Drug A')
751
+ expect(columns[0].key).toBe('sampleId')
752
+ expect(options[0].value).toBe('S001')
753
+ })
754
+
755
+ it('creates sample-prep templates and adapts prep steps', () => {
756
+ const template = createSamplePrepTemplate({
757
+ samples: ['S001', { name: 'Dilute S002', sourceSampleId: 'S002', type: 'dilution' }],
758
+ prepType: 'extraction',
759
+ outputVolume: 50,
760
+ })
761
+ const rows = toSamplePrepRows(template)
762
+ const frame = toSamplePrepDataFrame(template)
763
+
764
+ expect(template.template_id).toBe('sample-prep')
765
+ expect(rows[0].sourceSampleId).toBe('s001')
766
+ expect(rows[0].destinationSampleId).toBe('s001-prepared')
767
+ expect(rows[0].outputVolume).toBe(50)
768
+ expect(rows[1].id).toBe('dilute-s002')
769
+ expect(frame.rowKey).toBe('id')
770
+ expect(frame.columns.map(column => column.key)).toContain('status')
771
+ expect(toTemplateDataFrame(template).data[1].type).toBe('dilution')
772
+ })
773
+
774
+ it('creates dose-response templates and adapts dose conditions', () => {
775
+ const template = createDoseResponseTemplate({
776
+ compounds: { 'Drug A': [0.1, 10, 1] },
777
+ unit: 'uM',
778
+ replicates: 2,
779
+ })
780
+ const conditions = toDoseConditions(template)
781
+
782
+ expect(template.template_id).toBe('dose-response')
783
+ expect(template.data.compounds[0].concentrations).toEqual([10, 1, 0.1])
784
+ expect(conditions[0].unit).toBe('uM')
785
+ expect(conditions[0].concentrations).toEqual([10, 1, 0.1])
786
+ })
787
+
788
+ it('defaults metadata for explicit template records', () => {
789
+ const samplePrep = createSamplePrepTemplate({
790
+ samples: [{ name: 'Extract S001', sourceSampleId: 'S001' }],
791
+ })
792
+ const doseResponse = createDoseResponseTemplate({
793
+ compounds: [{ id: 'drug-a', name: 'Drug A', concentrations: [1, 0.1] }],
794
+ unit: 'uM',
795
+ controls: [{ id: 'vehicle', name: 'DMSO', role: 'vehicle' }],
796
+ })
797
+ const timeCourse = createTimeCourseTemplate({
798
+ timepoints: [{ id: 't0-hour', label: '0 hour', value: 0, unit: 'hour' }],
799
+ unit: 'hour',
800
+ conditions: [{ id: 'control', name: 'Control' }],
801
+ })
802
+ const assayMatrix = createAssayMatrixTemplate({
803
+ samples: [{ sampleId: 'S001', name: 'Control 1' }],
804
+ features: [{ id: 'lactate', name: 'Lactate', type: 'metabolite' }],
805
+ })
806
+
807
+ expect(samplePrep.data.steps[0].metadata).toEqual({})
808
+ expect(doseResponse.data.compounds[0].metadata).toEqual({})
809
+ expect(doseResponse.data.controls[0].metadata).toEqual({})
810
+ expect(timeCourse.data.timepoints[0].metadata).toEqual({})
811
+ expect(timeCourse.data.conditions[0].metadata).toEqual({})
812
+ expect(assayMatrix.data.samples[0].metadata).toEqual({})
813
+ expect(assayMatrix.data.features[0].metadata).toEqual({})
814
+ expect(toDoseConditions(doseResponse)[0].color).toBe('#3B82F6')
815
+ expect(toTimeCourseRows(timeCourse)[0].sampleId).toBe('control-t0-hour-r1')
816
+ expect(toAssayMatrixRows(assayMatrix)[0].sampleId).toBe('S001')
817
+ })
818
+
819
+ it('loads the shared dose-response fixture with layout', () => {
820
+ const conditions = toDoseConditions(doseResponseFixture as DoseResponseTemplate)
821
+ const layout = toDoseLayoutState(doseResponseFixture as DoseResponseTemplate)
822
+ const componentProps = toBioTemplateComponentProps(doseResponseFixture as DoseResponseTemplate)
823
+ const doseCalculator = componentProps.find(binding => binding.component === 'DoseCalculator')
824
+ const wellPlate = componentProps.find(binding => binding.component === 'WellPlate')
825
+
826
+ expect(conditions[0].label).toBe('Drug A')
827
+ expect(layout?.plates[0].wells.A2.value).toBe(10)
828
+ expect(doseCalculator?.propsObject.mode).toBe('serial')
829
+ expect((doseCalculator?.propsObject.targetWells as string[])).toContain('A2')
830
+ expect((wellPlate?.propsObject.wells as Record<string, { value?: number }>).A2.value).toBe(10)
831
+ })
832
+
833
+ it('rejects invalid dose-response data', () => {
834
+ expect(() => createDoseResponseTemplate({
835
+ compounds: { 'Drug A': [-1] },
836
+ unit: 'uM',
837
+ })).toThrow('negative concentration')
838
+ })
839
+
840
+ it('creates calibration-curve templates and adapts standard curves', () => {
841
+ const template = createCalibrationCurveTemplate({
842
+ concentrations: [0.1, { level: 'Standard 2', concentration: 1 }, 10],
843
+ analyte: 'Glucose',
844
+ unit: 'uM',
845
+ })
846
+ const rows = toCalibrationCurveRows(template)
847
+ const frame = toCalibrationCurveDataFrame(template)
848
+
849
+ expect(template.template_id).toBe('calibration-curve')
850
+ expect(template.data.points.map(point => point.role)).toEqual([
851
+ 'blank',
852
+ 'standard',
853
+ 'standard',
854
+ 'standard',
855
+ 'qc',
856
+ 'qc',
857
+ ])
858
+ expect(rows[2].id).toBe('standard-2')
859
+ expect(rows[2].analyte).toBe('Glucose')
860
+ expect(frame.rowKey).toBe('id')
861
+ expect(frame.columns.map(column => column.key)).toContain('status')
862
+ expect(toTemplateDataFrame(template).data[0].role).toBe('blank')
863
+ })
864
+
865
+ it('creates time-course templates and adapts samples and steps', () => {
866
+ const template = createTimeCourseTemplate({
867
+ timepoints: [24, 0, 6],
868
+ unit: 'hour',
869
+ conditions: ['Control', 'Treatment'],
870
+ replicates: 2,
871
+ })
872
+ const rows = toTimeCourseRows(template)
873
+ const steps = toTimeCourseSteps(template)
874
+
875
+ expect(template.template_id).toBe('time-course')
876
+ expect(template.data.timepoints.map(timepoint => timepoint.id)).toEqual(['t0-hour', 't6-hour', 't24-hour'])
877
+ expect(rows).toHaveLength(12)
878
+ expect(toTimeCourseDataFrame(template).columns[0].key).toBe('sampleId')
879
+ expect(toTemplateDataFrame(template).data).toHaveLength(12)
880
+ expect(steps[0].type).toBe('measurement')
881
+ })
882
+
883
+ it('creates protocol-step templates and adapts timeline and dataframe props', () => {
884
+ const template = createProtocolStepsTemplate({
885
+ steps: [
886
+ {
887
+ name: 'Measure readout',
888
+ type: 'measurement',
889
+ duration: 45,
890
+ order: 2,
891
+ },
892
+ 'Seed cells',
893
+ ],
894
+ })
895
+ const steps = toProtocolSteps(template)
896
+ const frame = toProtocolDataFrame(template)
897
+
898
+ expect(template.template_id).toBe('protocol-steps')
899
+ expect(steps.map(step => step.name)).toEqual(['Seed cells', 'Measure readout'])
900
+ expect(steps[1].type).toBe('measurement')
901
+ expect(frame.rowKey).toBe('id')
902
+ expect(frame.columns.map(column => column.key)).toContain('duration')
903
+ expect(toTemplateDataFrame(template).data[0].id).toBe('seed-cells')
904
+ })
905
+
906
+ it('creates assay-matrix templates and adapts pivoted rows and columns', () => {
907
+ const template = createAssayMatrixTemplate({
908
+ samples: ['S001', 'S002'],
909
+ features: ['Lactate', 'Glucose'],
910
+ values: {
911
+ s001: { lactate: 1.2, glucose: 5.4 },
912
+ s002: { lactate: 2.1, glucose: 4.8 },
913
+ },
914
+ })
915
+ const rows = toAssayMatrixRows(template)
916
+ const columns = toAssayMatrixColumns(template)
917
+
918
+ expect(template.template_id).toBe('assay-matrix')
919
+ expect(rows[0].lactate).toBe(1.2)
920
+ expect(columns.map(column => column.key)).toContain('glucose')
921
+ const assayFrame = toAssayMatrixDataFrame(template)
922
+ expect(assayFrame.rowKey).toBe('sampleId')
923
+ expect(assayFrame.data[0].lactate).toBe(1.2)
924
+ expect(toTemplateDataFrame(template).columns.map(column => column.key)).toContain('glucose')
925
+ })
926
+
927
+ it('creates reagent-list templates and adapts items and dataframe props', () => {
928
+ const template = createReagentListTemplate({
929
+ reagents: [
930
+ {
931
+ id: 'dmso',
932
+ name: 'DMSO',
933
+ kind: 'compound',
934
+ storageCondition: 'RT',
935
+ stockLevel: 80,
936
+ stockUnit: '%',
937
+ },
938
+ 'Anti-HA antibody',
939
+ ],
940
+ })
941
+ const items = toReagentListItems(template)
942
+ const frame = toReagentDataFrame(template)
943
+
944
+ expect(template.template_id).toBe('reagent-list')
945
+ expect(items[0].name).toBe('DMSO')
946
+ expect(items[0].storageCondition).toBe('RT')
947
+ expect(template.data.reagents[1].id).toBe('anti-ha-antibody')
948
+ expect(frame.rowKey).toBe('id')
949
+ expect(frame.columns.map(column => column.key)).toContain('stockLevel')
950
+ expect(toTemplateDataFrame(template).data[0].kind).toBe('compound')
951
+ })
952
+
953
+ it('creates flow-cytometry panel templates and adapts marker rows', () => {
954
+ const template = createFlowCytometryPanelTemplate({
955
+ markers: [
956
+ { id: 'cd3', marker: 'CD3', fluorophore: 'FITC', detector: 'B530/30' },
957
+ { id: 'cd4', marker: 'CD4', fluorophore: 'PE', detector: 'B585/42' },
958
+ ],
959
+ instrument: 'BD LSRFortessa',
960
+ })
961
+ const rows = toFlowPanelRows(template)
962
+ const frame = toFlowPanelDataFrame(template)
963
+
964
+ expect(template.template_id).toBe('flow-cytometry-panel')
965
+ expect(template.data.controls.map(control => control.kind)).toContain('single-stain')
966
+ expect(rows[0].marker).toBe('CD3')
967
+ expect(rows[0].controls).toContain('CD3 single stain')
968
+ expect(frame.rowKey).toBe('id')
969
+ expect(frame.columns.map(column => column.key)).toContain('fluorophore')
970
+ expect(toTemplateDataFrame(template).data[1].detector).toBe('B585/42')
971
+ })
972
+
973
+ it('creates instrument-run templates and adapts run queues', () => {
974
+ const template = createInstrumentRunTemplate({
975
+ items: ['S001', { name: 'S002', vial: 'A2' }],
976
+ instrument: 'LC-MS',
977
+ method: 'Default method',
978
+ })
979
+ const rows = toInstrumentRunRows(template)
980
+ const frame = toInstrumentRunDataFrame(template)
981
+
982
+ expect(template.template_id).toBe('instrument-run')
983
+ expect(template.data.items.map(item => item.kind)).toEqual(['blank', 'qc', 'sample', 'sample', 'qc'])
984
+ expect(rows[0].name).toBe('Blank start')
985
+ expect(rows[2].sampleId).toBe('s001')
986
+ expect(rows[3].vial).toBe('A2')
987
+ expect(frame.rowKey).toBe('id')
988
+ expect(frame.columns.map(column => column.key)).toContain('status')
989
+ expect(toTemplateDataFrame(template).data[3].sampleId).toBe('s002')
990
+ })
991
+
992
+ it('creates qPCR plate templates and adapts reactions to dataframe and wells', () => {
993
+ const template = createQpcrPlateTemplate({
994
+ samples: ['Control', 'Treatment'],
995
+ targets: ['ACTB', 'GAPDH'],
996
+ replicates: 2,
997
+ instrument: 'QuantStudio',
998
+ })
999
+ const rows = toQpcrRows(template)
1000
+ const frame = toQpcrDataFrame(template)
1001
+ const wells = toQpcrWellPlateWells(template)
1002
+
1003
+ expect(template.template_id).toBe('qpcr-plate')
1004
+ expect(template.data.reactions).toHaveLength(10)
1005
+ expect(template.data.reactions.at(-1)?.controlKind).toBe('no-template')
1006
+ expect(rows[0].sample).toBe('Control')
1007
+ expect(rows[0].target).toBe('ACTB')
1008
+ expect(frame.rowKey).toBe('id')
1009
+ expect(frame.columns.map(column => column.key)).toContain('cq')
1010
+ expect(wells.A1.sampleType).toBe('control')
1011
+ expect(wells.A1.metadata?.targetId).toBe('actb')
1012
+ expect(toTemplateDataFrame(template).data.at(-1)?.controlKind).toBe('no-template')
1013
+ })
1014
+
1015
+ it('loads the shared qPCR plate fixture', () => {
1016
+ const template = ensureTemplateEnvelope<QpcrPlateTemplate>(qpcrPlateFixture, 'qpcr-plate')
1017
+ const frame = toQpcrDataFrame(template)
1018
+ const wells = toQpcrWellPlateWells(template)
1019
+ const componentProps = toBioTemplateComponentProps(template)
1020
+ const wellPlate = componentProps.find(binding => binding.component === 'WellPlate')
1021
+ const table = componentProps.find(binding => binding.component === 'DataFrame')
1022
+
1023
+ expect(template.data.chemistry).toBe('taqman')
1024
+ expect(frame.data[2].flags).toBe('review')
1025
+ expect(frame.data[3].sample).toBe('no-template')
1026
+ expect(wells.A1.row).toBe(0)
1027
+ expect(wells.A1.col).toBe(0)
1028
+ expect(wells.A4.sampleType).toBe('no-template')
1029
+ expect((wellPlate?.propsObject.wells as Record<string, { sampleType?: string }>).A4.sampleType).toBe('no-template')
1030
+ expect((table?.propsObject.data as Record<string, unknown>[])[0].target).toBe('ACTB')
1031
+ })
1032
+
1033
+ it('rejects templates without a dataframe adapter', () => {
1034
+ expect(() => toTemplateDataFrame(createPlateMapTemplate())).toThrow(
1035
+ "Template 'plate-map' does not expose a DataFrame adapter."
1036
+ )
1037
+ expect(() => toTemplateDataFrame(createDoseResponseTemplate({
1038
+ compounds: { 'Drug A': [1] },
1039
+ unit: 'uM',
1040
+ }))).toThrow("Template 'dose-response' does not expose a DataFrame adapter.")
1041
+ })
1042
+
1043
+ it('accepts assay-matrix display names for values', () => {
1044
+ const template = createAssayMatrixTemplate({
1045
+ samples: ['S001'],
1046
+ features: ['Lactate'],
1047
+ values: {
1048
+ S001: { Lactate: 1.2 },
1049
+ },
1050
+ })
1051
+
1052
+ expect(template.data.measurements[0].sampleId).toBe('s001')
1053
+ expect(template.data.measurements[0].featureId).toBe('lactate')
1054
+ })
1055
+ })