@morscherlab/mint-sdk 1.0.0-beta.4 → 1.0.0-beta.5
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.
- package/dist/components/AppSidebar.vue.d.ts +1 -1
- package/dist/components/index.js +2 -2
- package/dist/{components-BkGF4B4y.js → components-DihbSJjU.js} +2524 -2517
- package/dist/components-DihbSJjU.js.map +1 -0
- package/dist/composables/index.js +3 -3
- package/dist/{composables-CHsME9H1.js → composables-BcgZ6diz.js} +2 -2
- package/dist/{composables-CHsME9H1.js.map → composables-BcgZ6diz.js.map} +1 -1
- package/dist/index.js +5 -5
- package/dist/install.js +2 -2
- package/dist/styles.css +738 -738
- package/dist/templates/adapters.d.ts +7 -1
- package/dist/templates/catalog.d.ts +5 -5
- package/dist/templates/index.d.ts +2 -2
- package/dist/templates/index.js +2 -2
- package/dist/templates/presets.d.ts +4 -4
- package/dist/templates/types.d.ts +4 -1
- package/dist/{templates-B5jmTWuk.js → templates-Cyt0Suwf.js} +213 -19
- package/dist/{templates-B5jmTWuk.js.map → templates-Cyt0Suwf.js.map} +1 -1
- package/dist/{useScheduleDrag-BgzpQT53.js → useExperimentData-CM6Y0u5L.js} +183 -183
- package/dist/useExperimentData-CM6Y0u5L.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/AppSidebar.test.ts +4 -2
- package/src/__tests__/components/AppTopBar.test.ts +9 -3
- package/src/__tests__/components/{AppPageSelector.test.ts → AppTopBarPageSelector.test.ts} +8 -8
- package/src/__tests__/components/{AppPillNav.test.ts → AppTopBarPillNav.test.ts} +7 -7
- package/src/__tests__/components/BioTemplatePackWorkspaceView.test.ts +17 -0
- package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +22 -0
- package/src/__tests__/components/BioTemplateRenderer.test.ts +25 -0
- package/src/__tests__/components/ComponentBindingRenderer.test.ts +117 -0
- package/src/__tests__/composables/useBioTemplatePackWorkspace.test.ts +1 -1
- package/src/__tests__/composables/useBioTemplatePresetWorkspace.test.ts +1 -1
- package/src/__tests__/composables/useControlSchema.test.ts +1 -1
- package/src/__tests__/templates/templates.test.ts +44 -0
- package/src/components/AppSidebar.vue +3 -3
- package/src/components/AppTopBar.vue +7 -7
- package/src/components/BioTemplatePresetWorkspaceView.vue +3 -3
- package/src/components/BioTemplateRenderer.story.vue +2 -2
- package/src/components/ComponentBindingRenderer.story.vue +30 -0
- package/src/components/ComponentBindingRenderer.vue +9 -0
- package/src/components/ExperimentPopover.story.vue +2 -2
- package/src/styles/components/app-page-selector.css +1 -1
- package/src/styles/components/app-pill-nav.css +1 -1
- package/src/styles/components/experiment-popover.css +2 -2
- package/src/templates/adapters.ts +193 -0
- package/src/templates/catalog.ts +5 -5
- package/src/templates/componentBindings.ts +52 -3
- package/src/templates/index.ts +6 -0
- package/src/templates/packs.ts +10 -1
- package/src/templates/presets.ts +14 -4
- package/src/templates/types.ts +4 -0
- package/dist/components-BkGF4B4y.js.map +0 -1
- package/dist/useScheduleDrag-BgzpQT53.js.map +0 -1
- /package/dist/__tests__/components/{AppPageSelector.test.d.ts → AppTopBarPageSelector.test.d.ts} +0 -0
- /package/dist/__tests__/components/{AppPillNav.test.d.ts → AppTopBarPillNav.test.d.ts} +0 -0
- /package/dist/components/internal/{AppPageSelectorInternal.vue.d.ts → AppTopBarPageSelectorInternal.vue.d.ts} +0 -0
- /package/dist/components/internal/{AppPillNavInternal.vue.d.ts → AppTopBarPillNavInternal.vue.d.ts} +0 -0
- /package/src/components/internal/{AppPageSelectorInternal.vue → AppTopBarPageSelectorInternal.vue} +0 -0
- /package/src/components/internal/{AppPillNavInternal.vue → AppTopBarPillNavInternal.vue} +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morscherlab/mint-sdk",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.5",
|
|
4
4
|
"description": "MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -747,11 +747,13 @@ describe('AppSidebar', () => {
|
|
|
747
747
|
expect(wrapper.find('.mint-sidebar--hidden').exists()).toBe(true)
|
|
748
748
|
})
|
|
749
749
|
|
|
750
|
-
it('
|
|
750
|
+
it('uses the first non-empty panel view when activeView is omitted', () => {
|
|
751
751
|
const wrapper = mount(AppSidebar, {
|
|
752
752
|
props: { panels: samplePanels },
|
|
753
753
|
})
|
|
754
|
-
expect(wrapper.find('.mint-sidebar--hidden').exists()).toBe(
|
|
754
|
+
expect(wrapper.find('.mint-sidebar--hidden').exists()).toBe(false)
|
|
755
|
+
expect(wrapper.findAllComponents(CollapsibleCard)).toHaveLength(2)
|
|
756
|
+
expect(wrapper.find('.mint-sidebar__sections').exists()).toBe(true)
|
|
755
757
|
})
|
|
756
758
|
})
|
|
757
759
|
|
|
@@ -1063,18 +1063,24 @@ describe('AppTopBar', () => {
|
|
|
1063
1063
|
version: '1.0',
|
|
1064
1064
|
route_prefix: '/test',
|
|
1065
1065
|
api_prefix: '/api/test',
|
|
1066
|
-
nav_items: [
|
|
1066
|
+
nav_items: [
|
|
1067
|
+
{ path: '/stale', label: 'Stale' },
|
|
1068
|
+
{ path: '/stale-two', label: 'Stale Two' },
|
|
1069
|
+
],
|
|
1067
1070
|
},
|
|
1068
1071
|
})
|
|
1069
1072
|
|
|
1070
1073
|
const wrapper = createWrapper({
|
|
1071
|
-
pageSelector: [
|
|
1074
|
+
pageSelector: [
|
|
1075
|
+
{ id: 'overview', label: 'Overview', to: '/' },
|
|
1076
|
+
{ id: 'reports', label: 'Reports', to: '/reports' },
|
|
1077
|
+
],
|
|
1072
1078
|
currentPageSelectorId: 'overview',
|
|
1073
1079
|
})
|
|
1074
1080
|
await wrapper.get('.mint-page-selector__trigger').trigger('click')
|
|
1075
1081
|
|
|
1076
1082
|
expect(wrapper.get('.mint-page-selector__label').text()).toBe('Overview')
|
|
1077
|
-
expect(wrapper.findAll('.mint-page-selector__item-label').map(item => item.text())).toEqual(['Overview'])
|
|
1083
|
+
expect(wrapper.findAll('.mint-page-selector__item-label').map(item => item.text())).toEqual(['Overview', 'Reports'])
|
|
1078
1084
|
})
|
|
1079
1085
|
})
|
|
1080
1086
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { mount } from '@vue/test-utils'
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import
|
|
3
|
+
import AppTopBarPageSelectorInternal from '../../components/internal/AppTopBarPageSelectorInternal.vue'
|
|
4
4
|
|
|
5
|
-
describe('
|
|
5
|
+
describe('AppTopBarPageSelectorInternal', () => {
|
|
6
6
|
it('accepts string shorthand pages', async () => {
|
|
7
|
-
const wrapper = mount(
|
|
7
|
+
const wrapper = mount(AppTopBarPageSelectorInternal, {
|
|
8
8
|
props: {
|
|
9
9
|
currentPageId: 'Workspace',
|
|
10
10
|
pages: ['Workspace', 'Results'],
|
|
@@ -25,7 +25,7 @@ describe('AppPageSelectorInternal', () => {
|
|
|
25
25
|
})
|
|
26
26
|
|
|
27
27
|
it('closes linked page actions without emitting select', async () => {
|
|
28
|
-
const wrapper = mount(
|
|
28
|
+
const wrapper = mount(AppTopBarPageSelectorInternal, {
|
|
29
29
|
props: {
|
|
30
30
|
currentPageId: 'workspace',
|
|
31
31
|
pages: [
|
|
@@ -43,7 +43,7 @@ describe('AppPageSelectorInternal', () => {
|
|
|
43
43
|
})
|
|
44
44
|
|
|
45
45
|
it('keeps disabled linked page actions inert', async () => {
|
|
46
|
-
const wrapper = mount(
|
|
46
|
+
const wrapper = mount(AppTopBarPageSelectorInternal, {
|
|
47
47
|
props: {
|
|
48
48
|
currentPageId: 'workspace',
|
|
49
49
|
pages: [
|
|
@@ -66,7 +66,7 @@ describe('AppPageSelectorInternal', () => {
|
|
|
66
66
|
|
|
67
67
|
it('emits select for button page actions', async () => {
|
|
68
68
|
const page = { id: 'workspace', label: 'Workspace' }
|
|
69
|
-
const wrapper = mount(
|
|
69
|
+
const wrapper = mount(AppTopBarPageSelectorInternal, {
|
|
70
70
|
props: {
|
|
71
71
|
pages: [page],
|
|
72
72
|
},
|
|
@@ -81,7 +81,7 @@ describe('AppPageSelectorInternal', () => {
|
|
|
81
81
|
it('renders PluginIcon-compatible metadata icons and keeps symbolic icons as fallback initials', async () => {
|
|
82
82
|
const iconPath = 'M4 19h16M7 16V8m5 8V4m5 12v-6'
|
|
83
83
|
const pngIcon = 'data:image/png;base64,iVBORw0KGgo='
|
|
84
|
-
const wrapper = mount(
|
|
84
|
+
const wrapper = mount(AppTopBarPageSelectorInternal, {
|
|
85
85
|
props: {
|
|
86
86
|
pages: [
|
|
87
87
|
{ id: 'analysis', label: 'Analysis', icon: iconPath },
|
|
@@ -117,7 +117,7 @@ describe('AppPageSelectorInternal', () => {
|
|
|
117
117
|
|
|
118
118
|
it('renders https page icons through PluginIcon', async () => {
|
|
119
119
|
const httpsIcon = 'https://example.com/plugin-icon.png'
|
|
120
|
-
const wrapper = mount(
|
|
120
|
+
const wrapper = mount(AppTopBarPageSelectorInternal, {
|
|
121
121
|
props: {
|
|
122
122
|
pages: [
|
|
123
123
|
{ id: 'docs', label: 'Docs', icon: httpsIcon },
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { mount } from '@vue/test-utils'
|
|
2
2
|
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import
|
|
3
|
+
import AppTopBarPillNavInternal from '../../components/internal/AppTopBarPillNavInternal.vue'
|
|
4
4
|
|
|
5
|
-
describe('
|
|
5
|
+
describe('AppTopBarPillNavInternal', () => {
|
|
6
6
|
it('accepts string shorthand items', async () => {
|
|
7
|
-
const wrapper = mount(
|
|
7
|
+
const wrapper = mount(AppTopBarPillNavInternal, {
|
|
8
8
|
props: {
|
|
9
9
|
currentItemId: 'Overview',
|
|
10
10
|
items: ['Overview', 'Analysis'],
|
|
@@ -22,7 +22,7 @@ describe('AppPillNavInternal', () => {
|
|
|
22
22
|
})
|
|
23
23
|
|
|
24
24
|
it('emits select for button items but not linked items', async () => {
|
|
25
|
-
const wrapper = mount(
|
|
25
|
+
const wrapper = mount(AppTopBarPillNavInternal, {
|
|
26
26
|
props: {
|
|
27
27
|
currentItemId: 'overview',
|
|
28
28
|
items: [
|
|
@@ -40,7 +40,7 @@ describe('AppPillNavInternal', () => {
|
|
|
40
40
|
|
|
41
41
|
it('renders SVG icons from item metadata', () => {
|
|
42
42
|
const icon = ['M4 12h16', 'M12 4v16']
|
|
43
|
-
const wrapper = mount(
|
|
43
|
+
const wrapper = mount(AppTopBarPillNavInternal, {
|
|
44
44
|
props: {
|
|
45
45
|
items: [
|
|
46
46
|
{ id: 'run', label: 'Run', icon },
|
|
@@ -59,7 +59,7 @@ describe('AppPillNavInternal', () => {
|
|
|
59
59
|
})
|
|
60
60
|
|
|
61
61
|
it('prevents disabled link navigation and selection', async () => {
|
|
62
|
-
const wrapper = mount(
|
|
62
|
+
const wrapper = mount(AppTopBarPillNavInternal, {
|
|
63
63
|
props: {
|
|
64
64
|
items: [
|
|
65
65
|
{ id: 'docs', label: 'Docs', href: '/docs', disabled: true },
|
|
@@ -77,7 +77,7 @@ describe('AppPillNavInternal', () => {
|
|
|
77
77
|
})
|
|
78
78
|
|
|
79
79
|
it('supports dropdown child items for grouped pill navigation', async () => {
|
|
80
|
-
const wrapper = mount(
|
|
80
|
+
const wrapper = mount(AppTopBarPillNavInternal, {
|
|
81
81
|
props: {
|
|
82
82
|
currentItemId: 'pca',
|
|
83
83
|
items: [
|
|
@@ -21,8 +21,10 @@ const globalOptions = {
|
|
|
21
21
|
stubs: {
|
|
22
22
|
DataFrame: true,
|
|
23
23
|
DoseCalculator: true,
|
|
24
|
+
ExperimentTimeline: true,
|
|
24
25
|
PlateMapEditor: true,
|
|
25
26
|
SampleSelector: true,
|
|
27
|
+
ScheduleCalendar: true,
|
|
26
28
|
WellPlate: true,
|
|
27
29
|
},
|
|
28
30
|
}
|
|
@@ -172,4 +174,19 @@ describe('BioTemplatePackWorkspaceView', () => {
|
|
|
172
174
|
})
|
|
173
175
|
expect(wrapper.find('.mint-bio-template-renderer').exists()).toBe(false)
|
|
174
176
|
})
|
|
177
|
+
|
|
178
|
+
it('renders metabolism pack run queues as timeline and calendar previews', () => {
|
|
179
|
+
const wrapper = mount(BioTemplatePackWorkspaceView, {
|
|
180
|
+
props: {
|
|
181
|
+
pack: 'metabolism',
|
|
182
|
+
},
|
|
183
|
+
global: globalOptions,
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
expect(wrapper.text()).toContain('Omics assay saves the full template collection')
|
|
187
|
+
expect(wrapper.find('[data-component-binding-id="instrument-run:ExperimentTimeline"]').exists()).toBe(true)
|
|
188
|
+
expect(wrapper.find('[data-component-binding-id="instrument-run:ScheduleCalendar"]').exists()).toBe(true)
|
|
189
|
+
expect(wrapper.find('[data-template-component="ExperimentTimeline"]').exists()).toBe(true)
|
|
190
|
+
expect(wrapper.find('[data-template-component="ScheduleCalendar"]').exists()).toBe(true)
|
|
191
|
+
})
|
|
175
192
|
})
|
|
@@ -303,4 +303,26 @@ describe('BioTemplatePresetWorkspaceView', () => {
|
|
|
303
303
|
expect(workspace.activeControlView.value).toBe('analysis')
|
|
304
304
|
expect(wrapper.findComponent(AppSidebar).props('activeView')).toBe('analysis')
|
|
305
305
|
})
|
|
306
|
+
|
|
307
|
+
it('renders LC-MS batch timeline and calendar previews from one preset prop', () => {
|
|
308
|
+
const wrapper = mount(BioTemplatePresetWorkspaceView, {
|
|
309
|
+
props: {
|
|
310
|
+
preset: 'lcms-batch',
|
|
311
|
+
},
|
|
312
|
+
global: {
|
|
313
|
+
stubs: {
|
|
314
|
+
DataFrame: true,
|
|
315
|
+
ExperimentTimeline: true,
|
|
316
|
+
ScheduleCalendar: true,
|
|
317
|
+
SampleSelector: true,
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
})
|
|
321
|
+
|
|
322
|
+
expect(wrapper.text()).toContain('LC-MS batch preset saves')
|
|
323
|
+
expect(wrapper.find('[data-component-binding-id="instrument-run:ExperimentTimeline"]').exists()).toBe(true)
|
|
324
|
+
expect(wrapper.find('[data-component-binding-id="instrument-run:ScheduleCalendar"]').exists()).toBe(true)
|
|
325
|
+
expect(wrapper.find('[data-template-component="ExperimentTimeline"]').exists()).toBe(true)
|
|
326
|
+
expect(wrapper.find('[data-template-component="ScheduleCalendar"]').exists()).toBe(true)
|
|
327
|
+
})
|
|
306
328
|
})
|
|
@@ -33,6 +33,31 @@ describe('BioTemplateRenderer', () => {
|
|
|
33
33
|
expect(wrapper.text()).toContain('dose-response')
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
+
it('renders instrument-run timelines from LC-MS template collections', () => {
|
|
37
|
+
const collection = createLcmsBatchCollection({
|
|
38
|
+
samples: ['S001', 'S002'],
|
|
39
|
+
instrument: 'LC-MS',
|
|
40
|
+
})
|
|
41
|
+
const wrapper = mount(BioTemplateRenderer, {
|
|
42
|
+
props: { target: collection },
|
|
43
|
+
global: {
|
|
44
|
+
stubs: {
|
|
45
|
+
DataFrame: true,
|
|
46
|
+
ExperimentTimeline: true,
|
|
47
|
+
SampleSelector: true,
|
|
48
|
+
ScheduleCalendar: true,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
expect(wrapper.find('[data-component-binding-id="instrument-run:ScheduleCalendar"]').exists()).toBe(true)
|
|
54
|
+
expect(wrapper.find('[data-component-binding-id="instrument-run:ExperimentTimeline"]').exists()).toBe(true)
|
|
55
|
+
expect(wrapper.find('[data-component-binding-id="assay-matrix:SampleSelector"]').exists()).toBe(true)
|
|
56
|
+
expect(wrapper.find('[data-template-component="ScheduleCalendar"]').exists()).toBe(true)
|
|
57
|
+
expect(wrapper.find('[data-template-component="ExperimentTimeline"]').exists()).toBe(true)
|
|
58
|
+
expect(wrapper.find('[data-template-id="instrument-run"]').exists()).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
36
61
|
it('filters rendered components with include and exclude lists', () => {
|
|
37
62
|
const collection = createWellPlateScreenCollection()
|
|
38
63
|
const wrapper = mount(BioTemplateRenderer, {
|
|
@@ -46,11 +46,48 @@ const PlateMapEditorStub = defineComponent({
|
|
|
46
46
|
template: '<div data-test="plate-map-editor" />',
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
+
const ExperimentTimelineStub = defineComponent({
|
|
50
|
+
name: 'ExperimentTimeline',
|
|
51
|
+
props: {
|
|
52
|
+
modelValue: { type: Array, default: () => [] },
|
|
53
|
+
editable: { type: Boolean, default: true },
|
|
54
|
+
size: { type: String, default: undefined },
|
|
55
|
+
},
|
|
56
|
+
template: '<div data-test="experiment-timeline" />',
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
const ScheduleCalendarStub = defineComponent({
|
|
60
|
+
name: 'ScheduleCalendar',
|
|
61
|
+
props: {
|
|
62
|
+
modelValue: { type: [String, Date], default: undefined },
|
|
63
|
+
events: { type: Array, default: () => [] },
|
|
64
|
+
readonly: { type: Boolean, default: false },
|
|
65
|
+
showNavigation: { type: Boolean, default: true },
|
|
66
|
+
showViewToggle: { type: Boolean, default: true },
|
|
67
|
+
view: { type: String, default: undefined },
|
|
68
|
+
},
|
|
69
|
+
template: '<div data-test="schedule-calendar" />',
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
const SampleSelectorStub = defineComponent({
|
|
73
|
+
name: 'SampleSelector',
|
|
74
|
+
props: {
|
|
75
|
+
samples: { type: Array, default: () => [] },
|
|
76
|
+
modelValue: { type: Array, default: () => [] },
|
|
77
|
+
enableGrouping: { type: Boolean, default: true },
|
|
78
|
+
enableSmartGroup: { type: Boolean, default: true },
|
|
79
|
+
},
|
|
80
|
+
template: '<div data-test="sample-selector" />',
|
|
81
|
+
})
|
|
82
|
+
|
|
49
83
|
const globalOptions = {
|
|
50
84
|
stubs: {
|
|
51
85
|
DataFrame: DataFrameStub,
|
|
52
86
|
DoseCalculator: DoseCalculatorStub,
|
|
87
|
+
ExperimentTimeline: ExperimentTimelineStub,
|
|
53
88
|
PlateMapEditor: PlateMapEditorStub,
|
|
89
|
+
SampleSelector: SampleSelectorStub,
|
|
90
|
+
ScheduleCalendar: ScheduleCalendarStub,
|
|
54
91
|
WellPlate: WellPlateStub,
|
|
55
92
|
},
|
|
56
93
|
}
|
|
@@ -158,4 +195,84 @@ describe('ComponentBindingRenderer', () => {
|
|
|
158
195
|
maxHeight: '280px',
|
|
159
196
|
})
|
|
160
197
|
})
|
|
198
|
+
|
|
199
|
+
it('renders ExperimentTimeline bindings with preview-safe props', () => {
|
|
200
|
+
const wrapper = mount(ComponentBindingRenderer, {
|
|
201
|
+
props: {
|
|
202
|
+
binding: {
|
|
203
|
+
id: 'instrument-run:ExperimentTimeline',
|
|
204
|
+
component: 'ExperimentTimeline',
|
|
205
|
+
propsObject: {
|
|
206
|
+
modelValue: [{ id: 's001-run', name: 'S001', type: 'measurement', status: 'pending', order: 1 }],
|
|
207
|
+
editable: true,
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
dense: true,
|
|
211
|
+
},
|
|
212
|
+
global: globalOptions,
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
expect(wrapper.find('[data-component-binding-id="instrument-run:ExperimentTimeline"]').exists()).toBe(true)
|
|
216
|
+
expect(wrapper.findComponent(ExperimentTimelineStub).props()).toMatchObject({
|
|
217
|
+
modelValue: [{ id: 's001-run', name: 'S001', type: 'measurement', status: 'pending', order: 1 }],
|
|
218
|
+
editable: false,
|
|
219
|
+
size: 'sm',
|
|
220
|
+
})
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('renders ScheduleCalendar bindings with preview-safe props', () => {
|
|
224
|
+
const events = [{ id: 's001-run', title: 'S001', start: '2024-01-01T08:00:00.000Z', end: '2024-01-01T08:10:00.000Z' }]
|
|
225
|
+
const wrapper = mount(ComponentBindingRenderer, {
|
|
226
|
+
props: {
|
|
227
|
+
binding: {
|
|
228
|
+
id: 'instrument-run:ScheduleCalendar',
|
|
229
|
+
component: 'ScheduleCalendar',
|
|
230
|
+
propsObject: {
|
|
231
|
+
modelValue: events[0].start,
|
|
232
|
+
events,
|
|
233
|
+
readonly: false,
|
|
234
|
+
showNavigation: true,
|
|
235
|
+
showViewToggle: true,
|
|
236
|
+
view: 'day',
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
dense: true,
|
|
240
|
+
},
|
|
241
|
+
global: globalOptions,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
expect(wrapper.find('[data-component-binding-id="instrument-run:ScheduleCalendar"]').exists()).toBe(true)
|
|
245
|
+
expect(wrapper.findComponent(ScheduleCalendarStub).props()).toMatchObject({
|
|
246
|
+
modelValue: events[0].start,
|
|
247
|
+
events,
|
|
248
|
+
readonly: true,
|
|
249
|
+
showNavigation: false,
|
|
250
|
+
showViewToggle: false,
|
|
251
|
+
view: 'day',
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
it('renders SampleSelector bindings with preview-safe grouping props', () => {
|
|
256
|
+
const wrapper = mount(ComponentBindingRenderer, {
|
|
257
|
+
props: {
|
|
258
|
+
binding: {
|
|
259
|
+
id: 'assay-matrix:SampleSelector',
|
|
260
|
+
component: 'SampleSelector',
|
|
261
|
+
propsObject: {
|
|
262
|
+
samples: ['S001', 'S002'],
|
|
263
|
+
modelValue: [],
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
global: globalOptions,
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
expect(wrapper.find('[data-component-binding-id="assay-matrix:SampleSelector"]').exists()).toBe(true)
|
|
271
|
+
expect(wrapper.findComponent(SampleSelectorStub).props()).toMatchObject({
|
|
272
|
+
samples: ['S001', 'S002'],
|
|
273
|
+
modelValue: [],
|
|
274
|
+
enableGrouping: false,
|
|
275
|
+
enableSmartGroup: false,
|
|
276
|
+
})
|
|
277
|
+
})
|
|
161
278
|
})
|
|
@@ -68,7 +68,7 @@ describe('useBioTemplatePackWorkspace', () => {
|
|
|
68
68
|
expect(workspace.componentUsage.value?.template).toContain('<DoseCalculator')
|
|
69
69
|
})
|
|
70
70
|
|
|
71
|
-
it('exposes live AppSidebar, AppTopBar, and
|
|
71
|
+
it('exposes live AppSidebar, AppTopBar, and pillNav bindings for mixed-view packs', () => {
|
|
72
72
|
const workspace = useBioTemplatePackWorkspace('omics-assay')
|
|
73
73
|
const inner = workspace.workspace.value
|
|
74
74
|
|
|
@@ -91,7 +91,7 @@ describe('useBioTemplatePresetWorkspace', () => {
|
|
|
91
91
|
expect(workspace.activeControlView.value).toBe('analysis')
|
|
92
92
|
})
|
|
93
93
|
|
|
94
|
-
it('exposes live FormBuilder, AppSidebar, AppTopBar, and
|
|
94
|
+
it('exposes live FormBuilder, AppSidebar, AppTopBar, and pillNav bindings', () => {
|
|
95
95
|
const workspace = useBioTemplatePresetWorkspace('lcms-batch')
|
|
96
96
|
|
|
97
97
|
workspace.form['onUpdate:modelValue']({
|
|
@@ -194,7 +194,7 @@ describe('useControlSchema', () => {
|
|
|
194
194
|
])
|
|
195
195
|
})
|
|
196
196
|
|
|
197
|
-
it('returns view helpers that can drive
|
|
197
|
+
it('returns view helpers that can drive AppTopBar pillNav and AppSidebar together', () => {
|
|
198
198
|
expect(controlsToViewIds(controls)).toEqual(['analysis'])
|
|
199
199
|
expect(controlsToViewItems(controls)).toEqual([{ id: 'analysis', label: 'Analysis' }])
|
|
200
200
|
expect(getDefaultControlView(controls)).toBe('analysis')
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
toAssayMatrixColumns,
|
|
55
55
|
toAssayMatrixDataFrame,
|
|
56
56
|
toAssayMatrixRows,
|
|
57
|
+
toAssayMatrixSampleOptions,
|
|
57
58
|
toCalibrationCurveDataFrame,
|
|
58
59
|
toCalibrationCurveRows,
|
|
59
60
|
toDoseConditions,
|
|
@@ -62,6 +63,8 @@ import {
|
|
|
62
63
|
toFlowPanelRows,
|
|
63
64
|
toInstrumentRunDataFrame,
|
|
64
65
|
toInstrumentRunRows,
|
|
66
|
+
toInstrumentRunScheduleEvents,
|
|
67
|
+
toInstrumentRunSteps,
|
|
65
68
|
toPlateMapEditorState,
|
|
66
69
|
toProtocolDataFrame,
|
|
67
70
|
toProtocolSteps,
|
|
@@ -87,6 +90,7 @@ import bioTemplateCatalogContract from '../../../../bio-template-catalog.contrac
|
|
|
87
90
|
import bioTemplatePacksContract from '../../../../bio-template-packs.contract.json'
|
|
88
91
|
import bioTemplatePresetsContract from '../../../../bio-template-presets.contract.json'
|
|
89
92
|
import type {
|
|
93
|
+
AssayMatrixTemplate,
|
|
90
94
|
DoseResponseTemplate,
|
|
91
95
|
FlowCytometryPanelTemplate,
|
|
92
96
|
InstrumentRunTemplate,
|
|
@@ -158,6 +162,9 @@ describe('bio data templates', () => {
|
|
|
158
162
|
'flow-cytometry-panel',
|
|
159
163
|
])
|
|
160
164
|
expect(getBioTemplatePackInfo('metabolomics')?.name).toBe('omics-assay')
|
|
165
|
+
expect(getBioTemplatePackInfo('metabolism')?.name).toBe('omics-assay')
|
|
166
|
+
expect(getBioTemplatePackInfo('metabolite profiling')?.name).toBe('omics-assay')
|
|
167
|
+
expect(getBioTemplatePackInfo('metabolomics')?.components).toContain('ExperimentTimeline')
|
|
161
168
|
expect(getBioTemplatePackInfo('longitudinal')?.init_command).toBe('mint init --template longitudinal-study')
|
|
162
169
|
expect(getBioTemplatePackInfo('gene expression')?.templates.at(-1)).toBe('qpcr-plate')
|
|
163
170
|
expect(getBioTemplatePackInfo('gene-expression')?.templates.at(-1)).toBe('qpcr-plate')
|
|
@@ -214,6 +221,9 @@ describe('bio data templates', () => {
|
|
|
214
221
|
'qpcr-plate',
|
|
215
222
|
])
|
|
216
223
|
expect(searchBioTemplatePresets('run queue')[0].name).toBe('lcms-batch')
|
|
224
|
+
expect(getBioTemplatePresetInfo('metabolomics')?.name).toBe('lcms-batch')
|
|
225
|
+
expect(getBioTemplatePresetInfo('metabolism')?.name).toBe('lcms-batch')
|
|
226
|
+
expect(getBioTemplatePresetInfo('metabolite profiling')?.name).toBe('lcms-batch')
|
|
217
227
|
expect(getBioTemplatePresetInfo('immunoassay')?.templates).toEqual([
|
|
218
228
|
'plate-map',
|
|
219
229
|
'sample-sheet',
|
|
@@ -288,6 +298,7 @@ describe('bio data templates', () => {
|
|
|
288
298
|
const qpcrBindings = getBioTemplateComponentBindings('gene expression')
|
|
289
299
|
const elisaBindings = getBioTemplateComponentBindings('elisa-assay')
|
|
290
300
|
const flowBindings = getBioTemplateComponentBindings('flow-cytometry-assay')
|
|
301
|
+
const lcmsBindings = getBioTemplateComponentBindings('lcms-batch')
|
|
291
302
|
const westernBindings = getBioTemplateComponentBindings('western-blot-assay')
|
|
292
303
|
const wellplateImports = toBioTemplateComponentImports('wellplate-screen')
|
|
293
304
|
|
|
@@ -298,11 +309,14 @@ describe('bio data templates', () => {
|
|
|
298
309
|
expect(qpcrBindings.map(binding => binding.component)).toContain('DataFrame')
|
|
299
310
|
expect(elisaBindings.map(binding => binding.component)).toContain('PlateMapEditor')
|
|
300
311
|
expect(elisaBindings.map(binding => binding.component)).toContain('DataFrame')
|
|
312
|
+
expect(lcmsBindings.map(binding => binding.component)).toContain('ScheduleCalendar')
|
|
313
|
+
expect(lcmsBindings.map(binding => binding.component)).toContain('ExperimentTimeline')
|
|
301
314
|
expect(flowBindings.map(binding => binding.template_id)).toEqual([
|
|
302
315
|
'sample-sheet',
|
|
303
316
|
'sample-sheet',
|
|
304
317
|
'flow-cytometry-panel',
|
|
305
318
|
'assay-matrix',
|
|
319
|
+
'assay-matrix',
|
|
306
320
|
])
|
|
307
321
|
expect(westernBindings.map(binding => binding.component)).toContain('ReagentList')
|
|
308
322
|
expect(westernBindings.map(binding => binding.component)).toContain('ExperimentTimeline')
|
|
@@ -412,6 +426,7 @@ describe('bio data templates', () => {
|
|
|
412
426
|
expect(componentPropsById['plate-map:WellPlate'].wells).toBeDefined()
|
|
413
427
|
expect(componentPropsById['calibration-curve:DataFrame'].data).toBeDefined()
|
|
414
428
|
expect(componentPropsById['assay-matrix:DataFrame'].columns).toBeDefined()
|
|
429
|
+
expect(componentPropsById['assay-matrix:SampleSelector'].samples).toEqual(['Control', 'Treatment'])
|
|
415
430
|
})
|
|
416
431
|
|
|
417
432
|
it('generates Vue snippets for concrete template component props', () => {
|
|
@@ -556,6 +571,7 @@ describe('bio data templates', () => {
|
|
|
556
571
|
expect(toInstrumentRunRows(lcmsTemplates['instrument-run'] as InstrumentRunTemplate)[0].kind).toBe('blank')
|
|
557
572
|
expect(toInstrumentRunRows(lcmsTemplates['instrument-run'] as InstrumentRunTemplate)[2].sampleId).toBe('s001')
|
|
558
573
|
expect(toTemplateDataFrame(lcmsTemplates['assay-matrix']).columns.map(column => column.key)).toContain('glucose')
|
|
574
|
+
expect(toAssayMatrixSampleOptions(lcmsTemplates['assay-matrix'] as AssayMatrixTemplate).map(option => option.label)).toEqual(['S001', 'S002'])
|
|
559
575
|
expect(Object.keys(flowTemplates)).toEqual(['sample-sheet', 'flow-cytometry-panel', 'assay-matrix'])
|
|
560
576
|
expect(flow.metadata?.preset).toBe('flow-cytometry-assay')
|
|
561
577
|
expect(flowTemplates['flow-cytometry-panel'].data).toMatchObject({
|
|
@@ -921,6 +937,10 @@ describe('bio data templates', () => {
|
|
|
921
937
|
const assayFrame = toAssayMatrixDataFrame(template)
|
|
922
938
|
expect(assayFrame.rowKey).toBe('sampleId')
|
|
923
939
|
expect(assayFrame.data[0].lactate).toBe(1.2)
|
|
940
|
+
expect(toAssayMatrixSampleOptions(template)).toEqual([
|
|
941
|
+
{ value: 's001', label: 'S001', description: undefined },
|
|
942
|
+
{ value: 's002', label: 'S002', description: undefined },
|
|
943
|
+
])
|
|
924
944
|
expect(toTemplateDataFrame(template).columns.map(column => column.key)).toContain('glucose')
|
|
925
945
|
})
|
|
926
946
|
|
|
@@ -978,6 +998,11 @@ describe('bio data templates', () => {
|
|
|
978
998
|
})
|
|
979
999
|
const rows = toInstrumentRunRows(template)
|
|
980
1000
|
const frame = toInstrumentRunDataFrame(template)
|
|
1001
|
+
const steps = toInstrumentRunSteps(template)
|
|
1002
|
+
const events = toInstrumentRunScheduleEvents(template)
|
|
1003
|
+
const componentProps = toBioTemplateComponentProps(template)
|
|
1004
|
+
const calendar = componentProps.find(binding => binding.component === 'ScheduleCalendar')
|
|
1005
|
+
const timeline = componentProps.find(binding => binding.component === 'ExperimentTimeline')
|
|
981
1006
|
|
|
982
1007
|
expect(template.template_id).toBe('instrument-run')
|
|
983
1008
|
expect(template.data.items.map(item => item.kind)).toEqual(['blank', 'qc', 'sample', 'sample', 'qc'])
|
|
@@ -987,6 +1012,25 @@ describe('bio data templates', () => {
|
|
|
987
1012
|
expect(frame.rowKey).toBe('id')
|
|
988
1013
|
expect(frame.columns.map(column => column.key)).toContain('status')
|
|
989
1014
|
expect(toTemplateDataFrame(template).data[3].sampleId).toBe('s002')
|
|
1015
|
+
expect(steps[2]).toMatchObject({
|
|
1016
|
+
id: 's001-run',
|
|
1017
|
+
type: 'measurement',
|
|
1018
|
+
name: 'S001',
|
|
1019
|
+
status: 'pending',
|
|
1020
|
+
order: 3,
|
|
1021
|
+
})
|
|
1022
|
+
expect(steps[2].parameters?.method).toBe('Default method')
|
|
1023
|
+
expect(events[0]).toMatchObject({
|
|
1024
|
+
id: 'blank-start',
|
|
1025
|
+
title: 'Blank start',
|
|
1026
|
+
start: '2024-01-01T08:00:00.000Z',
|
|
1027
|
+
end: '2024-01-01T08:10:00.000Z',
|
|
1028
|
+
status: 'pending',
|
|
1029
|
+
})
|
|
1030
|
+
expect(events[2].title).toBe('S001')
|
|
1031
|
+
expect(calendar?.propsObject.modelValue).toBe(events[0].start)
|
|
1032
|
+
expect(calendar?.propsObject.events).toEqual(events)
|
|
1033
|
+
expect(timeline?.propsObject.modelValue).toEqual(steps)
|
|
990
1034
|
})
|
|
991
1035
|
|
|
992
1036
|
it('creates qPCR plate templates and adapts reactions to dataframe and wells', () => {
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* the `panels` config and rendered as CollapsibleCards. Controls can be
|
|
7
7
|
* provided through named slots or auto-rendered from FormBuilder schemas.
|
|
8
8
|
*
|
|
9
|
-
* When
|
|
9
|
+
* When activeView is omitted, the first non-empty panel view is selected.
|
|
10
|
+
* When no view has matching panels, the sidebar hides entirely.
|
|
10
11
|
*
|
|
11
12
|
* @example
|
|
12
13
|
* ```vue
|
|
@@ -52,7 +53,7 @@ interface Props {
|
|
|
52
53
|
variant?: 'default' | 'analysis'
|
|
53
54
|
/** Map of view IDs to their tool sections */
|
|
54
55
|
panels?: Record<string, SidebarToolSection[]>
|
|
55
|
-
/** Which view's panels to display */
|
|
56
|
+
/** Which view's panels to display. Defaults to the first non-empty panel view. */
|
|
56
57
|
activeView?: string
|
|
57
58
|
/** Floating variant with absolute positioning. Defaults to false for analysis variant. */
|
|
58
59
|
floating?: boolean
|
|
@@ -219,7 +220,6 @@ const resolvedValues = computed<Record<string, unknown>>(() => ({
|
|
|
219
220
|
const resolvedActiveView = computed(() => {
|
|
220
221
|
if (props.activeView) return props.activeView
|
|
221
222
|
if (props.defaultView) return props.defaultView
|
|
222
|
-
if (!resolvedControls.value) return ''
|
|
223
223
|
return firstVisibleViewId(resolvedPanels.value)
|
|
224
224
|
})
|
|
225
225
|
|
|
@@ -20,8 +20,8 @@ import ThemeToggle from './ThemeToggle.vue'
|
|
|
20
20
|
import SettingsModal from './SettingsModal.vue'
|
|
21
21
|
import ExperimentPopover from './ExperimentPopover.vue'
|
|
22
22
|
import ExperimentSelectorModal from './ExperimentSelectorModal.vue'
|
|
23
|
-
import
|
|
24
|
-
import
|
|
23
|
+
import AppTopBarPageSelectorInternal from './internal/AppTopBarPageSelectorInternal.vue'
|
|
24
|
+
import AppTopBarPillNavInternal from './internal/AppTopBarPillNavInternal.vue'
|
|
25
25
|
import AppAvatarMenu from './AppAvatarMenu.vue'
|
|
26
26
|
import AppPluginSwitcher from './AppPluginSwitcher.vue'
|
|
27
27
|
import PluginIcon from './PluginIcon.vue'
|
|
@@ -126,9 +126,9 @@ const profileInitial = computed(() => {
|
|
|
126
126
|
})
|
|
127
127
|
|
|
128
128
|
const hasPlatformPageSelector = computed(() =>
|
|
129
|
-
props.pageSelector === undefined &&
|
|
129
|
+
props.pageSelector === undefined && (plugin.value?.nav_items?.length ?? 0) > 1,
|
|
130
130
|
)
|
|
131
|
-
const hasPageSelector = computed(() =>
|
|
131
|
+
const hasPageSelector = computed(() => (props.pageSelector?.length ?? 0) > 1 || hasPlatformPageSelector.value)
|
|
132
132
|
const hasPluginSwitcher = computed(() => !!props.pluginSwitcher)
|
|
133
133
|
const hasPillNav = computed(() => !!props.pillNav?.length)
|
|
134
134
|
const hasAccountMenu = computed(() => !!props.accountMenu?.length)
|
|
@@ -295,7 +295,7 @@ function currentItemIdFromLocation(pages: Array<Pick<PageSelectorItem, 'id' | 't
|
|
|
295
295
|
@select="emit('plugin-switcher-select', $event)"
|
|
296
296
|
@install-click="emit('plugin-switcher-install')"
|
|
297
297
|
/>
|
|
298
|
-
<
|
|
298
|
+
<AppTopBarPageSelectorInternal
|
|
299
299
|
v-else-if="hasPageSelector"
|
|
300
300
|
:pages="normalizedPageSelector"
|
|
301
301
|
:current-page-id="effectiveCurrentPageSelectorId"
|
|
@@ -307,7 +307,7 @@ function currentItemIdFromLocation(pages: Array<Pick<PageSelectorItem, 'id' | 't
|
|
|
307
307
|
<template v-if="$slots['page-selector-item-icon']" #item-icon="slotProps">
|
|
308
308
|
<slot name="page-selector-item-icon" v-bind="slotProps" />
|
|
309
309
|
</template>
|
|
310
|
-
</
|
|
310
|
+
</AppTopBarPageSelectorInternal>
|
|
311
311
|
|
|
312
312
|
<!-- Left: title -->
|
|
313
313
|
<div v-if="hasTitleGroup" class="mint-topbar-title-group">
|
|
@@ -323,7 +323,7 @@ function currentItemIdFromLocation(pages: Array<Pick<PageSelectorItem, 'id' | 't
|
|
|
323
323
|
<!-- Center: pill nav (new) -->
|
|
324
324
|
<div v-if="hasPillNav || $slots.center" class="mint-topbar__center">
|
|
325
325
|
<slot name="center">
|
|
326
|
-
<
|
|
326
|
+
<AppTopBarPillNavInternal
|
|
327
327
|
v-if="hasPillNav && pillNav"
|
|
328
328
|
:items="normalizedPillNav"
|
|
329
329
|
:current-item-id="currentPillId"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
/** Complete editable
|
|
2
|
+
/** Complete editable biology template preset workspace for WellPlate/DoseCalculator, LC-MS, qPCR, generated controls, preview components, and current-experiment save wiring. */
|
|
3
3
|
import { computed, effectScope, onScopeDispose, shallowRef, toRaw, unref, watch, type EffectScope } from 'vue'
|
|
4
4
|
import type { BioTemplateControlValues, TemplatePresetId } from '../templates'
|
|
5
5
|
import type {
|
|
@@ -11,7 +11,7 @@ import AlertBox from './AlertBox.vue'
|
|
|
11
11
|
import AppSidebar from './AppSidebar.vue'
|
|
12
12
|
import BaseButton from './BaseButton.vue'
|
|
13
13
|
import BioTemplateRenderer from './BioTemplateRenderer.vue'
|
|
14
|
-
import
|
|
14
|
+
import AppTopBarPillNavInternal from './internal/AppTopBarPillNavInternal.vue'
|
|
15
15
|
|
|
16
16
|
type BioTemplatePresetSidebarVariant = 'default' | 'analysis'
|
|
17
17
|
|
|
@@ -256,7 +256,7 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
|
256
256
|
<p class="mint-bio-template-preset-workspace__sidebar-title">
|
|
257
257
|
Controls
|
|
258
258
|
</p>
|
|
259
|
-
<
|
|
259
|
+
<AppTopBarPillNavInternal
|
|
260
260
|
v-if="resolvedWorkspace.pillNav.items.length > 1"
|
|
261
261
|
class="mint-bio-template-preset-workspace__view-nav"
|
|
262
262
|
:items="resolvedWorkspace.pillNav.items"
|