@morscherlab/mint-sdk 1.0.0-beta.3 → 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/README.md +9 -2
- package/dist/__tests__/composables/experiment-utils.test.d.ts +1 -0
- package/dist/__tests__/composables/useApi.test.d.ts +1 -0
- package/dist/components/AppContainer.vue.d.ts +1 -1
- package/dist/components/AppLayout.vue.d.ts +20 -1
- package/dist/components/AppSidebar.vue.d.ts +57 -5
- package/dist/components/AppTopBar.vue.d.ts +7 -25
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +3 -1
- package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -0
- package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +5 -0
- package/dist/components/ComponentBindingRenderer.vue.d.ts +44 -0
- package/dist/components/ControlWorkspaceView.vue.d.ts +24 -7
- package/dist/components/DoseDesignWorkspaceView.vue.d.ts +149 -0
- package/dist/components/ExperimentTimeline.vue.d.ts +1 -1
- package/dist/components/FormBuilder.vue.d.ts +9 -9
- package/dist/components/PlateMapEditor.vue.d.ts +1 -1
- package/dist/components/PluginWorkspaceView.vue.d.ts +310 -0
- package/dist/components/SettingsModal.vue.d.ts +1 -1
- package/dist/components/WellPlate.vue.d.ts +2 -2
- package/dist/components/index.d.ts +3 -12
- package/dist/components/index.js +3 -3
- package/dist/components/{AppPageSelector.vue.d.ts → internal/AppTopBarPageSelectorInternal.vue.d.ts} +1 -1
- package/dist/components/{AppPillNav.vue.d.ts → internal/AppTopBarPillNavInternal.vue.d.ts} +3 -1
- package/dist/components/{CalendarGridPanel.vue.d.ts → internal/CalendarGridPanelInternal.vue.d.ts} +1 -1
- package/dist/components/internal/FormSectionRenderer.vue.d.ts +4 -4
- package/dist/components/{WellEditPopup.vue.d.ts → internal/WellEditPopupInternal.vue.d.ts} +1 -1
- package/dist/{components-D_Sr0adg.js → components-DihbSJjU.js} +5932 -5408
- package/dist/components-DihbSJjU.js.map +1 -0
- package/dist/composables/experiment-utils.d.ts +8 -0
- package/dist/composables/index.d.ts +5 -7
- package/dist/composables/index.js +4 -4
- package/dist/composables/useAppExperiment.d.ts +31 -2
- package/dist/composables/useBioTemplateComponents.d.ts +5 -3
- package/dist/composables/useBioTemplatePackWorkspace.d.ts +3 -2
- package/dist/composables/useBioTemplatePresetWorkspace.d.ts +6 -5
- package/dist/composables/useBioTemplateWorkspace.d.ts +5 -4
- package/dist/composables/useControlSchema.d.ts +43 -21
- package/dist/composables/usePluginClient.d.ts +5 -2
- package/dist/{composables-C3dpXQN5.js → composables-BcgZ6diz.js} +40 -28
- package/dist/composables-BcgZ6diz.js.map +1 -0
- package/dist/index.d.ts +5 -12
- package/dist/index.js +5 -5
- package/dist/install.js +2 -2
- package/dist/styles.css +5637 -5663
- package/dist/templates/adapters.d.ts +7 -1
- package/dist/templates/catalog.d.ts +5 -5
- package/dist/templates/componentBindings.d.ts +13 -0
- package/dist/templates/index.d.ts +5 -5
- 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-50NPjaxL.js → templates-Cyt0Suwf.js} +322 -73
- package/dist/templates-Cyt0Suwf.js.map +1 -0
- package/dist/types/components.d.ts +6 -25
- package/dist/types/index.d.ts +1 -1
- package/dist/{useScheduleDrag-D4oWdh41.js → useExperimentData-CM6Y0u5L.js} +400 -357
- package/dist/useExperimentData-CM6Y0u5L.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/ActionItem.test.ts +6 -6
- package/src/__tests__/components/AppLayout.test.ts +44 -0
- package/src/__tests__/components/AppSidebar.test.ts +130 -2
- package/src/__tests__/components/AppToastContainer.test.ts +0 -11
- package/src/__tests__/components/AppTopBar.test.ts +189 -120
- package/src/__tests__/components/{AppPageSelector.test.ts → AppTopBarPageSelector.test.ts} +8 -8
- package/src/__tests__/components/{AppPillNav.test.ts → AppTopBarPillNav.test.ts} +53 -6
- package/src/__tests__/components/BioTemplateExperimentWorkspaceView.test.ts +7 -1
- package/src/__tests__/components/BioTemplatePackWorkspaceView.test.ts +32 -1
- package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +48 -1
- package/src/__tests__/components/BioTemplateRenderer.test.ts +25 -0
- package/src/__tests__/components/CalendarGridPanel.test.ts +3 -3
- package/src/__tests__/components/ComponentBindingRenderer.test.ts +278 -0
- package/src/__tests__/components/ControlWorkspaceView.test.ts +134 -63
- package/src/__tests__/components/DateTimePicker.test.ts +2 -2
- package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +185 -0
- package/src/__tests__/components/PluginWorkspaceView.test.ts +548 -0
- package/src/__tests__/composables/experiment-utils.test.ts +30 -0
- package/src/__tests__/composables/useApi.test.ts +30 -0
- package/src/__tests__/composables/useAppExperiment.test.ts +100 -1
- package/src/__tests__/composables/useBioTemplatePackWorkspace.test.ts +7 -4
- package/src/__tests__/composables/useBioTemplatePresetWorkspace.test.ts +7 -7
- package/src/__tests__/composables/useBioTemplateWorkspace.test.ts +6 -1
- package/src/__tests__/composables/useControlSchema.test.ts +151 -37
- package/src/__tests__/composables/usePluginClient.test.ts +99 -2
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +120 -25
- package/src/__tests__/templates/templates.test.ts +56 -0
- package/src/components/AppAvatarMenu.vue +3 -3
- package/src/components/AppLayout.story.vue +39 -0
- package/src/components/AppLayout.vue +83 -2
- package/src/components/AppPluginSwitcher.vue +5 -5
- package/src/components/AppSidebar.story.vue +113 -5
- package/src/components/AppSidebar.vue +147 -27
- package/src/components/AppTopBar.story.vue +2 -5
- package/src/components/AppTopBar.vue +35 -425
- package/src/components/BioTemplateExperimentWorkspaceView.story.vue +2 -2
- package/src/components/BioTemplateExperimentWorkspaceView.vue +6 -0
- package/src/components/BioTemplatePackWorkspaceView.story.vue +4 -4
- package/src/components/BioTemplatePackWorkspaceView.vue +1 -0
- package/src/components/BioTemplatePresetWorkspaceView.story.vue +14 -2
- package/src/components/BioTemplatePresetWorkspaceView.vue +12 -3
- package/src/components/BioTemplateRenderer.story.vue +2 -2
- package/src/components/BioTemplateRenderer.vue +15 -227
- package/src/components/ComponentBindingRenderer.story.vue +87 -0
- package/src/components/ComponentBindingRenderer.vue +317 -0
- package/src/components/ControlWorkspaceView.story.vue +20 -9
- package/src/components/ControlWorkspaceView.vue +43 -12
- package/src/components/DatePicker.vue +2 -2
- package/src/components/DateTimePicker.vue +2 -2
- package/src/components/DoseDesignWorkspaceView.story.vue +77 -0
- package/src/components/DoseDesignWorkspaceView.vue +255 -0
- package/src/components/ExperimentPopover.story.vue +2 -2
- package/src/components/ExperimentPopover.vue +2 -6
- package/src/components/ExperimentSelectorModal.vue +6 -5
- package/src/components/FormBuilder.story.vue +190 -0
- package/src/components/PluginWorkspaceView.story.vue +334 -0
- package/src/components/PluginWorkspaceView.vue +708 -0
- package/src/components/SettingsModal.story.vue +87 -0
- package/src/components/WellPlate.vue +2 -2
- package/src/components/index.ts +3 -12
- package/src/components/{AppPageSelector.vue → internal/AppTopBarPageSelectorInternal.vue} +9 -9
- package/src/components/internal/AppTopBarPillNavInternal.vue +194 -0
- package/src/components/{CalendarGridPanel.vue → internal/CalendarGridPanelInternal.vue} +1 -1
- package/src/components/{WellEditPopup.vue → internal/WellEditPopupInternal.vue} +3 -3
- package/src/composables/experiment-utils.ts +26 -0
- package/src/composables/index.ts +21 -7
- package/src/composables/useApi.ts +9 -2
- package/src/composables/useAppExperiment.ts +85 -13
- package/src/composables/useBioTemplateComponents.ts +12 -0
- package/src/composables/useBioTemplatePackWorkspace.ts +6 -2
- package/src/composables/useBioTemplatePresetWorkspace.ts +10 -21
- package/src/composables/useBioTemplateWorkspace.ts +6 -4
- package/src/composables/useControlSchema.ts +157 -69
- package/src/composables/usePluginClient.ts +50 -9
- package/src/index.ts +6 -563
- package/src/styles/components/app-layout.css +82 -0
- package/src/styles/components/app-page-selector.css +1 -1
- package/src/styles/components/app-pill-nav.css +71 -1
- package/src/styles/components/app-sidebar.css +119 -0
- package/src/styles/components/app-top-bar.css +0 -235
- package/src/styles/components/experiment-popover.css +2 -2
- package/src/styles/index.css +0 -1
- package/src/templates/adapters.ts +193 -0
- package/src/templates/catalog.ts +5 -5
- package/src/templates/componentBindings.ts +90 -3
- package/src/templates/index.ts +10 -0
- package/src/templates/packs.ts +10 -1
- package/src/templates/presets.ts +14 -4
- package/src/templates/types.ts +4 -0
- package/src/types/components.ts +6 -31
- package/src/types/index.ts +2 -6
- package/dist/__tests__/composables/usePluginApi.test.d.ts +0 -13
- package/dist/components/FormFieldRenderer.vue.d.ts +0 -28
- package/dist/components/FormSection.vue.d.ts +0 -30
- package/dist/components/GroupingModal.vue.d.ts +0 -12
- package/dist/components/SettingsButton.vue.d.ts +0 -30
- package/dist/components/ToastNotification.vue.d.ts +0 -2
- package/dist/components-D_Sr0adg.js.map +0 -1
- package/dist/composables/usePluginApi.d.ts +0 -22
- package/dist/composables-C3dpXQN5.js.map +0 -1
- package/dist/templates-50NPjaxL.js.map +0 -1
- package/dist/useScheduleDrag-D4oWdh41.js.map +0 -1
- package/src/__tests__/components/FormCompatibility.test.ts +0 -94
- package/src/__tests__/components/GroupingModal.test.ts +0 -73
- package/src/__tests__/components/SettingsButton.test.ts +0 -44
- package/src/__tests__/composables/usePluginApi.test.ts +0 -81
- package/src/components/AppPillNav.vue +0 -71
- package/src/components/FormFieldRenderer.vue +0 -35
- package/src/components/FormSection.vue +0 -37
- package/src/components/GroupingModal.story.vue +0 -52
- package/src/components/GroupingModal.vue +0 -61
- package/src/components/SettingsButton.story.vue +0 -58
- package/src/components/SettingsButton.vue +0 -64
- package/src/components/ToastNotification.vue +0 -9
- package/src/composables/usePluginApi.ts +0 -32
- package/src/styles/components/settings-button.css +0 -31
- /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/__tests__/components/{FormCompatibility.test.d.ts → ComponentBindingRenderer.test.d.ts} +0 -0
- /package/dist/__tests__/components/{GroupingModal.test.d.ts → DoseDesignWorkspaceView.test.d.ts} +0 -0
- /package/dist/__tests__/components/{SettingsButton.test.d.ts → PluginWorkspaceView.test.d.ts} +0 -0
- /package/dist/components/{ActionItem.vue.d.ts → internal/ActionItemInternal.vue.d.ts} +0 -0
- /package/src/components/{ActionItem.vue → internal/ActionItemInternal.vue} +0 -0
|
@@ -3,8 +3,11 @@ import { mount } from '@vue/test-utils'
|
|
|
3
3
|
import { computed, ref } from 'vue'
|
|
4
4
|
import { createPinia } from 'pinia'
|
|
5
5
|
import AppTopBar from '../../components/AppTopBar.vue'
|
|
6
|
+
import ExperimentPopover from '../../components/ExperimentPopover.vue'
|
|
7
|
+
import ExperimentSelectorModal from '../../components/ExperimentSelectorModal.vue'
|
|
6
8
|
import { usePlatformContext } from '../../composables/usePlatformContext'
|
|
7
|
-
import {
|
|
9
|
+
import { APP_EXPERIMENT_KEY, type AppExperimentState } from '../../composables/useAppExperiment'
|
|
10
|
+
import { defineControlModel, defineControls } from '../../composables/useControlSchema'
|
|
8
11
|
import ThemeToggle from '../../components/ThemeToggle.vue'
|
|
9
12
|
import SettingsModal from '../../components/SettingsModal.vue'
|
|
10
13
|
import type { TopBarSettingsConfig } from '../../types/components'
|
|
@@ -43,7 +46,15 @@ function createWrapper(props = {}, slots = {}) {
|
|
|
43
46
|
plugins: [pinia],
|
|
44
47
|
stubs: {
|
|
45
48
|
'router-link': {
|
|
46
|
-
|
|
49
|
+
props: ['to', 'custom'],
|
|
50
|
+
template: `
|
|
51
|
+
<slot
|
|
52
|
+
v-if="custom"
|
|
53
|
+
:href="typeof to === 'string' ? to : '#'"
|
|
54
|
+
:navigate="() => {}"
|
|
55
|
+
/>
|
|
56
|
+
<a v-else :href="typeof to === 'string' ? to : '#'"><slot /></a>
|
|
57
|
+
`,
|
|
47
58
|
},
|
|
48
59
|
BaseModal: {
|
|
49
60
|
template: '<div><slot /></div>',
|
|
@@ -525,6 +536,86 @@ describe('AppTopBar', () => {
|
|
|
525
536
|
})
|
|
526
537
|
|
|
527
538
|
describe('combined features', () => {
|
|
539
|
+
it('passes injected app experiment bindings to the popover and selector modal', () => {
|
|
540
|
+
vi.mocked(usePlatformContext).mockReturnValueOnce({
|
|
541
|
+
isIntegrated: computed(() => true),
|
|
542
|
+
context: ref({ isIntegrated: true, theme: 'system' }),
|
|
543
|
+
plugin: computed(() => undefined),
|
|
544
|
+
user: computed(() => undefined),
|
|
545
|
+
theme: computed(() => 'system' as const),
|
|
546
|
+
features: computed(() => undefined),
|
|
547
|
+
navigate: vi.fn(),
|
|
548
|
+
notify: vi.fn(),
|
|
549
|
+
sendToPlatform: vi.fn(),
|
|
550
|
+
})
|
|
551
|
+
const appExperiment: AppExperimentState = {
|
|
552
|
+
experimentName: ref('Dose response'),
|
|
553
|
+
experimentCode: ref('EXP-012'),
|
|
554
|
+
experimentStatus: ref('ongoing'),
|
|
555
|
+
experimentId: ref(12),
|
|
556
|
+
showModal: ref(false),
|
|
557
|
+
saveLoading: ref(false),
|
|
558
|
+
saveSuccessMessage: ref('Saved'),
|
|
559
|
+
showSave: ref(true),
|
|
560
|
+
showDetach: computed(() => true),
|
|
561
|
+
saveDisabled: computed(() => true),
|
|
562
|
+
saveDisabledMessage: computed(() => 'Locked'),
|
|
563
|
+
popover: computed(() => ({
|
|
564
|
+
experimentName: 'Dose response',
|
|
565
|
+
experimentCode: 'EXP-012',
|
|
566
|
+
experimentStatus: 'ongoing',
|
|
567
|
+
showSave: true,
|
|
568
|
+
showDetach: true,
|
|
569
|
+
saveDisabled: true,
|
|
570
|
+
saveDisabledMessage: 'Locked',
|
|
571
|
+
saveLoading: false,
|
|
572
|
+
saveSuccessMessage: 'Saved',
|
|
573
|
+
})),
|
|
574
|
+
selectorModal: computed(() => ({
|
|
575
|
+
modelValue: false,
|
|
576
|
+
currentExperimentId: 12,
|
|
577
|
+
})),
|
|
578
|
+
openModal: vi.fn(),
|
|
579
|
+
closeModal: vi.fn(),
|
|
580
|
+
handleSelect: vi.fn(),
|
|
581
|
+
handleSave: vi.fn(),
|
|
582
|
+
handleDetach: vi.fn(),
|
|
583
|
+
}
|
|
584
|
+
const wrapper = mount(AppTopBar, {
|
|
585
|
+
props: { title: 'Test App' },
|
|
586
|
+
global: {
|
|
587
|
+
plugins: [createPinia()],
|
|
588
|
+
provide: {
|
|
589
|
+
[APP_EXPERIMENT_KEY as symbol]: appExperiment,
|
|
590
|
+
},
|
|
591
|
+
stubs: {
|
|
592
|
+
'router-link': {
|
|
593
|
+
template: '<a><slot /></a>',
|
|
594
|
+
},
|
|
595
|
+
BaseModal: {
|
|
596
|
+
template: '<div><slot /></div>',
|
|
597
|
+
},
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
expect(wrapper.findComponent(ExperimentPopover).props()).toMatchObject({
|
|
603
|
+
experimentName: 'Dose response',
|
|
604
|
+
experimentCode: 'EXP-012',
|
|
605
|
+
experimentStatus: 'ongoing',
|
|
606
|
+
showSave: true,
|
|
607
|
+
showDetach: true,
|
|
608
|
+
saveDisabled: true,
|
|
609
|
+
saveDisabledMessage: 'Locked',
|
|
610
|
+
saveLoading: false,
|
|
611
|
+
saveSuccessMessage: 'Saved',
|
|
612
|
+
})
|
|
613
|
+
expect(wrapper.findComponent(ExperimentSelectorModal).props()).toMatchObject({
|
|
614
|
+
modelValue: false,
|
|
615
|
+
currentExperimentId: 12,
|
|
616
|
+
})
|
|
617
|
+
})
|
|
618
|
+
|
|
528
619
|
it('should render ThemeToggle and settings button together', () => {
|
|
529
620
|
const wrapper = createWrapper({
|
|
530
621
|
title: 'Test App',
|
|
@@ -591,8 +682,8 @@ describe('AppTopBar', () => {
|
|
|
591
682
|
})
|
|
592
683
|
})
|
|
593
684
|
|
|
594
|
-
describe('
|
|
595
|
-
it('
|
|
685
|
+
describe('navigation prop surface', () => {
|
|
686
|
+
it('renders a title-only layout without navigation props', () => {
|
|
596
687
|
const wrapper = createWrapper({
|
|
597
688
|
title: 'Classic App',
|
|
598
689
|
subtitle: 'Old API',
|
|
@@ -605,120 +696,113 @@ describe('AppTopBar', () => {
|
|
|
605
696
|
expect(wrapper.find('.mint-topbar__standalone-badge').exists()).toBe(true)
|
|
606
697
|
})
|
|
607
698
|
|
|
608
|
-
it('
|
|
699
|
+
it('composes page selector, pill nav, theme, and settings controls', () => {
|
|
609
700
|
const wrapper = createWrapper({
|
|
610
|
-
pluginName: 'Test Plugin',
|
|
611
701
|
title: 'Dashboard',
|
|
612
|
-
|
|
702
|
+
pageSelector: [
|
|
613
703
|
{ id: 'dashboard', label: 'Dashboard' },
|
|
614
704
|
{ id: 'settings', label: 'Settings' },
|
|
615
705
|
],
|
|
616
|
-
|
|
706
|
+
currentPageSelectorId: 'dashboard',
|
|
707
|
+
pillNav: [
|
|
617
708
|
{ id: 'overview', label: 'Overview' },
|
|
618
709
|
{ id: 'details', label: 'Details' },
|
|
619
710
|
],
|
|
711
|
+
currentPillId: 'overview',
|
|
620
712
|
showThemeToggle: true,
|
|
621
713
|
showSettings: true,
|
|
622
714
|
})
|
|
623
715
|
|
|
624
|
-
expect(wrapper.text()).
|
|
625
|
-
expect(wrapper.text()).
|
|
716
|
+
expect(wrapper.find('.mint-page-selector__label').text()).toBe('Dashboard')
|
|
717
|
+
expect(wrapper.findAll('.mint-pill-nav__item').map(item => item.text())).toEqual([
|
|
718
|
+
'Overview',
|
|
719
|
+
'Details',
|
|
720
|
+
])
|
|
721
|
+
expect(wrapper.findAll('.mint-pill-nav__item')[0].classes()).toContain('mint-pill-nav__item--active')
|
|
626
722
|
expect(wrapper.findComponent(ThemeToggle).exists()).toBe(true)
|
|
627
723
|
expect(wrapper.find('.mint-topbar__settings-btn').exists()).toBe(true)
|
|
628
724
|
})
|
|
629
725
|
|
|
630
|
-
it('renders
|
|
726
|
+
it('renders pill nav items with icon metadata', async () => {
|
|
631
727
|
const runIcon = 'M8 5v14l11-7z'
|
|
632
|
-
const controls = defineControls({
|
|
633
|
-
threshold: {
|
|
634
|
-
default: 0.05,
|
|
635
|
-
section: 'parameters',
|
|
636
|
-
view: 'analysis',
|
|
637
|
-
},
|
|
638
|
-
chartScale: {
|
|
639
|
-
default: 'linear',
|
|
640
|
-
section: 'display',
|
|
641
|
-
view: 'results',
|
|
642
|
-
},
|
|
643
|
-
})
|
|
644
728
|
const wrapper = createWrapper({
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
}),
|
|
651
|
-
currentTabId: 'analysis',
|
|
729
|
+
pillNav: [
|
|
730
|
+
{ id: 'analysis', label: 'Run', icon: runIcon },
|
|
731
|
+
{ id: 'results', label: 'Results' },
|
|
732
|
+
],
|
|
733
|
+
currentPillId: 'analysis',
|
|
652
734
|
})
|
|
653
735
|
|
|
654
|
-
const
|
|
655
|
-
expect(
|
|
656
|
-
expect(
|
|
657
|
-
expect(
|
|
658
|
-
expect(
|
|
736
|
+
const pills = wrapper.findAll('.mint-pill-nav__item')
|
|
737
|
+
expect(pills.map(pill => pill.text())).toEqual(['Run', 'Results'])
|
|
738
|
+
expect(pills[0].classes()).toContain('mint-pill-nav__item--active')
|
|
739
|
+
expect(pills[0].get('.mint-pill-nav__icon path').attributes('d')).toBe(runIcon)
|
|
740
|
+
expect(pills[1].find('.mint-pill-nav__icon').exists()).toBe(false)
|
|
659
741
|
|
|
660
|
-
await
|
|
661
|
-
expect(wrapper.emitted('
|
|
742
|
+
await pills[1].trigger('click')
|
|
743
|
+
expect(wrapper.emitted('pill-select')?.[0][0]).toMatchObject({ id: 'results', label: 'Results' })
|
|
662
744
|
})
|
|
663
745
|
|
|
664
|
-
it('uses shared dropdown state for the
|
|
746
|
+
it('uses shared dropdown state for the page selector menu', async () => {
|
|
665
747
|
const wrapper = createWrapper({
|
|
666
|
-
|
|
667
|
-
pages: [
|
|
748
|
+
pageSelector: [
|
|
668
749
|
{ id: 'dashboard', label: 'Dashboard' },
|
|
669
750
|
{ id: 'settings', label: 'Settings' },
|
|
670
751
|
],
|
|
752
|
+
currentPageSelectorId: 'dashboard',
|
|
671
753
|
})
|
|
672
|
-
const dropdown = () => wrapper.
|
|
754
|
+
const dropdown = () => wrapper.find('.mint-page-selector__menu')
|
|
673
755
|
|
|
674
|
-
expect(dropdown().
|
|
756
|
+
expect(dropdown().exists()).toBe(false)
|
|
675
757
|
|
|
676
|
-
await wrapper.get('.mint-
|
|
677
|
-
expect(dropdown().
|
|
758
|
+
await wrapper.get('.mint-page-selector__trigger').trigger('click')
|
|
759
|
+
expect(dropdown().exists()).toBe(true)
|
|
678
760
|
|
|
679
761
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
|
|
680
762
|
await wrapper.vm.$nextTick()
|
|
681
|
-
expect(dropdown().
|
|
763
|
+
expect(dropdown().exists()).toBe(false)
|
|
682
764
|
|
|
683
|
-
await wrapper.get('.mint-
|
|
684
|
-
expect(dropdown().
|
|
765
|
+
await wrapper.get('.mint-page-selector__trigger').trigger('click')
|
|
766
|
+
expect(dropdown().exists()).toBe(true)
|
|
685
767
|
|
|
686
768
|
document.body.dispatchEvent(new MouseEvent('click', { bubbles: true }))
|
|
687
769
|
await wrapper.vm.$nextTick()
|
|
688
|
-
expect(dropdown().
|
|
770
|
+
expect(dropdown().exists()).toBe(false)
|
|
689
771
|
|
|
690
772
|
wrapper.unmount()
|
|
691
773
|
})
|
|
692
774
|
|
|
693
|
-
it('renders PluginIcon-compatible metadata icons in the
|
|
775
|
+
it('renders PluginIcon-compatible metadata icons in the page selector menu', async () => {
|
|
694
776
|
const iconPath = 'M4 19h16M7 16V8m5 8V4m5 12v-6'
|
|
695
777
|
const pngIcon = 'data:image/png;base64,iVBORw0KGgo='
|
|
696
778
|
const wrapper = createWrapper({
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
{ id: 'dashboard', label: 'Dashboard', to: '/', icon: iconPath, description: 'Overview' },
|
|
779
|
+
pageSelector: [
|
|
780
|
+
{ id: 'dashboard', label: 'Dashboard', to: '/', icon: iconPath, hint: 'Overview' },
|
|
700
781
|
{ id: 'image', label: 'Image', to: '/image', icon: pngIcon },
|
|
701
|
-
{ id: '
|
|
782
|
+
{ id: 'text', label: 'Text', icon: 'home' },
|
|
702
783
|
],
|
|
703
|
-
|
|
784
|
+
currentPageSelectorId: 'dashboard',
|
|
704
785
|
})
|
|
705
786
|
|
|
706
|
-
await wrapper.get('.mint-
|
|
787
|
+
await wrapper.get('.mint-page-selector__trigger').trigger('click')
|
|
707
788
|
|
|
708
|
-
const icons = wrapper
|
|
789
|
+
const icons = wrapper
|
|
790
|
+
.findAllComponents({ name: 'PluginIcon' })
|
|
791
|
+
.filter(icon => icon.classes().includes('mint-page-selector__metadata-icon'))
|
|
709
792
|
expect(icons).toHaveLength(2)
|
|
710
793
|
expect(icons[0].props('icon')).toBe(iconPath)
|
|
711
794
|
expect(icons[1].props('icon')).toBe(pngIcon)
|
|
712
795
|
expect(icons.every((icon) => icon.props('variant') === 'tinted')).toBe(true)
|
|
713
796
|
|
|
714
797
|
const pathIcons = wrapper.findAll('.mint-plugin-icon__svg')
|
|
715
|
-
expect(pathIcons).
|
|
716
|
-
|
|
798
|
+
expect(pathIcons.map(icon => icon.find('path').attributes('d'))).toEqual(
|
|
799
|
+
expect.arrayContaining([iconPath]),
|
|
800
|
+
)
|
|
717
801
|
|
|
718
802
|
const imageIcons = wrapper.findAll('.mint-plugin-icon__img')
|
|
719
803
|
expect(imageIcons).toHaveLength(1)
|
|
720
804
|
expect(imageIcons[0].attributes('src')).toBe(pngIcon)
|
|
721
|
-
expect(wrapper.find('.mint-
|
|
805
|
+
expect(wrapper.find('.mint-page-selector__item-hint').text()).toBe('Overview')
|
|
722
806
|
})
|
|
723
807
|
})
|
|
724
808
|
|
|
@@ -926,7 +1010,7 @@ describe('AppTopBar', () => {
|
|
|
926
1010
|
expect(wrapper.find('.mint-topbar__logo-text').text()).toBe('M')
|
|
927
1011
|
})
|
|
928
1012
|
|
|
929
|
-
it('uses platform plugin nav metadata
|
|
1013
|
+
it('uses platform plugin nav metadata as the page selector when pageSelector is omitted', async () => {
|
|
930
1014
|
const previousPath = window.location.pathname
|
|
931
1015
|
window.history.pushState({}, '', '/test/analysis')
|
|
932
1016
|
try {
|
|
@@ -949,25 +1033,28 @@ describe('AppTopBar', () => {
|
|
|
949
1033
|
})
|
|
950
1034
|
|
|
951
1035
|
const wrapper = createWrapper()
|
|
952
|
-
await wrapper.get('.mint-
|
|
1036
|
+
await wrapper.get('.mint-page-selector__trigger').trigger('click')
|
|
953
1037
|
|
|
954
|
-
expect(wrapper.get('.mint-
|
|
955
|
-
expect(wrapper.
|
|
956
|
-
expect(wrapper.findAll('.mint-topbar-dropdown-item__label').map(item => item.text())).toEqual([
|
|
1038
|
+
expect(wrapper.get('.mint-page-selector__label').text()).toBe('Analysis')
|
|
1039
|
+
expect(wrapper.findAll('.mint-page-selector__item-label').map(item => item.text())).toEqual([
|
|
957
1040
|
'Dashboard',
|
|
958
1041
|
'Analysis',
|
|
959
1042
|
])
|
|
1043
|
+
expect(wrapper.findAll('.mint-page-selector__item-hint').map(item => item.text())).toEqual([
|
|
1044
|
+
'Dose Designer',
|
|
1045
|
+
'Dose Designer',
|
|
1046
|
+
])
|
|
960
1047
|
const dropdownIcons = wrapper
|
|
961
1048
|
.findAllComponents({ name: 'PluginIcon' })
|
|
962
|
-
.filter(icon => icon.classes().includes('mint-
|
|
1049
|
+
.filter(icon => icon.classes().includes('mint-page-selector__metadata-icon'))
|
|
963
1050
|
expect(dropdownIcons.map(icon => icon.props('icon'))).toEqual([pluginIcon, analysisIcon])
|
|
964
|
-
expect(wrapper.findAll('.mint-
|
|
1051
|
+
expect(wrapper.findAll('.mint-page-selector__item')[1].classes()).toContain('mint-page-selector__item--active')
|
|
965
1052
|
} finally {
|
|
966
1053
|
window.history.pushState({}, '', previousPath)
|
|
967
1054
|
}
|
|
968
1055
|
})
|
|
969
1056
|
|
|
970
|
-
it('keeps explicit
|
|
1057
|
+
it('keeps explicit pageSelector ahead of platform plugin nav metadata', async () => {
|
|
971
1058
|
mockPlatformCtx({
|
|
972
1059
|
isIntegrated: true,
|
|
973
1060
|
plugin: {
|
|
@@ -976,18 +1063,24 @@ describe('AppTopBar', () => {
|
|
|
976
1063
|
version: '1.0',
|
|
977
1064
|
route_prefix: '/test',
|
|
978
1065
|
api_prefix: '/api/test',
|
|
979
|
-
nav_items: [
|
|
1066
|
+
nav_items: [
|
|
1067
|
+
{ path: '/stale', label: 'Stale' },
|
|
1068
|
+
{ path: '/stale-two', label: 'Stale Two' },
|
|
1069
|
+
],
|
|
980
1070
|
},
|
|
981
1071
|
})
|
|
982
1072
|
|
|
983
1073
|
const wrapper = createWrapper({
|
|
984
|
-
|
|
985
|
-
|
|
1074
|
+
pageSelector: [
|
|
1075
|
+
{ id: 'overview', label: 'Overview', to: '/' },
|
|
1076
|
+
{ id: 'reports', label: 'Reports', to: '/reports' },
|
|
1077
|
+
],
|
|
1078
|
+
currentPageSelectorId: 'overview',
|
|
986
1079
|
})
|
|
987
|
-
await wrapper.get('.mint-
|
|
1080
|
+
await wrapper.get('.mint-page-selector__trigger').trigger('click')
|
|
988
1081
|
|
|
989
|
-
expect(wrapper.get('.mint-
|
|
990
|
-
expect(wrapper.findAll('.mint-
|
|
1082
|
+
expect(wrapper.get('.mint-page-selector__label').text()).toBe('Overview')
|
|
1083
|
+
expect(wrapper.findAll('.mint-page-selector__item-label').map(item => item.text())).toEqual(['Overview', 'Reports'])
|
|
991
1084
|
})
|
|
992
1085
|
})
|
|
993
1086
|
|
|
@@ -1026,59 +1119,35 @@ describe('AppTopBar', () => {
|
|
|
1026
1119
|
])
|
|
1027
1120
|
})
|
|
1028
1121
|
|
|
1029
|
-
it('
|
|
1030
|
-
const wrapper = createWrapper({
|
|
1031
|
-
pluginName: 'Analysis',
|
|
1032
|
-
currentPageId: 'Overview',
|
|
1033
|
-
pages: ['Overview', 'Settings'],
|
|
1034
|
-
})
|
|
1035
|
-
|
|
1036
|
-
await wrapper.get('.mint-topbar-plugin-name').trigger('click')
|
|
1037
|
-
await wrapper.findAll('.mint-topbar-dropdown-item')[1].trigger('click')
|
|
1038
|
-
|
|
1039
|
-
expect(wrapper.emitted('page-select')).toEqual([
|
|
1040
|
-
[{ id: 'Settings', label: 'Settings' }],
|
|
1041
|
-
])
|
|
1042
|
-
})
|
|
1043
|
-
|
|
1044
|
-
it('accepts string shorthand for legacy tabs', async () => {
|
|
1045
|
-
const wrapper = createWrapper({
|
|
1046
|
-
currentTabId: 'Run',
|
|
1047
|
-
tabs: ['Run', 'Results'],
|
|
1048
|
-
})
|
|
1049
|
-
|
|
1050
|
-
expect(wrapper.findAll('.mint-topbar-tab').map(tab => tab.text())).toEqual([
|
|
1051
|
-
'Run',
|
|
1052
|
-
'Results',
|
|
1053
|
-
])
|
|
1054
|
-
|
|
1055
|
-
await wrapper.findAll('.mint-topbar-tab')[1].trigger('click')
|
|
1056
|
-
|
|
1057
|
-
expect(wrapper.emitted('tab-select')).toEqual([
|
|
1058
|
-
[{ id: 'Results', label: 'Results' }],
|
|
1059
|
-
])
|
|
1060
|
-
})
|
|
1061
|
-
|
|
1062
|
-
it('accepts string shorthand for legacy tab dropdown options', async () => {
|
|
1122
|
+
it('emits pill-option-select for pillNav dropdown children', async () => {
|
|
1063
1123
|
const wrapper = createWrapper({
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
{ id: '
|
|
1124
|
+
currentPillId: 'pca',
|
|
1125
|
+
pillNav: [
|
|
1126
|
+
{ id: 'charts', label: 'Charts' },
|
|
1127
|
+
{
|
|
1128
|
+
id: 'visualize',
|
|
1129
|
+
label: 'Visualize',
|
|
1130
|
+
children: [
|
|
1131
|
+
{ id: 'pca', label: 'PCA', description: 'Principal component analysis' },
|
|
1132
|
+
{ id: 'heatmap', label: 'Heatmap', description: 'Metabolite heatmap' },
|
|
1133
|
+
],
|
|
1134
|
+
},
|
|
1067
1135
|
],
|
|
1068
1136
|
})
|
|
1069
1137
|
|
|
1070
|
-
await wrapper.
|
|
1071
|
-
|
|
1072
|
-
'Summary',
|
|
1073
|
-
'Details',
|
|
1074
|
-
])
|
|
1075
|
-
|
|
1076
|
-
await wrapper.findAll('.mint-topbar-dropdown-item')[1].trigger('click')
|
|
1138
|
+
await wrapper.findAll('.mint-pill-nav__item')[1].trigger('click')
|
|
1139
|
+
await wrapper.findAll('.mint-pill-nav__dropdown-item')[1].trigger('click')
|
|
1077
1140
|
|
|
1078
|
-
expect(wrapper.emitted('
|
|
1079
|
-
id: '
|
|
1080
|
-
label: '
|
|
1141
|
+
expect(wrapper.emitted('pill-option-select')?.[0]?.[0]).toEqual({
|
|
1142
|
+
id: 'heatmap',
|
|
1143
|
+
label: 'Heatmap',
|
|
1144
|
+
description: 'Metabolite heatmap',
|
|
1145
|
+
})
|
|
1146
|
+
expect(wrapper.emitted('pill-option-select')?.[0]?.[1]).toMatchObject({
|
|
1147
|
+
id: 'visualize',
|
|
1148
|
+
label: 'Visualize',
|
|
1081
1149
|
})
|
|
1082
1150
|
})
|
|
1151
|
+
|
|
1083
1152
|
})
|
|
1084
1153
|
})
|
|
@@ -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('AppPageSelector', () => {
|
|
|
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('AppPageSelector', () => {
|
|
|
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('AppPageSelector', () => {
|
|
|
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('AppPageSelector', () => {
|
|
|
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('AppPageSelector', () => {
|
|
|
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('AppPillNav', () => {
|
|
|
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('AppPillNav', () => {
|
|
|
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('AppPillNav', () => {
|
|
|
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 },
|
|
@@ -75,4 +75,51 @@ describe('AppPillNav', () => {
|
|
|
75
75
|
|
|
76
76
|
expect(wrapper.emitted('select')).toBeUndefined()
|
|
77
77
|
})
|
|
78
|
+
|
|
79
|
+
it('supports dropdown child items for grouped pill navigation', async () => {
|
|
80
|
+
const wrapper = mount(AppTopBarPillNavInternal, {
|
|
81
|
+
props: {
|
|
82
|
+
currentItemId: 'pca',
|
|
83
|
+
items: [
|
|
84
|
+
{ id: 'charts', label: 'Charts' },
|
|
85
|
+
{
|
|
86
|
+
id: 'visualize',
|
|
87
|
+
label: 'Visualize',
|
|
88
|
+
children: [
|
|
89
|
+
{ id: 'pca', label: 'PCA', description: 'Principal component analysis' },
|
|
90
|
+
{ id: 'heatmap', label: 'Heatmap', description: 'Metabolite heatmap' },
|
|
91
|
+
],
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
const visualizeButton = wrapper.findAll('.mint-pill-nav__item')[1]
|
|
98
|
+
expect(visualizeButton.classes()).toContain('mint-pill-nav__item--active')
|
|
99
|
+
expect(wrapper.find('.mint-pill-nav__dropdown').exists()).toBe(false)
|
|
100
|
+
|
|
101
|
+
await visualizeButton.trigger('click')
|
|
102
|
+
|
|
103
|
+
expect(wrapper.find('.mint-pill-nav__dropdown').exists()).toBe(true)
|
|
104
|
+
expect(wrapper.findAll('.mint-pill-nav__dropdown-item').map(item => item.text())).toEqual([
|
|
105
|
+
'PCAPrincipal component analysis',
|
|
106
|
+
'HeatmapMetabolite heatmap',
|
|
107
|
+
])
|
|
108
|
+
|
|
109
|
+
await wrapper.findAll('.mint-pill-nav__dropdown-item')[1].trigger('click')
|
|
110
|
+
|
|
111
|
+
expect(wrapper.emitted('option-select')).toEqual([
|
|
112
|
+
[
|
|
113
|
+
{ id: 'heatmap', label: 'Heatmap', description: 'Metabolite heatmap' },
|
|
114
|
+
{
|
|
115
|
+
id: 'visualize',
|
|
116
|
+
label: 'Visualize',
|
|
117
|
+
children: [
|
|
118
|
+
{ id: 'pca', label: 'PCA', description: 'Principal component analysis' },
|
|
119
|
+
{ id: 'heatmap', label: 'Heatmap', description: 'Metabolite heatmap' },
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
])
|
|
124
|
+
})
|
|
78
125
|
})
|
|
@@ -118,12 +118,15 @@ describe('BioTemplateExperimentWorkspaceView', () => {
|
|
|
118
118
|
kind: 'collection',
|
|
119
119
|
},
|
|
120
120
|
slots: {
|
|
121
|
-
default: ({ bindings, componentPropsById, getComponentProps }) => h(
|
|
121
|
+
default: ({ bindings, componentBindingsById, componentPropsById, getComponentProps }) => h(
|
|
122
122
|
'pre',
|
|
123
123
|
{ class: 'component-props' },
|
|
124
124
|
JSON.stringify({
|
|
125
125
|
doseMode: componentPropsById['dose-response:DoseCalculator'].mode,
|
|
126
|
+
bindingComponent: componentBindingsById['dose-response:DoseCalculator'].component,
|
|
127
|
+
bindingMode: componentBindingsById['dose-response:DoseCalculator'].props.mode,
|
|
126
128
|
bindingDoseMode: bindings.componentPropsById['dose-response:DoseCalculator'].mode,
|
|
129
|
+
nestedBindingMode: bindings.componentBindingsById['dose-response:DoseCalculator'].props.mode,
|
|
127
130
|
hasPlateWells: Boolean(componentPropsById['plate-map:WellPlate'].wells),
|
|
128
131
|
doseModeFromGetter: getComponentProps('DoseCalculator')?.mode,
|
|
129
132
|
bindingDoseModeFromGetter: bindings.getComponentProps('DoseCalculator')?.mode,
|
|
@@ -143,7 +146,10 @@ describe('BioTemplateExperimentWorkspaceView', () => {
|
|
|
143
146
|
|
|
144
147
|
expect(JSON.parse(wrapper.find('.component-props').text())).toEqual({
|
|
145
148
|
doseMode: 'serial',
|
|
149
|
+
bindingComponent: 'DoseCalculator',
|
|
150
|
+
bindingMode: 'serial',
|
|
146
151
|
bindingDoseMode: 'serial',
|
|
152
|
+
nestedBindingMode: 'serial',
|
|
147
153
|
hasPlateWells: true,
|
|
148
154
|
doseModeFromGetter: 'serial',
|
|
149
155
|
bindingDoseModeFromGetter: 'serial',
|