@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.
- 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 +56 -4
- 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/AppPageSelectorInternal.vue.d.ts} +1 -1
- package/dist/components/{AppPillNav.vue.d.ts → internal/AppPillNavInternal.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-BkGF4B4y.js} +4484 -3967
- package/dist/components-BkGF4B4y.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-CHsME9H1.js} +40 -28
- package/dist/composables-CHsME9H1.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 +3625 -3651
- package/dist/templates/componentBindings.d.ts +13 -0
- package/dist/templates/index.d.ts +3 -3
- package/dist/templates/index.js +2 -2
- package/dist/{templates-50NPjaxL.js → templates-B5jmTWuk.js} +111 -56
- package/dist/templates-B5jmTWuk.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 → useScheduleDrag-BgzpQT53.js} +160 -117
- package/dist/useScheduleDrag-BgzpQT53.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/AppPageSelector.test.ts +8 -8
- package/src/__tests__/components/AppPillNav.test.ts +53 -6
- package/src/__tests__/components/AppSidebar.test.ts +126 -0
- package/src/__tests__/components/AppToastContainer.test.ts +0 -11
- package/src/__tests__/components/AppTopBar.test.ts +182 -119
- package/src/__tests__/components/BioTemplateExperimentWorkspaceView.test.ts +7 -1
- package/src/__tests__/components/BioTemplatePackWorkspaceView.test.ts +15 -1
- package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +26 -1
- package/src/__tests__/components/CalendarGridPanel.test.ts +3 -3
- package/src/__tests__/components/ComponentBindingRenderer.test.ts +161 -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 +6 -3
- package/src/__tests__/composables/useBioTemplatePresetWorkspace.test.ts +6 -6
- package/src/__tests__/composables/useBioTemplateWorkspace.test.ts +6 -1
- package/src/__tests__/composables/useControlSchema.test.ts +150 -36
- 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 +12 -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 +144 -24
- 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 +11 -2
- package/src/components/BioTemplateRenderer.vue +15 -227
- package/src/components/ComponentBindingRenderer.story.vue +57 -0
- package/src/components/ComponentBindingRenderer.vue +308 -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.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/AppPageSelectorInternal.vue} +9 -9
- package/src/components/internal/AppPillNavInternal.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-pill-nav.css +70 -0
- package/src/styles/components/app-sidebar.css +119 -0
- package/src/styles/components/app-top-bar.css +0 -235
- package/src/styles/index.css +0 -1
- package/src/templates/componentBindings.ts +38 -0
- package/src/templates/index.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/{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
|
@@ -5,6 +5,7 @@ import axios, { type AxiosRequestConfig } from 'axios'
|
|
|
5
5
|
import {
|
|
6
6
|
buildPluginEndpointUrl,
|
|
7
7
|
createPluginClient,
|
|
8
|
+
getPluginPageSelectorItems,
|
|
8
9
|
resolvePluginBaseUrl,
|
|
9
10
|
useCurrentExperiment,
|
|
10
11
|
usePluginClient,
|
|
@@ -121,12 +122,44 @@ describe('usePluginClient', () => {
|
|
|
121
122
|
it('resolves plugin base URLs without making requests', () => {
|
|
122
123
|
expect(resolvePluginBaseUrl(contract)).toBe('/api/drp')
|
|
123
124
|
|
|
124
|
-
expect(resolvePluginBaseUrl(
|
|
125
|
+
expect(resolvePluginBaseUrl(
|
|
126
|
+
{ ...contract, plugin: { ...contract.plugin, apiPrefix: '/api/drp/' } },
|
|
127
|
+
)).toBe('/api/drp')
|
|
128
|
+
expect(resolvePluginBaseUrl(contract, '/api/explicit/')).toBe('/api/explicit')
|
|
125
129
|
|
|
126
|
-
vi.stubEnv('VITE_API_PREFIX', '/api/env')
|
|
130
|
+
vi.stubEnv('VITE_API_PREFIX', '/api/env/')
|
|
127
131
|
expect(resolvePluginBaseUrl(contract, '/api/explicit')).toBe('/api/env')
|
|
128
132
|
})
|
|
129
133
|
|
|
134
|
+
it('normalizes platform-injected plugin base URLs', () => {
|
|
135
|
+
;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
|
|
136
|
+
isIntegrated: true,
|
|
137
|
+
theme: 'light',
|
|
138
|
+
plugin: {
|
|
139
|
+
id: 'drp',
|
|
140
|
+
name: 'DRP',
|
|
141
|
+
version: '1.0.0',
|
|
142
|
+
route_prefix: '/ignored/',
|
|
143
|
+
api_prefix: '/api/platform-drp/',
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
expect(resolvePluginBaseUrl(contract)).toBe('/api/platform-drp')
|
|
148
|
+
|
|
149
|
+
;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
|
|
150
|
+
isIntegrated: true,
|
|
151
|
+
theme: 'light',
|
|
152
|
+
plugin: {
|
|
153
|
+
id: 'drp',
|
|
154
|
+
name: 'DRP',
|
|
155
|
+
version: '1.0.0',
|
|
156
|
+
route_prefix: '/platform-drp/',
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
expect(resolvePluginBaseUrl(contract)).toBe('/api/platform-drp')
|
|
161
|
+
})
|
|
162
|
+
|
|
130
163
|
it('builds concrete endpoint URLs without making requests', () => {
|
|
131
164
|
const url = buildPluginEndpointUrl(
|
|
132
165
|
contract,
|
|
@@ -146,6 +179,70 @@ describe('usePluginClient', () => {
|
|
|
146
179
|
expect(requestConfigs).toHaveLength(0)
|
|
147
180
|
})
|
|
148
181
|
|
|
182
|
+
it('maps plugin contract nav items to AppTopBar page selector metadata', () => {
|
|
183
|
+
const pageSelectorItems = getPluginPageSelectorItems({
|
|
184
|
+
...contract,
|
|
185
|
+
plugin: {
|
|
186
|
+
...contract.plugin,
|
|
187
|
+
name: 'Dose Designer',
|
|
188
|
+
description: 'Build dose designs',
|
|
189
|
+
icon: 'PLUGIN_ICON',
|
|
190
|
+
navItems: [
|
|
191
|
+
{ path: '/', label: 'Dashboard', icon: 'DASHBOARD_ICON', description: 'Overview' },
|
|
192
|
+
{ id: 'dose-design', path: 'dose-design', label: 'Dose Design' },
|
|
193
|
+
{ path: '/qc/review/', label: 'QC Review', icon: 'QC_ICON' },
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
expect(pageSelectorItems).toEqual([
|
|
199
|
+
{
|
|
200
|
+
id: 'dashboard',
|
|
201
|
+
label: 'Dashboard',
|
|
202
|
+
to: '/',
|
|
203
|
+
icon: 'DASHBOARD_ICON',
|
|
204
|
+
hint: 'Overview',
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
id: 'dose-design',
|
|
208
|
+
label: 'Dose Design',
|
|
209
|
+
to: '/dose-design',
|
|
210
|
+
icon: 'PLUGIN_ICON',
|
|
211
|
+
hint: 'Dose Designer',
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
id: 'qc-review',
|
|
215
|
+
label: 'QC Review',
|
|
216
|
+
to: '/qc/review',
|
|
217
|
+
icon: 'QC_ICON',
|
|
218
|
+
hint: 'Dose Designer',
|
|
219
|
+
},
|
|
220
|
+
])
|
|
221
|
+
expect(requestConfigs).toHaveLength(0)
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
it('provides a dashboard page selector fallback when contract nav items are absent', () => {
|
|
225
|
+
expect(getPluginPageSelectorItems({
|
|
226
|
+
...contract,
|
|
227
|
+
plugin: {
|
|
228
|
+
...contract.plugin,
|
|
229
|
+
name: 'Dose Designer',
|
|
230
|
+
description: 'Build dose designs',
|
|
231
|
+
icon: 'PLUGIN_ICON',
|
|
232
|
+
navItems: [],
|
|
233
|
+
},
|
|
234
|
+
})).toEqual([
|
|
235
|
+
{
|
|
236
|
+
id: 'dashboard',
|
|
237
|
+
label: 'Dose Designer',
|
|
238
|
+
to: '/',
|
|
239
|
+
icon: 'PLUGIN_ICON',
|
|
240
|
+
hint: 'Build dose designs',
|
|
241
|
+
},
|
|
242
|
+
])
|
|
243
|
+
expect(requestConfigs).toHaveLength(0)
|
|
244
|
+
})
|
|
245
|
+
|
|
149
246
|
it('can build path-only endpoint URLs with inferred experiment ids', () => {
|
|
150
247
|
window.history.replaceState({}, '', '/experiments/42/plugins/drp')
|
|
151
248
|
|
|
@@ -18,6 +18,7 @@ interface FrontendManifest {
|
|
|
18
18
|
templateExports: Array<{ name: string }>
|
|
19
19
|
templatePacks: Array<{ name: string }>
|
|
20
20
|
}
|
|
21
|
+
exports: Array<{ name: string }>
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
const TEST_DIR = dirname(fileURLToPath(import.meta.url))
|
|
@@ -42,6 +43,27 @@ function parseComponentExports(): string[] {
|
|
|
42
43
|
.sort()
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
function parseRootComponentExports(): string[] {
|
|
47
|
+
const content = readFileSync(join(SDK_ROOT, 'src', 'index.ts'), 'utf8')
|
|
48
|
+
if (/export\s+\*\s+from\s+['"]\.\/components['"]/.test(content)) {
|
|
49
|
+
return parseComponentExports()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const names = new Set<string>()
|
|
53
|
+
|
|
54
|
+
for (const match of content.matchAll(/export\s+\{([^}]*)\}\s+from\s+['"]\.\/components['"]/g)) {
|
|
55
|
+
const body = match[1].replace(/\/\/.*$/gm, '')
|
|
56
|
+
for (const item of body.split(',')) {
|
|
57
|
+
const spec = item.trim()
|
|
58
|
+
if (!spec || spec.startsWith('type ')) continue
|
|
59
|
+
const name = spec.split(/\s+as\s+/, 1)[0].trim()
|
|
60
|
+
if (/^[A-Z]\w*$/.test(name)) names.add(name)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return [...names].sort()
|
|
65
|
+
}
|
|
66
|
+
|
|
45
67
|
function parseDeprecatedComponentExports(): Record<string, string> {
|
|
46
68
|
const componentsDir = join(SDK_ROOT, 'src', 'components')
|
|
47
69
|
const content = readFileSync(join(componentsDir, 'index.ts'), 'utf8')
|
|
@@ -115,6 +137,27 @@ function parseDocumentedComposableExports(): string[] {
|
|
|
115
137
|
return [...new Set(names)].sort()
|
|
116
138
|
}
|
|
117
139
|
|
|
140
|
+
function parseRootComposableExports(): string[] {
|
|
141
|
+
const rootContent = readFileSync(join(SDK_ROOT, 'src', 'index.ts'), 'utf8')
|
|
142
|
+
const composableExports = parseDocumentedComposableExports()
|
|
143
|
+
|
|
144
|
+
if (/export\s+\*\s+from\s+['"]\.\/composables['"]/.test(rootContent)) {
|
|
145
|
+
return composableExports
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const names: string[] = []
|
|
149
|
+
for (const match of rootContent.matchAll(/export\s+\{([^}]*)\}\s+from\s+['"]\.\/composables['"]/g)) {
|
|
150
|
+
for (const item of match[1].split(',')) {
|
|
151
|
+
const spec = item.trim()
|
|
152
|
+
if (!spec || spec.startsWith('type ')) continue
|
|
153
|
+
const name = spec.split(/\s+as\s+/, 1)[0].trim()
|
|
154
|
+
if (name) names.push(name)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return [...new Set(names)].sort()
|
|
159
|
+
}
|
|
160
|
+
|
|
118
161
|
function parseTemplateExports(): string[] {
|
|
119
162
|
const indexContent = readFileSync(join(SDK_ROOT, 'src', 'templates', 'index.ts'), 'utf8')
|
|
120
163
|
const names = new Set<string>()
|
|
@@ -148,6 +191,13 @@ describe('frontend docs catalog coverage', () => {
|
|
|
148
191
|
expect(missingFromManifest(documentedComponents, publicComponents)).toEqual([])
|
|
149
192
|
})
|
|
150
193
|
|
|
194
|
+
it('re-exports every public component from the root SDK entrypoint', () => {
|
|
195
|
+
const publicComponents = parseComponentExports()
|
|
196
|
+
const rootComponents = parseRootComponentExports()
|
|
197
|
+
|
|
198
|
+
expect(missingFromManifest(publicComponents, rootComponents)).toEqual([])
|
|
199
|
+
})
|
|
200
|
+
|
|
151
201
|
it('marks every deprecated public component export with its replacement', () => {
|
|
152
202
|
const manifest = extractFrontendManifest()
|
|
153
203
|
const expected = parseDeprecatedComponentExports()
|
|
@@ -161,34 +211,28 @@ describe('frontend docs catalog coverage', () => {
|
|
|
161
211
|
expect(documented).toEqual(expected)
|
|
162
212
|
})
|
|
163
213
|
|
|
164
|
-
it('keeps compatibility
|
|
214
|
+
it('keeps retired compatibility APIs out of public SDK entrypoints', () => {
|
|
165
215
|
const rootIndex = readFileSync(join(SDK_ROOT, 'src', 'index.ts'), 'utf8')
|
|
216
|
+
const componentsIndex = readFileSync(join(SDK_ROOT, 'src', 'components', 'index.ts'), 'utf8')
|
|
166
217
|
const composablesIndex = readFileSync(join(SDK_ROOT, 'src', 'composables', 'index.ts'), 'utf8')
|
|
167
218
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
it('keeps deprecated usePluginApi docs focused on migration', () => {
|
|
186
|
-
const content = readFileSync(join(SDK_ROOT, 'src', 'composables', 'usePluginApi.ts'), 'utf8')
|
|
187
|
-
|
|
188
|
-
expect(content).toContain('Migration shape for generated plugins')
|
|
189
|
-
expect(content).toContain('useGeneratedPluginClient')
|
|
190
|
-
expect(content).not.toContain('const api = usePluginApi')
|
|
191
|
-
expect(content).not.toContain("fallbackPrefix: '/api/drp'")
|
|
219
|
+
const retiredNames = [
|
|
220
|
+
'ToastNotification',
|
|
221
|
+
'SettingsButton',
|
|
222
|
+
'GroupingModal',
|
|
223
|
+
'FormSection',
|
|
224
|
+
'FormFieldRenderer',
|
|
225
|
+
'AppPageSelector',
|
|
226
|
+
'AppPillNav',
|
|
227
|
+
'usePluginApi',
|
|
228
|
+
'UsePluginApiOptions',
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
for (const name of retiredNames) {
|
|
232
|
+
expect(rootIndex).not.toMatch(new RegExp(`\\b${name}\\b`))
|
|
233
|
+
expect(componentsIndex).not.toMatch(new RegExp(`\\b${name}\\b`))
|
|
234
|
+
expect(composablesIndex).not.toMatch(new RegExp(`\\b${name}\\b`))
|
|
235
|
+
}
|
|
192
236
|
})
|
|
193
237
|
|
|
194
238
|
it('documents every public composable/helper export used by docs and doctor', () => {
|
|
@@ -200,6 +244,57 @@ describe('frontend docs catalog coverage', () => {
|
|
|
200
244
|
expect(missingFromManifest(documentedComposables, publicComposables)).toEqual([])
|
|
201
245
|
})
|
|
202
246
|
|
|
247
|
+
it('documents generated component binding helpers for control models', () => {
|
|
248
|
+
const manifest = extractFrontendManifest()
|
|
249
|
+
const documentedComposables = new Set(manifest.categories.composables.map(item => item.name))
|
|
250
|
+
|
|
251
|
+
expect(documentedComposables.has('defineControlComponentBindings')).toBe(true)
|
|
252
|
+
expect(documentedComposables.has('controlValuesToComponentBindings')).toBe(true)
|
|
253
|
+
expect(documentedComposables.has('controlValuesToComponentBindingsById')).toBe(true)
|
|
254
|
+
expect(documentedComposables.has('defineWellPlateDoseComponentBindings')).toBe(true)
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('re-exports every documented composable/helper from the root SDK entrypoint', () => {
|
|
258
|
+
const publicComposables = parseDocumentedComposableExports()
|
|
259
|
+
const rootComposables = parseRootComposableExports()
|
|
260
|
+
|
|
261
|
+
expect(missingFromManifest(publicComposables, rootComposables)).toEqual([])
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('re-exports the public types barrel from the root SDK entrypoint', () => {
|
|
265
|
+
const rootIndex = readFileSync(join(SDK_ROOT, 'src', 'index.ts'), 'utf8')
|
|
266
|
+
|
|
267
|
+
expect(rootIndex).toMatch(/export\s+type\s+\*\s+from\s+['"]\.\/types['"]/)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('re-exports the public stores barrel from the root SDK entrypoint', () => {
|
|
271
|
+
const rootIndex = readFileSync(join(SDK_ROOT, 'src', 'index.ts'), 'utf8')
|
|
272
|
+
|
|
273
|
+
expect(rootIndex).toMatch(/export\s+\*\s+from\s+['"]\.\/stores['"]/)
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
it('re-exports the public templates barrel from the root SDK entrypoint', () => {
|
|
277
|
+
const rootIndex = readFileSync(join(SDK_ROOT, 'src', 'index.ts'), 'utf8')
|
|
278
|
+
|
|
279
|
+
expect(rootIndex).toMatch(/export\s+\*\s+from\s+['"]\.\/templates['"]/)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it('expands root barrel exports into the docs manifest', () => {
|
|
283
|
+
const manifest = extractFrontendManifest()
|
|
284
|
+
const rootExports = manifest.exports.map(item => item.name)
|
|
285
|
+
|
|
286
|
+
expect(rootExports).toEqual(expect.arrayContaining([
|
|
287
|
+
'BaseButton',
|
|
288
|
+
'DoseDesignWorkspaceView',
|
|
289
|
+
'WellPlate',
|
|
290
|
+
'useForm',
|
|
291
|
+
'useReagentSeries',
|
|
292
|
+
'ConversionResult',
|
|
293
|
+
'FormSectionSchema',
|
|
294
|
+
'createWellPlateScreenCollection',
|
|
295
|
+
]))
|
|
296
|
+
})
|
|
297
|
+
|
|
203
298
|
it('documents curated biology template packs', () => {
|
|
204
299
|
const manifest = extractFrontendManifest()
|
|
205
300
|
|
|
@@ -43,6 +43,8 @@ import {
|
|
|
43
43
|
searchBioTemplatePacks,
|
|
44
44
|
searchBioTemplateCatalog,
|
|
45
45
|
searchBioTemplatePresets,
|
|
46
|
+
toBioTemplateComponentBindings,
|
|
47
|
+
toBioTemplateComponentBindingsById,
|
|
46
48
|
toBioTemplateComponentImports,
|
|
47
49
|
toBioTemplateComponentProps,
|
|
48
50
|
toBioTemplateComponentPropsByComponent,
|
|
@@ -330,14 +332,21 @@ describe('bio data templates', () => {
|
|
|
330
332
|
const state = toPlateMapEditorState(plateMapFixture as PlateMapTemplate)
|
|
331
333
|
const wells = toWellPlateWells(plateMapFixture as PlateMapTemplate)
|
|
332
334
|
const componentProps = toBioTemplateComponentProps(plateMapFixture as PlateMapTemplate)
|
|
335
|
+
const componentBindings = toBioTemplateComponentBindings(plateMapFixture as PlateMapTemplate)
|
|
333
336
|
const componentPropsById = toBioTemplateComponentPropsById(plateMapFixture as PlateMapTemplate)
|
|
337
|
+
const componentBindingsById = toBioTemplateComponentBindingsById(plateMapFixture as PlateMapTemplate)
|
|
334
338
|
const wellPlate = componentProps.find(binding => binding.component === 'WellPlate')
|
|
339
|
+
const wellPlateBinding = componentBindings.find(binding => binding.component === 'WellPlate')
|
|
335
340
|
|
|
336
341
|
expect(state.plates[0].name).toBe('Drug screen plate')
|
|
337
342
|
expect(wells.A2.sampleType).toBe('drug-a')
|
|
338
343
|
expect(wellPlate?.propsObject.format).toBe(96)
|
|
339
344
|
expect((wellPlate?.propsObject.wells as Record<string, { sampleType?: string }>).A2.sampleType).toBe('drug-a')
|
|
345
|
+
expect(wellPlateBinding?.props.format).toBe(96)
|
|
346
|
+
expect(wellPlateBinding?.propNames).toContain('wells')
|
|
340
347
|
expect(componentPropsById['plate-map:WellPlate']).toEqual(wellPlate?.propsObject)
|
|
348
|
+
expect(componentBindingsById['plate-map:WellPlate'].component).toBe('WellPlate')
|
|
349
|
+
expect(componentBindingsById['plate-map:WellPlate'].props).toEqual(wellPlate?.propsObject)
|
|
341
350
|
expect((componentPropsById['plate-map:WellPlate'].wells as Record<string, { sampleType?: string }>).A2.sampleType).toBe('drug-a')
|
|
342
351
|
})
|
|
343
352
|
|
|
@@ -347,6 +356,7 @@ describe('bio data templates', () => {
|
|
|
347
356
|
compounds: { 'Drug A': [10, 1] },
|
|
348
357
|
})
|
|
349
358
|
const propsById = toBioTemplateComponentPropsById(collection)
|
|
359
|
+
const bindingsById = toBioTemplateComponentBindingsById(collection)
|
|
350
360
|
const propsByComponent = toBioTemplateComponentPropsByComponent(collection)
|
|
351
361
|
const doseCalculatorProps = getBioTemplateComponentProps(collection, 'DoseCalculator')
|
|
352
362
|
const doseWellPlateProps = getBioTemplateComponentProps(collection, 'WellPlate', {
|
|
@@ -362,6 +372,8 @@ describe('bio data templates', () => {
|
|
|
362
372
|
'dose-response:WellPlate',
|
|
363
373
|
])
|
|
364
374
|
expect(propsById['dose-response:DoseCalculator'].mode).toBe('serial')
|
|
375
|
+
expect(bindingsById['dose-response:DoseCalculator'].props.mode).toBe('serial')
|
|
376
|
+
expect(bindingsById['dose-response:DoseCalculator'].propNames).toContain('mode')
|
|
365
377
|
expect(propsById['plate-map:WellPlate'].wells).toBeDefined()
|
|
366
378
|
expect(propsById['dose-response:WellPlate'].wells).toBeDefined()
|
|
367
379
|
expect(propsByComponent.WellPlate).toHaveLength(2)
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { computed } from 'vue'
|
|
4
4
|
import { useDropdownState } from '../composables/useDropdownState'
|
|
5
5
|
import type { AccountMenuItem } from '../types/components'
|
|
6
|
-
import
|
|
6
|
+
import ActionItemInternal from './internal/ActionItemInternal.vue'
|
|
7
7
|
|
|
8
8
|
interface Props {
|
|
9
9
|
userName?: string
|
|
@@ -86,7 +86,7 @@ function handleSignOut() {
|
|
|
86
86
|
<slot name="items" :close="closeSilently">
|
|
87
87
|
<template v-for="item in items" :key="item.id">
|
|
88
88
|
<div v-if="item.divider" class="mint-avatar-menu__divider" role="separator" />
|
|
89
|
-
<
|
|
89
|
+
<ActionItemInternal
|
|
90
90
|
v-else
|
|
91
91
|
:href="item.href"
|
|
92
92
|
:to="item.to"
|
|
@@ -99,7 +99,7 @@ function handleSignOut() {
|
|
|
99
99
|
</span>
|
|
100
100
|
<span class="mint-avatar-menu__item-label">{{ item.label }}</span>
|
|
101
101
|
<span v-if="item.rightLabel" class="mint-avatar-menu__item-right">{{ item.rightLabel }}</span>
|
|
102
|
-
</
|
|
102
|
+
</ActionItemInternal>
|
|
103
103
|
</template>
|
|
104
104
|
</slot>
|
|
105
105
|
|
|
@@ -48,6 +48,7 @@ const logScale = ref(false)
|
|
|
48
48
|
:sidebar-position="state.sidebarPosition"
|
|
49
49
|
:floating="state.floating ?? true"
|
|
50
50
|
:sidebar-width="state.sidebarWidth"
|
|
51
|
+
responsive-sidebar
|
|
51
52
|
>
|
|
52
53
|
<template #topbar>
|
|
53
54
|
<AppTopBar
|
|
@@ -216,6 +217,44 @@ const logScale = ref(false)
|
|
|
216
217
|
</div>
|
|
217
218
|
</Variant>
|
|
218
219
|
|
|
220
|
+
<Variant title="Responsive Sidebar">
|
|
221
|
+
<div style="height: 500px; max-width: 760px; border: 1px solid var(--border-color, #e2e8f0);">
|
|
222
|
+
<AppLayout
|
|
223
|
+
floating
|
|
224
|
+
responsive-sidebar
|
|
225
|
+
sidebar-width="320px"
|
|
226
|
+
>
|
|
227
|
+
<template #topbar>
|
|
228
|
+
<AppTopBar title="Responsive Analysis" home-path="" />
|
|
229
|
+
</template>
|
|
230
|
+
<template #sidebar>
|
|
231
|
+
<AppSidebar
|
|
232
|
+
variant="analysis"
|
|
233
|
+
title="Feature Filters"
|
|
234
|
+
subtitle="12 active samples"
|
|
235
|
+
:badge="2"
|
|
236
|
+
:panels="toolPanels"
|
|
237
|
+
active-view="analysis"
|
|
238
|
+
>
|
|
239
|
+
<template #section-parameters>
|
|
240
|
+
<BaseSlider v-model="threshold" label="Threshold" :min="0" :max="100" />
|
|
241
|
+
<BaseSelect v-model="method" label="Method" :options="methods" />
|
|
242
|
+
</template>
|
|
243
|
+
<template #section-filters>
|
|
244
|
+
<BaseToggle v-model="showOutliers" label="Exclude outliers" />
|
|
245
|
+
</template>
|
|
246
|
+
</AppSidebar>
|
|
247
|
+
</template>
|
|
248
|
+
<div style="padding: 2rem;">
|
|
249
|
+
<p style="color: var(--text-muted, #94a3b8);">
|
|
250
|
+
At narrow widths, AppLayout owns the sidebar toggle and backdrop,
|
|
251
|
+
while AppSidebar owns the panel chrome and collapse state.
|
|
252
|
+
</p>
|
|
253
|
+
</div>
|
|
254
|
+
</AppLayout>
|
|
255
|
+
</div>
|
|
256
|
+
</Variant>
|
|
257
|
+
|
|
219
258
|
<Variant title="With Experiment Selector">
|
|
220
259
|
<ExperimentProvider>
|
|
221
260
|
<div style="height: 600px;">
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* AppLayout - Page layout shell with topbar, sidebar, and main content slots
|
|
4
4
|
*
|
|
5
5
|
* Provides a responsive application layout structure with optional topbar and sidebar.
|
|
6
|
-
* The sidebar slot is a simple pass-through
|
|
6
|
+
* The sidebar slot is a simple pass-through on desktop. When responsiveSidebar is
|
|
7
|
+
* enabled, AppLayout owns the mobile toggle/backdrop shell around the sidebar.
|
|
7
8
|
*
|
|
8
9
|
* @example
|
|
9
10
|
* ```vue
|
|
@@ -20,7 +21,7 @@
|
|
|
20
21
|
* </AppLayout>
|
|
21
22
|
* ```
|
|
22
23
|
*/
|
|
23
|
-
import { computed } from 'vue'
|
|
24
|
+
import { computed, ref } from 'vue'
|
|
24
25
|
|
|
25
26
|
interface Props {
|
|
26
27
|
/** Position of sidebar (left or right side of screen) */
|
|
@@ -29,23 +30,61 @@ interface Props {
|
|
|
29
30
|
sidebarWidth?: string
|
|
30
31
|
/** When true, topbar/sidebar/main render as floating cards with gaps */
|
|
31
32
|
floating?: boolean
|
|
33
|
+
/** Convert the sidebar into a mobile overlay with built-in toggle and backdrop below 1024px. */
|
|
34
|
+
responsiveSidebar?: boolean
|
|
35
|
+
/** Controlled mobile sidebar open state. Desktop sidebar remains visible. */
|
|
36
|
+
sidebarOpen?: boolean
|
|
37
|
+
/** Initial mobile sidebar open state when sidebarOpen is uncontrolled. */
|
|
38
|
+
defaultSidebarOpen?: boolean
|
|
39
|
+
/** Accessible label for the mobile sidebar toggle. */
|
|
40
|
+
sidebarToggleLabel?: string
|
|
41
|
+
/** Accessible label used when the mobile sidebar is open. */
|
|
42
|
+
sidebarCloseLabel?: string
|
|
32
43
|
}
|
|
33
44
|
|
|
34
45
|
const props = withDefaults(defineProps<Props>(), {
|
|
35
46
|
sidebarPosition: 'left',
|
|
36
47
|
sidebarWidth: 'auto',
|
|
37
48
|
floating: false,
|
|
49
|
+
responsiveSidebar: false,
|
|
50
|
+
sidebarOpen: undefined,
|
|
51
|
+
defaultSidebarOpen: false,
|
|
52
|
+
sidebarToggleLabel: 'Open sidebar',
|
|
53
|
+
sidebarCloseLabel: 'Close sidebar',
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const emit = defineEmits<{
|
|
57
|
+
'update:sidebarOpen': [value: boolean]
|
|
58
|
+
}>()
|
|
59
|
+
|
|
60
|
+
const internalSidebarOpen = ref(props.defaultSidebarOpen)
|
|
61
|
+
const sidebarOpenModel = computed({
|
|
62
|
+
get: () => props.sidebarOpen ?? internalSidebarOpen.value,
|
|
63
|
+
set: (value: boolean) => {
|
|
64
|
+
internalSidebarOpen.value = value
|
|
65
|
+
emit('update:sidebarOpen', value)
|
|
66
|
+
},
|
|
38
67
|
})
|
|
39
68
|
|
|
40
69
|
const layoutClasses = computed(() => [
|
|
41
70
|
'mint-layout',
|
|
42
71
|
props.sidebarPosition === 'right' ? 'mint-layout--sidebar-right' : '',
|
|
43
72
|
props.floating ? 'mint-layout--floating' : '',
|
|
73
|
+
props.responsiveSidebar ? 'mint-layout--responsive-sidebar' : '',
|
|
74
|
+
sidebarOpenModel.value ? 'mint-layout--sidebar-open' : '',
|
|
44
75
|
])
|
|
45
76
|
|
|
46
77
|
const sidebarStyle = computed(() => {
|
|
47
78
|
return props.sidebarWidth !== 'auto' ? { width: props.sidebarWidth } : undefined
|
|
48
79
|
})
|
|
80
|
+
|
|
81
|
+
function toggleSidebar() {
|
|
82
|
+
sidebarOpenModel.value = !sidebarOpenModel.value
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function closeSidebar() {
|
|
86
|
+
sidebarOpenModel.value = false
|
|
87
|
+
}
|
|
49
88
|
</script>
|
|
50
89
|
|
|
51
90
|
<template>
|
|
@@ -55,6 +94,48 @@ const sidebarStyle = computed(() => {
|
|
|
55
94
|
</div>
|
|
56
95
|
|
|
57
96
|
<div class="mint-layout__body">
|
|
97
|
+
<button
|
|
98
|
+
v-if="responsiveSidebar && $slots.sidebar"
|
|
99
|
+
type="button"
|
|
100
|
+
class="mint-layout__sidebar-toggle"
|
|
101
|
+
:aria-label="sidebarOpenModel ? sidebarCloseLabel : sidebarToggleLabel"
|
|
102
|
+
:aria-expanded="sidebarOpenModel"
|
|
103
|
+
@click="toggleSidebar"
|
|
104
|
+
>
|
|
105
|
+
<svg
|
|
106
|
+
v-if="!sidebarOpenModel"
|
|
107
|
+
class="mint-layout__sidebar-toggle-icon"
|
|
108
|
+
viewBox="0 0 24 24"
|
|
109
|
+
fill="none"
|
|
110
|
+
stroke="currentColor"
|
|
111
|
+
stroke-width="2"
|
|
112
|
+
stroke-linecap="round"
|
|
113
|
+
stroke-linejoin="round"
|
|
114
|
+
aria-hidden="true"
|
|
115
|
+
>
|
|
116
|
+
<path d="M4 6h16M4 12h16M4 18h16" />
|
|
117
|
+
</svg>
|
|
118
|
+
<svg
|
|
119
|
+
v-else
|
|
120
|
+
class="mint-layout__sidebar-toggle-icon"
|
|
121
|
+
viewBox="0 0 24 24"
|
|
122
|
+
fill="none"
|
|
123
|
+
stroke="currentColor"
|
|
124
|
+
stroke-width="2"
|
|
125
|
+
stroke-linecap="round"
|
|
126
|
+
stroke-linejoin="round"
|
|
127
|
+
aria-hidden="true"
|
|
128
|
+
>
|
|
129
|
+
<path d="M6 18 18 6M6 6l12 12" />
|
|
130
|
+
</svg>
|
|
131
|
+
</button>
|
|
132
|
+
|
|
133
|
+
<div
|
|
134
|
+
v-if="responsiveSidebar && $slots.sidebar && sidebarOpenModel"
|
|
135
|
+
class="mint-layout__sidebar-backdrop"
|
|
136
|
+
@click="closeSidebar"
|
|
137
|
+
/>
|
|
138
|
+
|
|
58
139
|
<div
|
|
59
140
|
v-if="$slots.sidebar"
|
|
60
141
|
class="mint-layout__sidebar"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
/** Dropdown menu for switching between installed plugins with color swatches, version badges, and an install link. */
|
|
3
3
|
import { useDropdownState } from '../composables/useDropdownState'
|
|
4
4
|
import type { PluginSwitcherPlugin } from '../types/components'
|
|
5
|
-
import
|
|
5
|
+
import ActionItemInternal from './internal/ActionItemInternal.vue'
|
|
6
6
|
|
|
7
7
|
interface Props {
|
|
8
8
|
current: PluginSwitcherPlugin
|
|
@@ -74,7 +74,7 @@ function handleInstall() {
|
|
|
74
74
|
|
|
75
75
|
<div v-if="isOpen" class="mint-plugin-switcher__menu" role="menu">
|
|
76
76
|
<div class="mint-plugin-switcher__menu-title">Switch plugin</div>
|
|
77
|
-
<
|
|
77
|
+
<ActionItemInternal
|
|
78
78
|
v-for="plugin in plugins"
|
|
79
79
|
:key="plugin.id"
|
|
80
80
|
:href="plugin.href"
|
|
@@ -107,13 +107,13 @@ function handleInstall() {
|
|
|
107
107
|
>
|
|
108
108
|
<path d="M5 13l4 4L19 7" />
|
|
109
109
|
</svg>
|
|
110
|
-
</
|
|
110
|
+
</ActionItemInternal>
|
|
111
111
|
|
|
112
112
|
<template v-if="plugins.length && (installHref || installTo || $slots.install)">
|
|
113
113
|
<div class="mint-plugin-switcher__divider" role="separator" />
|
|
114
114
|
</template>
|
|
115
115
|
<slot name="install">
|
|
116
|
-
<
|
|
116
|
+
<ActionItemInternal
|
|
117
117
|
v-if="installHref || installTo"
|
|
118
118
|
:href="installHref"
|
|
119
119
|
:to="installTo"
|
|
@@ -126,7 +126,7 @@ function handleInstall() {
|
|
|
126
126
|
<line x1="5" y1="12" x2="19" y2="12" />
|
|
127
127
|
</svg>
|
|
128
128
|
<span>{{ installLabel }}</span>
|
|
129
|
-
</
|
|
129
|
+
</ActionItemInternal>
|
|
130
130
|
</slot>
|
|
131
131
|
</div>
|
|
132
132
|
</div>
|