@morscherlab/mint-sdk 1.0.0-beta.3 → 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 (165) hide show
  1. package/README.md +9 -2
  2. package/dist/__tests__/composables/experiment-utils.test.d.ts +1 -0
  3. package/dist/__tests__/composables/useApi.test.d.ts +1 -0
  4. package/dist/components/AppContainer.vue.d.ts +1 -1
  5. package/dist/components/AppLayout.vue.d.ts +20 -1
  6. package/dist/components/AppSidebar.vue.d.ts +56 -4
  7. package/dist/components/AppTopBar.vue.d.ts +7 -25
  8. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +3 -1
  9. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -0
  10. package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +5 -0
  11. package/dist/components/ComponentBindingRenderer.vue.d.ts +44 -0
  12. package/dist/components/ControlWorkspaceView.vue.d.ts +24 -7
  13. package/dist/components/DoseDesignWorkspaceView.vue.d.ts +149 -0
  14. package/dist/components/ExperimentTimeline.vue.d.ts +1 -1
  15. package/dist/components/FormBuilder.vue.d.ts +9 -9
  16. package/dist/components/PlateMapEditor.vue.d.ts +1 -1
  17. package/dist/components/PluginWorkspaceView.vue.d.ts +310 -0
  18. package/dist/components/SettingsModal.vue.d.ts +1 -1
  19. package/dist/components/WellPlate.vue.d.ts +2 -2
  20. package/dist/components/index.d.ts +3 -12
  21. package/dist/components/index.js +3 -3
  22. package/dist/components/{AppPageSelector.vue.d.ts → internal/AppPageSelectorInternal.vue.d.ts} +1 -1
  23. package/dist/components/{AppPillNav.vue.d.ts → internal/AppPillNavInternal.vue.d.ts} +3 -1
  24. package/dist/components/{CalendarGridPanel.vue.d.ts → internal/CalendarGridPanelInternal.vue.d.ts} +1 -1
  25. package/dist/components/internal/FormSectionRenderer.vue.d.ts +4 -4
  26. package/dist/components/{WellEditPopup.vue.d.ts → internal/WellEditPopupInternal.vue.d.ts} +1 -1
  27. package/dist/{components-D_Sr0adg.js → components-BkGF4B4y.js} +4484 -3967
  28. package/dist/components-BkGF4B4y.js.map +1 -0
  29. package/dist/composables/experiment-utils.d.ts +8 -0
  30. package/dist/composables/index.d.ts +5 -7
  31. package/dist/composables/index.js +4 -4
  32. package/dist/composables/useAppExperiment.d.ts +31 -2
  33. package/dist/composables/useBioTemplateComponents.d.ts +5 -3
  34. package/dist/composables/useBioTemplatePackWorkspace.d.ts +3 -2
  35. package/dist/composables/useBioTemplatePresetWorkspace.d.ts +6 -5
  36. package/dist/composables/useBioTemplateWorkspace.d.ts +5 -4
  37. package/dist/composables/useControlSchema.d.ts +43 -21
  38. package/dist/composables/usePluginClient.d.ts +5 -2
  39. package/dist/{composables-C3dpXQN5.js → composables-CHsME9H1.js} +40 -28
  40. package/dist/composables-CHsME9H1.js.map +1 -0
  41. package/dist/index.d.ts +5 -12
  42. package/dist/index.js +5 -5
  43. package/dist/install.js +2 -2
  44. package/dist/styles.css +3625 -3651
  45. package/dist/templates/componentBindings.d.ts +13 -0
  46. package/dist/templates/index.d.ts +3 -3
  47. package/dist/templates/index.js +2 -2
  48. package/dist/{templates-50NPjaxL.js → templates-B5jmTWuk.js} +111 -56
  49. package/dist/templates-B5jmTWuk.js.map +1 -0
  50. package/dist/types/components.d.ts +6 -25
  51. package/dist/types/index.d.ts +1 -1
  52. package/dist/{useScheduleDrag-D4oWdh41.js → useScheduleDrag-BgzpQT53.js} +160 -117
  53. package/dist/useScheduleDrag-BgzpQT53.js.map +1 -0
  54. package/package.json +1 -1
  55. package/src/__tests__/components/ActionItem.test.ts +6 -6
  56. package/src/__tests__/components/AppLayout.test.ts +44 -0
  57. package/src/__tests__/components/AppPageSelector.test.ts +8 -8
  58. package/src/__tests__/components/AppPillNav.test.ts +53 -6
  59. package/src/__tests__/components/AppSidebar.test.ts +126 -0
  60. package/src/__tests__/components/AppToastContainer.test.ts +0 -11
  61. package/src/__tests__/components/AppTopBar.test.ts +182 -119
  62. package/src/__tests__/components/BioTemplateExperimentWorkspaceView.test.ts +7 -1
  63. package/src/__tests__/components/BioTemplatePackWorkspaceView.test.ts +15 -1
  64. package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +26 -1
  65. package/src/__tests__/components/CalendarGridPanel.test.ts +3 -3
  66. package/src/__tests__/components/ComponentBindingRenderer.test.ts +161 -0
  67. package/src/__tests__/components/ControlWorkspaceView.test.ts +134 -63
  68. package/src/__tests__/components/DateTimePicker.test.ts +2 -2
  69. package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +185 -0
  70. package/src/__tests__/components/PluginWorkspaceView.test.ts +548 -0
  71. package/src/__tests__/composables/experiment-utils.test.ts +30 -0
  72. package/src/__tests__/composables/useApi.test.ts +30 -0
  73. package/src/__tests__/composables/useAppExperiment.test.ts +100 -1
  74. package/src/__tests__/composables/useBioTemplatePackWorkspace.test.ts +6 -3
  75. package/src/__tests__/composables/useBioTemplatePresetWorkspace.test.ts +6 -6
  76. package/src/__tests__/composables/useBioTemplateWorkspace.test.ts +6 -1
  77. package/src/__tests__/composables/useControlSchema.test.ts +150 -36
  78. package/src/__tests__/composables/usePluginClient.test.ts +99 -2
  79. package/src/__tests__/docs/frontendDocsCatalog.test.ts +120 -25
  80. package/src/__tests__/templates/templates.test.ts +12 -0
  81. package/src/components/AppAvatarMenu.vue +3 -3
  82. package/src/components/AppLayout.story.vue +39 -0
  83. package/src/components/AppLayout.vue +83 -2
  84. package/src/components/AppPluginSwitcher.vue +5 -5
  85. package/src/components/AppSidebar.story.vue +113 -5
  86. package/src/components/AppSidebar.vue +144 -24
  87. package/src/components/AppTopBar.story.vue +2 -5
  88. package/src/components/AppTopBar.vue +35 -425
  89. package/src/components/BioTemplateExperimentWorkspaceView.story.vue +2 -2
  90. package/src/components/BioTemplateExperimentWorkspaceView.vue +6 -0
  91. package/src/components/BioTemplatePackWorkspaceView.story.vue +4 -4
  92. package/src/components/BioTemplatePackWorkspaceView.vue +1 -0
  93. package/src/components/BioTemplatePresetWorkspaceView.story.vue +14 -2
  94. package/src/components/BioTemplatePresetWorkspaceView.vue +11 -2
  95. package/src/components/BioTemplateRenderer.vue +15 -227
  96. package/src/components/ComponentBindingRenderer.story.vue +57 -0
  97. package/src/components/ComponentBindingRenderer.vue +308 -0
  98. package/src/components/ControlWorkspaceView.story.vue +20 -9
  99. package/src/components/ControlWorkspaceView.vue +43 -12
  100. package/src/components/DatePicker.vue +2 -2
  101. package/src/components/DateTimePicker.vue +2 -2
  102. package/src/components/DoseDesignWorkspaceView.story.vue +77 -0
  103. package/src/components/DoseDesignWorkspaceView.vue +255 -0
  104. package/src/components/ExperimentPopover.vue +2 -6
  105. package/src/components/ExperimentSelectorModal.vue +6 -5
  106. package/src/components/FormBuilder.story.vue +190 -0
  107. package/src/components/PluginWorkspaceView.story.vue +334 -0
  108. package/src/components/PluginWorkspaceView.vue +708 -0
  109. package/src/components/SettingsModal.story.vue +87 -0
  110. package/src/components/WellPlate.vue +2 -2
  111. package/src/components/index.ts +3 -12
  112. package/src/components/{AppPageSelector.vue → internal/AppPageSelectorInternal.vue} +9 -9
  113. package/src/components/internal/AppPillNavInternal.vue +194 -0
  114. package/src/components/{CalendarGridPanel.vue → internal/CalendarGridPanelInternal.vue} +1 -1
  115. package/src/components/{WellEditPopup.vue → internal/WellEditPopupInternal.vue} +3 -3
  116. package/src/composables/experiment-utils.ts +26 -0
  117. package/src/composables/index.ts +21 -7
  118. package/src/composables/useApi.ts +9 -2
  119. package/src/composables/useAppExperiment.ts +85 -13
  120. package/src/composables/useBioTemplateComponents.ts +12 -0
  121. package/src/composables/useBioTemplatePackWorkspace.ts +6 -2
  122. package/src/composables/useBioTemplatePresetWorkspace.ts +10 -21
  123. package/src/composables/useBioTemplateWorkspace.ts +6 -4
  124. package/src/composables/useControlSchema.ts +157 -69
  125. package/src/composables/usePluginClient.ts +50 -9
  126. package/src/index.ts +6 -563
  127. package/src/styles/components/app-layout.css +82 -0
  128. package/src/styles/components/app-pill-nav.css +70 -0
  129. package/src/styles/components/app-sidebar.css +119 -0
  130. package/src/styles/components/app-top-bar.css +0 -235
  131. package/src/styles/index.css +0 -1
  132. package/src/templates/componentBindings.ts +38 -0
  133. package/src/templates/index.ts +4 -0
  134. package/src/types/components.ts +6 -31
  135. package/src/types/index.ts +2 -6
  136. package/dist/__tests__/composables/usePluginApi.test.d.ts +0 -13
  137. package/dist/components/FormFieldRenderer.vue.d.ts +0 -28
  138. package/dist/components/FormSection.vue.d.ts +0 -30
  139. package/dist/components/GroupingModal.vue.d.ts +0 -12
  140. package/dist/components/SettingsButton.vue.d.ts +0 -30
  141. package/dist/components/ToastNotification.vue.d.ts +0 -2
  142. package/dist/components-D_Sr0adg.js.map +0 -1
  143. package/dist/composables/usePluginApi.d.ts +0 -22
  144. package/dist/composables-C3dpXQN5.js.map +0 -1
  145. package/dist/templates-50NPjaxL.js.map +0 -1
  146. package/dist/useScheduleDrag-D4oWdh41.js.map +0 -1
  147. package/src/__tests__/components/FormCompatibility.test.ts +0 -94
  148. package/src/__tests__/components/GroupingModal.test.ts +0 -73
  149. package/src/__tests__/components/SettingsButton.test.ts +0 -44
  150. package/src/__tests__/composables/usePluginApi.test.ts +0 -81
  151. package/src/components/AppPillNav.vue +0 -71
  152. package/src/components/FormFieldRenderer.vue +0 -35
  153. package/src/components/FormSection.vue +0 -37
  154. package/src/components/GroupingModal.story.vue +0 -52
  155. package/src/components/GroupingModal.vue +0 -61
  156. package/src/components/SettingsButton.story.vue +0 -58
  157. package/src/components/SettingsButton.vue +0 -64
  158. package/src/components/ToastNotification.vue +0 -9
  159. package/src/composables/usePluginApi.ts +0 -32
  160. package/src/styles/components/settings-button.css +0 -31
  161. /package/dist/__tests__/components/{FormCompatibility.test.d.ts → ComponentBindingRenderer.test.d.ts} +0 -0
  162. /package/dist/__tests__/components/{GroupingModal.test.d.ts → DoseDesignWorkspaceView.test.d.ts} +0 -0
  163. /package/dist/__tests__/components/{SettingsButton.test.d.ts → PluginWorkspaceView.test.d.ts} +0 -0
  164. /package/dist/components/{ActionItem.vue.d.ts → internal/ActionItemInternal.vue.d.ts} +0 -0
  165. /package/src/components/{ActionItem.vue → internal/ActionItemInternal.vue} +0 -0
@@ -1,5 +1,5 @@
1
1
  import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { ref, computed } from 'vue'
2
+ import { ref, computed, nextTick } from 'vue'
3
3
 
4
4
  // Capture side-effect callbacks so tests can invoke them directly
5
5
  let capturedScopeDispose: (() => void) | null = null
@@ -83,6 +83,43 @@ describe('useAppExperiment', () => {
83
83
  // -------------------------------------------------------------------------
84
84
 
85
85
  describe('set', () => {
86
+ it('should initialize from a direct experiment option', () => {
87
+ const { experimentName, experimentCode, experimentId } = useAppExperiment({
88
+ experiment: makeExperiment({ id: 8, name: 'Initial run', experiment_code: 'EXP-008' }),
89
+ })
90
+
91
+ expect(experimentName.value).toBe('Initial run')
92
+ expect(experimentCode.value).toBe('EXP-008')
93
+ expect(experimentId.value).toBe(8)
94
+ })
95
+
96
+ it('should sync and clear a reactive experiment option', async () => {
97
+ const source = ref<ExperimentSummary | null>(
98
+ makeExperiment({ id: 8, name: 'Initial run', experiment_code: 'EXP-008' }),
99
+ )
100
+ const { experimentName, experimentCode, experimentId } = useAppExperiment({
101
+ experiment: source,
102
+ })
103
+
104
+ expect(experimentName.value).toBe('Initial run')
105
+ expect(experimentCode.value).toBe('EXP-008')
106
+ expect(experimentId.value).toBe(8)
107
+
108
+ source.value = makeExperiment({ id: 9, name: 'Updated run', experiment_code: 'EXP-009' })
109
+ await nextTick()
110
+
111
+ expect(experimentName.value).toBe('Updated run')
112
+ expect(experimentCode.value).toBe('EXP-009')
113
+ expect(experimentId.value).toBe(9)
114
+
115
+ source.value = null
116
+ await nextTick()
117
+
118
+ expect(experimentName.value).toBeUndefined()
119
+ expect(experimentCode.value).toBeUndefined()
120
+ expect(experimentId.value).toBeNull()
121
+ })
122
+
86
123
  it('should update experimentName and experimentId on set', () => {
87
124
  const { set, experimentName, experimentId } = useAppExperiment()
88
125
  const experiment = makeExperiment({ id: 7, name: 'Alpha Run' })
@@ -511,6 +548,68 @@ describe('useAppExperiment', () => {
511
548
  })
512
549
  })
513
550
 
551
+ // -------------------------------------------------------------------------
552
+ // component bindings
553
+ // -------------------------------------------------------------------------
554
+
555
+ describe('component bindings', () => {
556
+ it('exposes live ExperimentPopover props from the provided state', async () => {
557
+ const saveDisabled = ref(false)
558
+ const saveDisabledMessage = ref<string | undefined>('Ready to save')
559
+ const onSave = vi.fn().mockResolvedValue('Saved')
560
+ const { set } = useAppExperiment({ onSave, saveDisabled, saveDisabledMessage })
561
+ const state = getState()
562
+
563
+ set(makeExperiment({
564
+ id: 12,
565
+ name: 'Dose response',
566
+ status: 'ongoing',
567
+ experiment_code: 'EXP-012',
568
+ }))
569
+
570
+ expect(state.popover.value).toMatchObject({
571
+ experimentName: 'Dose response',
572
+ experimentCode: 'EXP-012',
573
+ experimentStatus: 'ongoing',
574
+ showSave: true,
575
+ showDetach: true,
576
+ saveDisabled: false,
577
+ saveDisabledMessage: 'Ready to save',
578
+ saveLoading: false,
579
+ })
580
+
581
+ saveDisabled.value = true
582
+ saveDisabledMessage.value = 'Locked'
583
+
584
+ expect(state.popover.value.saveDisabled).toBe(true)
585
+ expect(state.popover.value.saveDisabledMessage).toBe('Locked')
586
+
587
+ await state.handleSave()
588
+
589
+ expect(state.popover.value.saveSuccessMessage).toBe('Saved')
590
+ })
591
+
592
+ it('exposes live ExperimentSelectorModal props from state and return value', () => {
593
+ const appExperiment = useAppExperiment()
594
+ const state = getState()
595
+
596
+ expect(state.selectorModal.value).toEqual({
597
+ modelValue: false,
598
+ currentExperimentId: null,
599
+ })
600
+ expect(appExperiment.selectorModal.value).toEqual(state.selectorModal.value)
601
+
602
+ appExperiment.set(makeExperiment({ id: 77 }))
603
+ state.openModal()
604
+
605
+ expect(state.selectorModal.value).toEqual({
606
+ modelValue: true,
607
+ currentExperimentId: 77,
608
+ })
609
+ expect(appExperiment.popover.value.experimentName).toBe('Test Experiment')
610
+ })
611
+ })
612
+
514
613
  // -------------------------------------------------------------------------
515
614
  // Timer cleanup via onScopeDispose
516
615
  // -------------------------------------------------------------------------
@@ -46,7 +46,7 @@ describe('useBioTemplatePackWorkspace', () => {
46
46
  expect(workspace.sidebar.value).toBe(workspace.workspace.value.sidebar)
47
47
  expect(workspace.sidebar.value.values).toBe(workspace.workspace.value.values)
48
48
  expect(workspace.sidebar.value.activeView).toBe('design')
49
- expect(workspace.topBar.value).toBe(workspace.workspace.value.topBar)
49
+ expect(workspace.topBar.value).toBe(workspace.workspace.value.topBar.value)
50
50
  expect(workspace.pillNav.value).toBe(workspace.workspace.value.pillNav)
51
51
  expect(workspace.pillNav.value.items.map(item => item.id)).toContain('design')
52
52
  expect(workspace.topBarSettings.value).toBe(workspace.workspace.value.topBarSettings)
@@ -56,9 +56,12 @@ describe('useBioTemplatePackWorkspace', () => {
56
56
  expect(workspace.topBarSettings.value.settingsConfig.values).toBe(workspace.workspace.value.values)
57
57
  expect(workspace.sidebar.value.panels.design.map(panel => panel.id)).toContain('dose')
58
58
  expect(workspace.componentBindings.value.map(binding => binding.component)).toContain('DoseCalculator')
59
+ expect(workspace.componentBindingsById.value['dose-response:DoseCalculator'].component).toBe('DoseCalculator')
60
+ expect(workspace.componentBindingsById.value['dose-response:DoseCalculator'].props.mode).toBe('serial')
59
61
  expect(workspace.componentProps.value.map(binding => binding.component)).toContain('PlateMapEditor')
60
62
  expect(workspace.componentPropsById.value['plate-map:WellPlate'].wells).toBeDefined()
61
63
  expect(workspace.bindings.value.componentPropsById['plate-map:WellPlate'].wells).toBeDefined()
64
+ expect(workspace.bindings.value.componentBindingsById['plate-map:WellPlate'].props.wells).toBeDefined()
62
65
  expect(workspace.componentPropsByComponent.value.WellPlate.length).toBeGreaterThan(0)
63
66
  expect(workspace.getComponentProps('WellPlate')?.wells).toBeDefined()
64
67
  expect(workspace.bindings.value.getComponentProps('WellPlate')?.wells).toBeDefined()
@@ -79,10 +82,10 @@ describe('useBioTemplatePackWorkspace', () => {
79
82
  workspace.pillNav.value.onSelect({ id: 'analysis', label: 'Analysis' })
80
83
  expect(inner.activeView.value).toBe('analysis')
81
84
  expect(workspace.sidebar.value.activeView).toBe('analysis')
82
- expect(workspace.topBar.value.currentTabId).toBe('analysis')
85
+ expect(workspace.topBar.value.currentPillId).toBe('analysis')
83
86
  expect(workspace.bindings.value.topBar.value.currentPillId).toBe('analysis')
84
87
 
85
- workspace.topBar.value.onTabSelect({ id: 'run', label: 'Run' })
88
+ workspace.topBar.value.onPillSelect({ id: 'run', label: 'Run' })
86
89
  expect(inner.activeView.value).toBe('run')
87
90
  expect(workspace.sidebar.value.activeView).toBe('run')
88
91
  expect(workspace.pillNav.value.currentItemId).toBe('run')
@@ -53,16 +53,18 @@ describe('useBioTemplatePresetWorkspace', () => {
53
53
  expect(workspace.bindings.topBarSettings).toBe(workspace.topBarSettings)
54
54
  expect(workspace.bindings.pillNav).toBe(workspace.pillNav)
55
55
  expect(workspace.bindings.topBar.value.currentPillId).toBe('design')
56
- expect(workspace.bindings.topBarTabs.value.currentTabId).toBe('design')
57
56
  expect(workspace.controlViewIds.value).toEqual(['design'])
58
57
  expect(workspace.controlViewItems.value).toEqual([{ id: 'design', label: 'Design' }])
59
58
  expect(workspace.activeControlView.value).toBe('design')
60
59
  expect(workspace.renderer.value.target).toBe(workspace.collection.value)
61
60
  expect(workspace.bindings.renderer.value.target).toBe(workspace.collection.value)
62
61
  expect(workspace.componentBindings.value.map(binding => binding.component)).toContain('DoseCalculator')
62
+ expect(workspace.componentBindingsById.value['dose-response:DoseCalculator'].component).toBe('DoseCalculator')
63
+ expect(workspace.componentBindingsById.value['dose-response:DoseCalculator'].props.mode).toBe('serial')
63
64
  expect(workspace.componentProps.value.map(binding => binding.component)).toContain('WellPlate')
64
65
  expect(workspace.componentPropsById.value['dose-response:DoseCalculator'].mode).toBe('serial')
65
66
  expect(workspace.bindings.componentPropsById.value['dose-response:DoseCalculator'].mode).toBe('serial')
67
+ expect(workspace.bindings.componentBindingsById.value['dose-response:DoseCalculator'].props.mode).toBe('serial')
66
68
  expect(workspace.componentPropsByComponent.value.WellPlate).toHaveLength(2)
67
69
  expect(workspace.getComponentProps('DoseCalculator')?.mode).toBe('serial')
68
70
  expect(workspace.bindings.getComponentProps('DoseCalculator')?.mode).toBe('serial')
@@ -82,7 +84,7 @@ describe('useBioTemplatePresetWorkspace', () => {
82
84
  workspace.setActiveControlView('analysis')
83
85
  expect(workspace.activeControlView.value).toBe('analysis')
84
86
  expect(workspace.sidebar.activeView).toBe('analysis')
85
- expect(workspace.topBar.currentTabId).toBe('analysis')
87
+ expect(workspace.topBar.value.currentPillId).toBe('analysis')
86
88
  expect(workspace.pillNav.currentItemId).toBe('analysis')
87
89
 
88
90
  workspace.setActiveControlView('missing')
@@ -104,15 +106,13 @@ describe('useBioTemplatePresetWorkspace', () => {
104
106
  workspace.pillNav.onSelect({ id: 'analysis', label: 'Analysis' })
105
107
  expect(workspace.activeControlView.value).toBe('analysis')
106
108
  expect(workspace.sidebar.activeView).toBe('analysis')
107
- expect(workspace.topBar.currentTabId).toBe('analysis')
109
+ expect(workspace.topBar.value.currentPillId).toBe('analysis')
108
110
  expect(workspace.bindings.topBar.value.currentPillId).toBe('analysis')
109
- expect(workspace.bindings.topBarTabs.value.currentTabId).toBe('analysis')
110
111
 
111
- workspace.topBar.onTabSelect({ id: 'run', label: 'Run' })
112
+ workspace.topBar.value.onPillSelect({ id: 'run', label: 'Run' })
112
113
  expect(workspace.activeControlView.value).toBe('run')
113
114
  expect(workspace.pillNav.currentItemId).toBe('run')
114
115
  expect(workspace.bindings.topBar.value.currentPillId).toBe('run')
115
- expect(workspace.bindings.topBarTabs.value.currentTabId).toBe('run')
116
116
  })
117
117
 
118
118
  it('accepts direct initial values for generated preset controls and collection data', () => {
@@ -36,6 +36,9 @@ describe('useBioTemplateWorkspace', () => {
36
36
  expect(workspace.sidebar.panels.design.map(panel => panel.id)).toContain('dose')
37
37
  expect(workspace.componentProps.map(binding => binding.component)).toContain('DoseCalculator')
38
38
  expect(workspace.componentProps.map(binding => binding.component)).toContain('WellPlate')
39
+ expect(workspace.componentBindingsById['plate-map:WellPlate'].component).toBe('WellPlate')
40
+ expect(workspace.componentBindingsById['plate-map:WellPlate'].props.wells).toBeDefined()
41
+ expect(workspace.bindings.componentBindingsById['dose-response:DoseCalculator'].props.mode).toBe('serial')
39
42
  expect(workspace.componentPropsById['plate-map:WellPlate'].wells).toBeDefined()
40
43
  expect(workspace.bindings.componentPropsById['plate-map:WellPlate'].wells).toBeDefined()
41
44
  expect(workspace.componentPropsById['dose-response:DoseCalculator'].mode).toBe('serial')
@@ -56,14 +59,16 @@ describe('useBioTemplateWorkspace', () => {
56
59
  expect(workspace.controlSchema.sampleNames).toBe(workspace.controls.controls.sampleNames)
57
60
  expect(workspace.values.sampleNames).toEqual(['Control', 'Treatment'])
58
61
  expect(workspace.activeView.value).toBe('design')
59
- expect(workspace.topBar.tabs.map(tab => tab.id)).toContain('design')
62
+ expect(workspace.topBar.value.pillNav.map(item => item.id)).toContain('design')
60
63
  expect(workspace.pillNav.items.map(item => item.id)).toContain('design')
61
64
  expect(workspace.controls.initialValues.sampleNames).toEqual(['Control', 'Treatment'])
62
65
  expect(workspace.sidebar.panels.design.map(panel => panel.id)).toContain('layout')
63
66
  expect(workspace.componentBindings.map(binding => binding.component)).toContain('PlateMapEditor')
64
67
  expect(workspace.componentImports[0].statement).toContain('WellPlate')
65
68
  expect(workspace.componentProps).toEqual([])
69
+ expect(workspace.componentBindingsById).toEqual({})
66
70
  expect(workspace.componentPropsById).toEqual({})
71
+ expect(workspace.bindings.componentBindingsById).toEqual({})
67
72
  expect(workspace.bindings.componentPropsById).toEqual({})
68
73
  expect(workspace.componentPropsByComponent).toEqual({})
69
74
  expect(workspace.getComponentProps('WellPlate')).toBeUndefined()
@@ -7,15 +7,18 @@ import {
7
7
  controlsToSidebarPanels,
8
8
  controlsToSettingsSchema,
9
9
  controlsToTopBarSettingsConfig,
10
- controlsToTopBarTabs,
11
10
  controlsToViewIds,
12
11
  controlsToViewItems,
12
+ controlValuesToComponentBindings,
13
+ controlValuesToComponentBindingsById,
13
14
  controlValuesToComponentProps,
15
+ defineControlComponentBindings,
14
16
  defineControlModel,
15
17
  defineDoseDesignControlModel,
16
18
  defineDoseCalculatorControlProps,
17
19
  defineControls,
18
20
  defineWellPlateControlProps,
21
+ defineWellPlateDoseComponentBindings,
19
22
  defineWellPlateDoseControlProps,
20
23
  getDefaultControlView,
21
24
  getControlDefaults,
@@ -194,11 +197,10 @@ describe('useControlSchema', () => {
194
197
  it('returns view helpers that can drive AppPillNav and AppSidebar together', () => {
195
198
  expect(controlsToViewIds(controls)).toEqual(['analysis'])
196
199
  expect(controlsToViewItems(controls)).toEqual([{ id: 'analysis', label: 'Analysis' }])
197
- expect(controlsToTopBarTabs(controls)).toEqual([{ id: 'analysis', label: 'Analysis' }])
198
200
  expect(getDefaultControlView(controls)).toBe('analysis')
199
201
  })
200
202
 
201
- it('uses shared view metadata for AppTopBar tabs and AppPillNav items', () => {
203
+ it('uses shared view metadata for AppTopBar pill navigation', () => {
202
204
  const runIcon = 'M8 5v14l11-7z'
203
205
  const options = {
204
206
  views: {
@@ -218,14 +220,6 @@ describe('useControlSchema', () => {
218
220
  disabled: false,
219
221
  },
220
222
  ])
221
- expect(controlsToTopBarTabs(controls, options)).toEqual([
222
- {
223
- id: 'analysis',
224
- label: 'Run',
225
- icon: runIcon,
226
- disabled: false,
227
- },
228
- ])
229
223
  })
230
224
 
231
225
  it('flattens nested control models into workspace bindings', () => {
@@ -282,6 +276,23 @@ describe('useControlSchema', () => {
282
276
  disabled: values => !values.showHeatmap,
283
277
  },
284
278
  },
279
+ components: defineControlComponentBindings({
280
+ dose: {
281
+ component: 'DoseCalculator',
282
+ props: {
283
+ mode: 'concentrationUnit',
284
+ targetWells: values => values.showHeatmap ? ['A1', 'A2'] : [],
285
+ disabled: values => !values.showHeatmap,
286
+ },
287
+ },
288
+ plate: {
289
+ component: 'WellPlate',
290
+ props: {
291
+ modelValue: values => values.showHeatmap ? ['A1', 'A2'] : [],
292
+ disabled: values => !values.showHeatmap,
293
+ },
294
+ },
295
+ }),
285
296
  })
286
297
 
287
298
  expect(model.controls.concentrationUnit).toMatchObject({
@@ -309,11 +320,12 @@ describe('useControlSchema', () => {
309
320
  })
310
321
  expect(model.componentProps).toBeDefined()
311
322
  expect(model.componentPropsById).toBeDefined()
323
+ expect(model.componentBindings).toBeDefined()
312
324
 
313
325
  const workspace = useControlWorkspace(model.controls, model.controlOptions)
314
326
 
315
327
  expect(workspace.values.replicates).toBe(4)
316
- expect(workspace.topBar.tabs.map(tab => [tab.id, tab.label])).toEqual([
328
+ expect(workspace.topBar.value.pillNav.map(item => [item.id, item.label])).toEqual([
317
329
  ['design', 'Design'],
318
330
  ['results', 'Results'],
319
331
  ])
@@ -342,6 +354,80 @@ describe('useControlSchema', () => {
342
354
  disabled: false,
343
355
  },
344
356
  })
357
+ expect(workspace.getComponentBindings(model.componentBindings)).toEqual([
358
+ {
359
+ id: 'dose',
360
+ component: 'DoseCalculator',
361
+ props: {
362
+ mode: 'uM',
363
+ targetWells: ['A1', 'A2'],
364
+ disabled: false,
365
+ },
366
+ },
367
+ {
368
+ id: 'plate',
369
+ component: 'WellPlate',
370
+ props: {
371
+ modelValue: ['A1', 'A2'],
372
+ disabled: false,
373
+ },
374
+ },
375
+ ])
376
+ expect(workspace.getComponentBindingsById(model.componentBindings).plate.props.modelValue).toEqual(['A1', 'A2'])
377
+ })
378
+
379
+ it('maps control values into named SDK component bindings', () => {
380
+ const values = {
381
+ threshold: 0.05,
382
+ selectedWells: ['A1'],
383
+ enabled: true,
384
+ }
385
+ const bindings = defineControlComponentBindings([
386
+ {
387
+ component: 'ResultChart',
388
+ props: {
389
+ score: 'threshold',
390
+ disabled: values => !values.enabled,
391
+ },
392
+ },
393
+ {
394
+ component: 'ResultChart',
395
+ props: ['selectedWells'],
396
+ },
397
+ {
398
+ id: 'plate',
399
+ component: 'WellPlate',
400
+ props: {
401
+ modelValue: 'selectedWells',
402
+ },
403
+ },
404
+ ])
405
+
406
+ expect(controlValuesToComponentBindings(values, bindings)).toEqual([
407
+ {
408
+ id: 'ResultChart',
409
+ component: 'ResultChart',
410
+ props: {
411
+ score: 0.05,
412
+ disabled: false,
413
+ },
414
+ },
415
+ {
416
+ id: 'ResultChart-2',
417
+ component: 'ResultChart',
418
+ props: {
419
+ selectedWells: ['A1'],
420
+ },
421
+ },
422
+ {
423
+ id: 'plate',
424
+ component: 'WellPlate',
425
+ props: {
426
+ modelValue: ['A1'],
427
+ },
428
+ },
429
+ ])
430
+ expect(controlValuesToComponentBindingsById(values, bindings).plate.component).toBe('WellPlate')
345
431
  })
346
432
 
347
433
  it('keeps direct view controls in separate generated sections', () => {
@@ -466,7 +552,6 @@ describe('useControlSchema', () => {
466
552
  expect(toolkit.sidebarPanels.analysis).toHaveLength(2)
467
553
  expect(toolkit.viewIds).toEqual(['analysis'])
468
554
  expect(toolkit.viewItems).toEqual([{ id: 'analysis', label: 'Analysis' }])
469
- expect(toolkit.topBarTabs).toEqual([{ id: 'analysis', label: 'Analysis' }])
470
555
  expect(toolkit.defaultView).toBe('analysis')
471
556
  expect(toolkit.sidebar.panels).toBe(toolkit.sidebarPanels)
472
557
  expect(toolkit.sidebar.forms).toBe(toolkit.sectionSchemas)
@@ -502,8 +587,8 @@ describe('useControlSchema', () => {
502
587
  expect(workspace.sidebar.modelValue).toBe(workspace.values)
503
588
  expect(workspace.sidebar.values).toBe(workspace.values)
504
589
  expect(workspace.sidebar.activeView).toBe('default')
505
- expect(workspace.topBar.tabs).toEqual([{ id: 'default', label: 'Default' }])
506
- expect(workspace.topBar.currentTabId).toBe('default')
590
+ expect(workspace.topBar.value.pillNav).toEqual([{ id: 'default', label: 'Default' }])
591
+ expect(workspace.topBar.value.currentPillId).toBe('default')
507
592
  expect(workspace.pillNav.items).toEqual([{ id: 'default', label: 'Default' }])
508
593
  expect(workspace.pillNav.currentItemId).toBe('default')
509
594
  expect(workspace.topBarSettings.settingsConfig.values).toBe(workspace.values)
@@ -514,8 +599,11 @@ describe('useControlSchema', () => {
514
599
  expect(workspace.topBarSettings.settingsConfig.schema).toBeUndefined()
515
600
  expect(workspace.bindings.form).toBe(workspace.form)
516
601
  expect(workspace.bindings.sidebar).toBe(workspace.sidebar)
602
+ expect(workspace.bindings.topBar).toBe(workspace.topBar)
517
603
  expect(workspace.bindings.topBarSettings).toBe(workspace.topBarSettings)
518
604
  expect(workspace.bindings.pillNav).toBe(workspace.pillNav)
605
+ expect(workspace.bindings.componentBindings).toBe(workspace.componentBindings)
606
+ expect(workspace.bindings.componentBindingsById).toBe(workspace.componentBindingsById)
519
607
  expect(workspace.bindings.componentProps).toBe(workspace.componentProps)
520
608
  expect(workspace.bindings.componentPropsById).toBe(workspace.componentPropsById)
521
609
  expect(workspace.bindings.topBar.value).toMatchObject({
@@ -524,12 +612,6 @@ describe('useControlSchema', () => {
524
612
  showSettings: true,
525
613
  settingsConfig: workspace.topBarSettingsConfig,
526
614
  })
527
- expect(workspace.bindings.topBarTabs.value).toMatchObject({
528
- tabs: workspace.topBar.tabs,
529
- currentTabId: 'default',
530
- showSettings: true,
531
- settingsConfig: workspace.topBarSettingsConfig,
532
- })
533
615
 
534
616
  workspace.form['onUpdate:modelValue']({ threshold: 0.5, model: 'logistic' })
535
617
  expect(workspace.values.threshold).toBe(0.5)
@@ -545,7 +627,7 @@ describe('useControlSchema', () => {
545
627
  workspace.topBarSettings.onSettingsValuesChange({ mode: 'careful' })
546
628
  expect(workspace.values.mode).toBe('careful')
547
629
 
548
- workspace.topBar.onTabSelect({ id: 'missing', label: 'Missing' })
630
+ workspace.topBar.value.onPillSelect({ id: 'missing', label: 'Missing' })
549
631
  expect(workspace.activeView.value).toBe('default')
550
632
 
551
633
  workspace.resetValues()
@@ -702,6 +784,12 @@ describe('useControlSchema', () => {
702
784
  expect(workspace.componentPropsById.value.dose.mode).toBe('serial')
703
785
  expect(workspace.componentPropsById.value.dose.targetWells).toEqual(['E1'])
704
786
  expect(workspace.componentPropsById.value.dose.disabled).toBe(true)
787
+ expect(workspace.componentBindings.value.map(binding => [binding.id, binding.component])).toEqual([
788
+ ['plate', 'WellPlate'],
789
+ ['dose', 'DoseCalculator'],
790
+ ])
791
+ expect(workspace.componentBindingsById.value.plate.props.modelValue).toEqual(['E1'])
792
+ expect(workspace.componentBindingsById.value.dose.props.disabled).toBe(true)
705
793
 
706
794
  const updatePlateSelection = workspace.componentPropsById.value.plate['onUpdate:modelValue']
707
795
  expect(typeof updatePlateSelection).toBe('function')
@@ -753,6 +841,25 @@ describe('useControlSchema', () => {
753
841
  expect(doseProps.onApplyToWells).toBeUndefined()
754
842
  })
755
843
 
844
+ it('creates WellPlate and DoseCalculator component bindings for dose controls', () => {
845
+ const workspace = useControlWorkspace(defineControls({
846
+ selectedWells: { type: 'tags', default: ['B1'] },
847
+ plateFormat: 384,
848
+ doseMode: ['serial', 'dilution'],
849
+ disabled: false,
850
+ }))
851
+ const bindings = workspace.getComponentBindings(defineWellPlateDoseComponentBindings())
852
+
853
+ expect(bindings.map(binding => [binding.id, binding.component])).toEqual([
854
+ ['plate', 'WellPlate'],
855
+ ['dose', 'DoseCalculator'],
856
+ ])
857
+ expect(bindings[0].props.modelValue).toEqual(['B1'])
858
+ expect(bindings[0].props.format).toBe(384)
859
+ expect(bindings[1].props.mode).toBe('serial')
860
+ expect(bindings[1].props.targetWells).toEqual(['B1'])
861
+ })
862
+
756
863
  it('creates a complete dose design control model from one helper', () => {
757
864
  const model = defineDoseDesignControlModel({
758
865
  selectedWells: ['C1', 'C2'],
@@ -783,6 +890,10 @@ describe('useControlSchema', () => {
783
890
  expect(componentPropsById.dose.targetWells).toEqual(['C1', 'C2'])
784
891
  expect(componentPropsById.dose.disabled).toBe(false)
785
892
  expect(componentPropsById.dose.molecularWeight).toBe(300.44)
893
+ expect(workspace.getComponentBindings(model.componentBindings).map(binding => [binding.id, binding.component])).toEqual([
894
+ ['plate', 'WellPlate'],
895
+ ['dose', 'DoseCalculator'],
896
+ ])
786
897
 
787
898
  const updatePlateSelection = componentPropsById.plate['onUpdate:modelValue']
788
899
  expect(typeof updatePlateSelection).toBe('function')
@@ -814,9 +925,13 @@ describe('useControlSchema', () => {
814
925
  expect(componentPropsById.wellPlate.showLegend).toBe(true)
815
926
  expect(componentPropsById.doseCalculator.mode).toBe('serial')
816
927
  expect(componentPropsById.doseCalculator.targetWells).toEqual(['A1', 'A2'])
928
+ expect(workspace.getComponentBindings(model.componentBindings).map(binding => [binding.id, binding.component])).toEqual([
929
+ ['wellPlate', 'WellPlate'],
930
+ ['doseCalculator', 'DoseCalculator'],
931
+ ])
817
932
  })
818
933
 
819
- it('keeps AppTopBar tabs and AppSidebar active view on the same workspace state', async () => {
934
+ it('keeps AppTopBar pill navigation and AppSidebar active view on the same workspace state', async () => {
820
935
  const workspace = useControlWorkspace({
821
936
  threshold: {
822
937
  default: 0.05,
@@ -837,47 +952,43 @@ describe('useControlSchema', () => {
837
952
 
838
953
  expect(workspace.activeView.value).toBe('analysis')
839
954
  expect(workspace.sidebar.activeView).toBe('analysis')
840
- expect(workspace.topBar.currentTabId).toBe('analysis')
955
+ expect(workspace.topBar.value.currentPillId).toBe('analysis')
841
956
  expect(workspace.pillNav.currentItemId).toBe('analysis')
842
957
  expect(workspace.bindings.topBar.value.currentPillId).toBe('analysis')
843
- expect(workspace.bindings.topBarTabs.value.currentTabId).toBe('analysis')
844
- expect(workspace.topBar.tabs.map(tab => tab.label)).toEqual(['Run', 'Results'])
958
+ expect(workspace.topBar.value.pillNav.map(item => item.label)).toEqual(['Run', 'Results'])
845
959
  expect(workspace.pillNav.items.map(item => item.label)).toEqual(['Run', 'Results'])
846
960
 
847
- workspace.topBar.onTabSelect({ id: 'results', label: 'Results' })
961
+ workspace.topBar.value.onPillSelect({ id: 'results', label: 'Results' })
848
962
  expect(workspace.activeView.value).toBe('results')
849
963
  expect(workspace.sidebar.activeView).toBe('results')
850
- expect(workspace.topBar.currentTabId).toBe('results')
964
+ expect(workspace.topBar.value.currentPillId).toBe('results')
851
965
  expect(workspace.pillNav.currentItemId).toBe('results')
852
966
  expect(workspace.bindings.topBar.value.currentPillId).toBe('results')
853
- expect(workspace.bindings.topBarTabs.value.currentTabId).toBe('results')
854
967
 
855
968
  workspace.setActiveView('analysis')
856
969
  expect(workspace.activeView.value).toBe('analysis')
857
970
  expect(workspace.sidebar.activeView).toBe('analysis')
858
- expect(workspace.topBar.currentTabId).toBe('analysis')
971
+ expect(workspace.topBar.value.currentPillId).toBe('analysis')
859
972
  expect(workspace.pillNav.currentItemId).toBe('analysis')
860
973
  workspace.bindings.topBar.value.onPillSelect({ id: 'results', label: 'Results' })
861
974
  expect(workspace.activeView.value).toBe('results')
862
975
  expect(workspace.bindings.topBar.value.currentPillId).toBe('results')
863
- expect(workspace.bindings.topBarTabs.value.currentTabId).toBe('results')
864
976
 
865
- workspace.bindings.topBarTabs.value.onTabSelect({ id: 'analysis', label: 'Run' })
977
+ workspace.pillNav.onSelect({ id: 'analysis', label: 'Run' })
866
978
  expect(workspace.activeView.value).toBe('analysis')
867
979
  expect(workspace.bindings.topBar.value.currentPillId).toBe('analysis')
868
- expect(workspace.bindings.topBarTabs.value.currentTabId).toBe('analysis')
869
980
 
870
981
  workspace.pillNav.onSelect({ id: 'results', label: 'Results' })
871
982
  expect(workspace.activeView.value).toBe('results')
872
983
  expect(workspace.sidebar.activeView).toBe('results')
873
- expect(workspace.topBar.currentTabId).toBe('results')
984
+ expect(workspace.topBar.value.currentPillId).toBe('results')
874
985
  expect(workspace.pillNav.currentItemId).toBe('results')
875
986
 
876
987
  workspace.activeView.value = 'analysis'
877
988
  await nextTick()
878
989
 
879
990
  expect(workspace.sidebar.activeView).toBe('analysis')
880
- expect(workspace.topBar.currentTabId).toBe('analysis')
991
+ expect(workspace.topBar.value.currentPillId).toBe('analysis')
881
992
  expect(workspace.pillNav.currentItemId).toBe('analysis')
882
993
 
883
994
  workspace.pillNav.currentItemId = 'results'
@@ -885,7 +996,7 @@ describe('useControlSchema', () => {
885
996
 
886
997
  expect(workspace.activeView.value).toBe('results')
887
998
  expect(workspace.sidebar.activeView).toBe('results')
888
- expect(workspace.topBar.currentTabId).toBe('results')
999
+ expect(workspace.topBar.value.currentPillId).toBe('results')
889
1000
  })
890
1001
 
891
1002
  it('builds controls and component bindings for biology templates', () => {
@@ -900,6 +1011,8 @@ describe('useControlSchema', () => {
900
1011
  expect(controlsToolkit.sidebarPanels.design.map(panel => panel.id)).toContain('dose')
901
1012
  expect(componentsToolkit.bindings.map(binding => binding.component)).toContain('WellPlate')
902
1013
  expect(componentsToolkit.imports[0].statement).toContain('DoseCalculator')
1014
+ expect(componentsToolkit.componentBindingsById['dose-response:DoseCalculator'].component).toBe('DoseCalculator')
1015
+ expect(componentsToolkit.componentBindingsById['dose-response:DoseCalculator'].props.mode).toBe('serial')
903
1016
  expect(componentsToolkit.componentProps.map(binding => binding.component)).toContain('DoseCalculator')
904
1017
  expect(componentsToolkit.componentPropsById['dose-response:DoseCalculator'].mode).toBe('serial')
905
1018
  expect(componentsToolkit.componentPropsByComponent.WellPlate).toHaveLength(2)
@@ -910,6 +1023,7 @@ describe('useControlSchema', () => {
910
1023
  const presetOnlyToolkit = useBioTemplateComponents('wellplate-screen')
911
1024
  expect(presetOnlyToolkit.imports[0].statement).toContain('PlateMapEditor')
912
1025
  expect(presetOnlyToolkit.componentProps).toEqual([])
1026
+ expect(presetOnlyToolkit.componentBindingsById).toEqual({})
913
1027
  expect(presetOnlyToolkit.componentPropsById).toEqual({})
914
1028
  expect(presetOnlyToolkit.componentPropsByComponent).toEqual({})
915
1029
  expect(presetOnlyToolkit.getComponentProps('WellPlate')).toBeUndefined()