@morscherlab/mint-sdk 1.3.0-beta.4 → 1.3.0-beta.6
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 +23 -140
- package/dist/{ResourcePickerListInternal-c3IT8PIu.js → ResourcePickerListInternal-BEXnyBNz.js} +7 -3
- package/dist/ResourcePickerListInternal-BEXnyBNz.js.map +1 -0
- package/dist/components/WellPlate.colors.d.ts +1 -5
- package/dist/components/WellPlate.rendering.d.ts +5 -2
- package/dist/components/index.d.ts +32 -17
- package/dist/components/index.js +3 -3
- package/dist/{components-CF0XMy8W.js → components-C-CAD2Ma.js} +84 -134
- package/dist/components-C-CAD2Ma.js.map +1 -0
- package/dist/composables/index.d.ts +0 -3
- package/dist/composables/index.js +4 -4
- package/dist/composables/useAutoGroup.d.ts +1 -1
- package/dist/composables/useBioTemplateComponents.d.ts +15 -3
- package/dist/composables/useBioTemplateControls.d.ts +5 -1
- package/dist/composables/useBioTemplatePackWorkspace.d.ts +2 -0
- package/dist/composables/useBioTemplatePresetWorkspace.d.ts +5 -1
- package/dist/composables/useBioTemplateWorkspace.d.ts +5 -1
- package/dist/composables/useExperimentSave.d.ts +5 -1
- package/dist/composables/useGroupAssignment.d.ts +5 -1
- package/dist/composables/usePlatformContext.d.ts +3 -0
- package/dist/composables/useTemplateCollection.d.ts +5 -1
- package/dist/composables-BKhsoONv.js +591 -0
- package/dist/composables-BKhsoONv.js.map +1 -0
- package/dist/index.js +24 -24
- package/dist/index.js.map +1 -1
- package/dist/install.js +1 -1
- package/dist/lcms.d.ts +11 -11
- package/dist/styles.css +50 -143
- package/dist/templates/index.js +1 -1
- package/dist/{templates-Bj9hZ7iV.js → templates-DcxyATPT.js} +31 -44
- package/dist/templates-DcxyATPT.js.map +1 -0
- package/dist/types/auth.d.ts +0 -31
- package/dist/types/index.d.ts +1 -1
- package/dist/types/platform.d.ts +2 -0
- package/dist/utils/rack.d.ts +16 -16
- package/dist/{validation-B4Y8_eEe.js → validation-kbTxfKYD.js} +50 -12
- package/dist/validation-kbTxfKYD.js.map +1 -0
- package/package.json +1 -6
- package/src/__tests__/components/AppTopBar.test.ts +24 -8
- package/src/__tests__/components/EmptyState.test.ts +15 -0
- package/src/__tests__/components/FileUploader.test.ts +6 -9
- package/src/__tests__/components/WellPlate.decoration.test.ts +4 -28
- package/src/__tests__/components/WellPlate.rendering.test.ts +20 -33
- package/src/__tests__/components/WellPlate.sampleTypeIndicator.test.ts +7 -7
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +2 -2
- package/src/__tests__/styles/controlRefreshRules.test.ts +0 -6
- package/src/__tests__/styles/menusShellRefresh.test.ts +8 -2
- package/src/__tests__/templates/templatesDeprecation.test.ts +58 -11
- package/src/components/AppTopBar.vue +5 -4
- package/src/components/EmptyState.vue +3 -1
- package/src/components/FileUploader.vue +30 -46
- package/src/components/WellPlate.colors.ts +6 -10
- package/src/components/WellPlate.legend.ts +4 -4
- package/src/components/WellPlate.rendering.ts +9 -8
- package/src/components/WellPlate.vue +5 -22
- package/src/components/index.ts +32 -17
- package/src/composables/autoGroup/index.ts +1 -1
- package/src/composables/index.ts +0 -7
- package/src/composables/useAutoGroup.ts +1 -1
- package/src/composables/useBioTemplateComponents.ts +15 -3
- package/src/composables/useBioTemplateControls.ts +5 -1
- package/src/composables/useBioTemplatePackWorkspace.ts +2 -0
- package/src/composables/useBioTemplatePresetWorkspace.ts +5 -1
- package/src/composables/useBioTemplateWorkspace.ts +5 -1
- package/src/composables/useExperimentSave.ts +5 -1
- package/src/composables/useGroupAssignment.ts +5 -1
- package/src/composables/useTemplateCollection.ts +5 -1
- package/src/index.ts +1 -1
- package/src/lcms.ts +11 -11
- package/src/styles/components/app-top-bar.css +7 -31
- package/src/styles/components/empty-state.css +3 -2
- package/src/styles/components/file-uploader.css +22 -55
- package/src/styles/components/well-plate.css +18 -55
- package/src/types/auth.ts +0 -41
- package/src/types/index.ts +0 -5
- package/src/types/platform.ts +2 -0
- package/src/utils/rack.ts +16 -16
- package/dist/ResourcePickerListInternal-c3IT8PIu.js.map +0 -1
- package/dist/__tests__/composables/useAuth.test.d.ts +0 -1
- package/dist/__tests__/composables/usePasskey.test.d.ts +0 -1
- package/dist/components-CF0XMy8W.js.map +0 -1
- package/dist/composables/useAuth.d.ts +0 -47
- package/dist/composables/usePasskey.d.ts +0 -10
- package/dist/composables/useWellPlateEditor.d.ts +0 -33
- package/dist/composables-Dlyz_Aiv.js +0 -1256
- package/dist/composables-Dlyz_Aiv.js.map +0 -1
- package/dist/plotly.min-DDhZCNEM.js +0 -168582
- package/dist/plotly.min-DDhZCNEM.js.map +0 -1
- package/dist/templates-Bj9hZ7iV.js.map +0 -1
- package/dist/validation-B4Y8_eEe.js.map +0 -1
- package/src/__tests__/composables/useAuth.test.ts +0 -290
- package/src/__tests__/composables/usePasskey.test.ts +0 -38
- package/src/composables/useAuth.ts +0 -470
- package/src/composables/usePasskey.ts +0 -216
- package/src/composables/useWellPlateEditor.ts +0 -415
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morscherlab/mint-sdk",
|
|
3
|
-
"version": "1.3.0-beta.
|
|
3
|
+
"version": "1.3.0-beta.6",
|
|
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",
|
|
@@ -67,16 +67,12 @@
|
|
|
67
67
|
"story:preview": "histoire preview"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
|
-
"@simplewebauthn/browser": "^14.0.0",
|
|
71
70
|
"pinia": "^4.0.3",
|
|
72
71
|
"tailwindcss": "^4.3.3",
|
|
73
72
|
"vue": "^3.5.43",
|
|
74
73
|
"vue-router": "^5.3.1"
|
|
75
74
|
},
|
|
76
75
|
"peerDependenciesMeta": {
|
|
77
|
-
"@simplewebauthn/browser": {
|
|
78
|
-
"optional": true
|
|
79
|
-
},
|
|
80
76
|
"vue-router": {
|
|
81
77
|
"optional": true
|
|
82
78
|
}
|
|
@@ -89,7 +85,6 @@
|
|
|
89
85
|
},
|
|
90
86
|
"devDependencies": {
|
|
91
87
|
"@histoire/plugin-vue": "1.0.0-beta.1",
|
|
92
|
-
"@simplewebauthn/browser": "14.0.0",
|
|
93
88
|
"@types/node": "22.20.4",
|
|
94
89
|
"@vitejs/plugin-vue": "6.0.9",
|
|
95
90
|
"@vue/language-core": "3.3.11",
|
|
@@ -1211,12 +1211,20 @@ describe('AppTopBar', () => {
|
|
|
1211
1211
|
expect(wrapper.get('.mint-topbar__plugin-name').text()).toBe('LC-MS Analysis')
|
|
1212
1212
|
})
|
|
1213
1213
|
|
|
1214
|
-
it('should render the
|
|
1214
|
+
it('should render the platform display name when integrated', () => {
|
|
1215
|
+
mockPlatformCtx({ isIntegrated: true, plugin: { ...integratedPlugin, display_name: 'LC-MS Peak Review' } })
|
|
1216
|
+
|
|
1217
|
+
const wrapper = createWrapper()
|
|
1218
|
+
|
|
1219
|
+
expect(wrapper.get('.mint-topbar__plugin-name').text()).toBe('LC-MS Peak Review')
|
|
1220
|
+
})
|
|
1221
|
+
|
|
1222
|
+
it('should not render the plugin version when integrated', () => {
|
|
1215
1223
|
mockPlatformCtx({ isIntegrated: true, plugin: integratedPlugin })
|
|
1216
1224
|
|
|
1217
1225
|
const wrapper = createWrapper()
|
|
1218
1226
|
|
|
1219
|
-
expect(wrapper.
|
|
1227
|
+
expect(wrapper.text()).not.toContain('v2.4.0')
|
|
1220
1228
|
})
|
|
1221
1229
|
|
|
1222
1230
|
it('should not render the plugin identity text when standalone', () => {
|
|
@@ -1225,21 +1233,29 @@ describe('AppTopBar', () => {
|
|
|
1225
1233
|
expect(wrapper.find('.mint-topbar__plugin-name').exists()).toBe(false)
|
|
1226
1234
|
})
|
|
1227
1235
|
|
|
1228
|
-
it('should
|
|
1236
|
+
it('should hide the plugin name when a page selector is shown', () => {
|
|
1229
1237
|
mockPlatformCtx({ isIntegrated: true, plugin: integratedPlugin })
|
|
1230
1238
|
|
|
1231
1239
|
const wrapper = createWrapper({ pageSelector: pages, currentPageSelectorId: 'peaks' })
|
|
1232
1240
|
|
|
1233
|
-
expect(wrapper.
|
|
1234
|
-
.toContain('mint-page-selector')
|
|
1241
|
+
expect(wrapper.find('.mint-topbar__plugin-name').exists()).toBe(false)
|
|
1235
1242
|
})
|
|
1236
1243
|
|
|
1237
|
-
it('should
|
|
1238
|
-
mockPlatformCtx({
|
|
1244
|
+
it('should hide the plugin name when the plugin declares several nav items', () => {
|
|
1245
|
+
mockPlatformCtx({
|
|
1246
|
+
isIntegrated: true,
|
|
1247
|
+
plugin: {
|
|
1248
|
+
...integratedPlugin,
|
|
1249
|
+
nav_items: [
|
|
1250
|
+
{ id: 'peaks', path: '/', label: 'Peaks' },
|
|
1251
|
+
{ id: 'report', path: '/report', label: 'Report' },
|
|
1252
|
+
],
|
|
1253
|
+
},
|
|
1254
|
+
})
|
|
1239
1255
|
|
|
1240
1256
|
const wrapper = createWrapper()
|
|
1241
1257
|
|
|
1242
|
-
expect(wrapper.find('.mint-
|
|
1258
|
+
expect(wrapper.find('.mint-topbar__plugin-name').exists()).toBe(false)
|
|
1243
1259
|
})
|
|
1244
1260
|
|
|
1245
1261
|
it('should not repeat the title next to the integrated identity', () => {
|
|
@@ -20,4 +20,19 @@ describe('EmptyState', () => {
|
|
|
20
20
|
|
|
21
21
|
expect(wrapper.emitted('action')).toHaveLength(1)
|
|
22
22
|
})
|
|
23
|
+
|
|
24
|
+
it('should wrap default-slot content in the spaced content block', () => {
|
|
25
|
+
const wrapper = mount(EmptyState, {
|
|
26
|
+
props: { title: 'No instruments yet', description: 'Add the lab systems.' },
|
|
27
|
+
slots: { default: '<button class="slot-action">New instrument</button>' },
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
expect(wrapper.find('.mint-empty-state__content .slot-action').exists()).toBe(true)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should not render the content block without default-slot content', () => {
|
|
34
|
+
const wrapper = mount(EmptyState, { props: { title: 'Empty' } })
|
|
35
|
+
|
|
36
|
+
expect(wrapper.find('.mint-empty-state__content').exists()).toBe(false)
|
|
37
|
+
})
|
|
23
38
|
})
|
|
@@ -3,16 +3,13 @@ import { describe, expect, it } from 'vitest'
|
|
|
3
3
|
import FileUploader from '../../components/FileUploader.vue'
|
|
4
4
|
|
|
5
5
|
describe('FileUploader', () => {
|
|
6
|
-
it('should
|
|
7
|
-
const wrapper = mount(FileUploader, { props: {
|
|
8
|
-
|
|
9
|
-
dataTransfer: { items: { length: 3 } },
|
|
10
|
-
})
|
|
11
|
-
expect(wrapper.get('.mint-file-uploader__text').text()).toBe('Release to add 3 files')
|
|
6
|
+
it('should name the accepted formats in the prompt when accept is set', () => {
|
|
7
|
+
const wrapper = mount(FileUploader, { props: { accept: '.raw, .mzML' } })
|
|
8
|
+
expect(wrapper.get('.mint-file-uploader__text').text()).toBe('Drop RAW, MZML or click')
|
|
12
9
|
})
|
|
13
10
|
|
|
14
|
-
it('should
|
|
15
|
-
const wrapper = mount(FileUploader
|
|
16
|
-
expect(wrapper.get('.mint-file-
|
|
11
|
+
it('should prompt for any file when accept is not set', () => {
|
|
12
|
+
const wrapper = mount(FileUploader)
|
|
13
|
+
expect(wrapper.get('.mint-file-uploader__text').text()).toBe('Drop any file or click')
|
|
17
14
|
})
|
|
18
15
|
})
|
|
@@ -26,7 +26,7 @@ describe('WellPlate per-well decoration', () => {
|
|
|
26
26
|
.toEqual(['2px', 'solid', 'var(--mint-warning)'])
|
|
27
27
|
})
|
|
28
28
|
|
|
29
|
-
it('should render
|
|
29
|
+
it('should render no glyph when a QC well has no label or id', () => {
|
|
30
30
|
const wrapper = mount(WellPlate, {
|
|
31
31
|
props: {
|
|
32
32
|
format: 6,
|
|
@@ -34,37 +34,13 @@ describe('WellPlate per-well decoration', () => {
|
|
|
34
34
|
},
|
|
35
35
|
})
|
|
36
36
|
|
|
37
|
-
expect(wrapper.findAll('.mint-well-plate__well')[0].text()).toBe('
|
|
37
|
+
expect(wrapper.findAll('.mint-well-plate__well')[0].text()).toBe('')
|
|
38
38
|
})
|
|
39
39
|
|
|
40
|
-
it('should
|
|
41
|
-
const wrapper = mount(WellPlate, {
|
|
42
|
-
props: {
|
|
43
|
-
format: 6,
|
|
44
|
-
showWellIds: true,
|
|
45
|
-
wells: { A1: { id: 'A1', row: 0, col: 0, state: 'filled', sampleType: 'iqc' } },
|
|
46
|
-
},
|
|
47
|
-
})
|
|
48
|
-
|
|
49
|
-
expect(wrapper.findAll('.mint-well-plate__well')[0].find('.mint-well-plate__corner-glyph').text()).toBe('i')
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
it('should keep the iQC marker glyph when a custom label is shown', () => {
|
|
53
|
-
const wrapper = mount(WellPlate, {
|
|
54
|
-
props: {
|
|
55
|
-
format: 6,
|
|
56
|
-
showWellLabels: true,
|
|
57
|
-
wells: { A1: { id: 'A1', row: 0, col: 0, state: 'filled', sampleType: 'iqc', metadata: { label: 'Std 1' } } },
|
|
58
|
-
},
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
expect(wrapper.findAll('.mint-well-plate__well')[0].find('.mint-well-plate__corner-glyph').text()).toBe('i')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('should draw legend swatches with the sample-type marker when the legend is shown', () => {
|
|
40
|
+
it('should draw plain legend swatches when the legend is shown', () => {
|
|
65
41
|
const wrapper = mount(WellPlate, { props: { format: 6, showLegend: true } })
|
|
66
42
|
|
|
67
43
|
const dots = wrapper.findAll('.mint-well-plate__sample-legend-dot')
|
|
68
|
-
expect(dots.map(dot => dot.text())).toEqual(['', '', '
|
|
44
|
+
expect(dots.map(dot => dot.text())).toEqual(['', '', '', ''])
|
|
69
45
|
})
|
|
70
46
|
})
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import {
|
|
3
|
-
sampleTypeGlyph,
|
|
4
3
|
wellBadge,
|
|
5
4
|
wellClasses,
|
|
6
5
|
wellLabel,
|
|
@@ -70,45 +69,45 @@ describe('WellPlate rendering helpers', () => {
|
|
|
70
69
|
heatmap: { enabled: false },
|
|
71
70
|
sampleColors: { treated: '#123456' },
|
|
72
71
|
})).toEqual({
|
|
73
|
-
backgroundColor: 'color-mix(in srgb, #123456
|
|
74
|
-
border: '1px solid #123456',
|
|
72
|
+
backgroundColor: 'color-mix(in srgb, #123456 15%, transparent)',
|
|
73
|
+
border: '1px solid color-mix(in srgb, #123456 40%, transparent)',
|
|
75
74
|
})
|
|
76
75
|
|
|
77
76
|
expect(wellStyle(well({ sampleType: 'qc' }), {
|
|
78
77
|
heatmap: { enabled: false },
|
|
79
78
|
sampleColors: {},
|
|
80
79
|
})).toEqual({
|
|
81
|
-
backgroundColor: '
|
|
82
|
-
border: '
|
|
80
|
+
backgroundColor: 'color-mix(in srgb, #8b5cf6 15%, transparent)',
|
|
81
|
+
border: '1px solid color-mix(in srgb, #8b5cf6 40%, transparent)',
|
|
83
82
|
})
|
|
84
83
|
})
|
|
85
84
|
|
|
86
|
-
it('should
|
|
85
|
+
it('should tint a sample well green when no custom color is set', () => {
|
|
87
86
|
expect(wellStyle(well({ sampleType: 'sample' }), {
|
|
88
87
|
heatmap: { enabled: false },
|
|
89
88
|
sampleColors: {},
|
|
90
|
-
}).border).toBe('1px solid
|
|
89
|
+
}).border).toBe('1px solid color-mix(in srgb, #10b981 40%, transparent)')
|
|
91
90
|
})
|
|
92
91
|
|
|
93
|
-
it('should
|
|
94
|
-
expect(wellStyle(well({ sampleType: '
|
|
92
|
+
it('should tint a blank well orange when no custom color is set', () => {
|
|
93
|
+
expect(wellStyle(well({ sampleType: 'blank' }), {
|
|
95
94
|
heatmap: { enabled: false },
|
|
96
|
-
sampleColors: {
|
|
97
|
-
}).border).toBe('1px solid #
|
|
95
|
+
sampleColors: {},
|
|
96
|
+
}).border).toBe('1px solid color-mix(in srgb, #f97316 40%, transparent)')
|
|
98
97
|
})
|
|
99
98
|
|
|
100
|
-
it('should
|
|
101
|
-
expect(wellStyle(well({ sampleType: '
|
|
99
|
+
it('should color a well by its group before its sample type', () => {
|
|
100
|
+
expect(wellStyle(well({ sampleType: 'iqc', group: 'trt' }), {
|
|
102
101
|
heatmap: { enabled: false },
|
|
103
102
|
sampleColors: { trt: '#123456' },
|
|
104
|
-
}).border).toBe('
|
|
103
|
+
}).border).toBe('1px solid color-mix(in srgb, #123456 40%, transparent)')
|
|
105
104
|
})
|
|
106
105
|
|
|
107
|
-
it('should
|
|
108
|
-
expect(wellStyle(well({ sampleType: '
|
|
106
|
+
it('should tint a well with a group color token', () => {
|
|
107
|
+
expect(wellStyle(well({ sampleType: 'sample', group: 'trt' }), {
|
|
109
108
|
heatmap: { enabled: false },
|
|
110
|
-
sampleColors: {
|
|
111
|
-
}).
|
|
109
|
+
sampleColors: { trt: 'var(--mint-sample-3)' },
|
|
110
|
+
}).backgroundColor).toBe('color-mix(in srgb, var(--mint-sample-3) 15%, transparent)')
|
|
112
111
|
})
|
|
113
112
|
|
|
114
113
|
it('should leave empty wells to the stylesheet when the well has no sample type', () => {
|
|
@@ -125,29 +124,17 @@ describe('WellPlate rendering helpers', () => {
|
|
|
125
124
|
})).toEqual({})
|
|
126
125
|
})
|
|
127
126
|
|
|
128
|
-
it('should return the Q glyph when the well is QC', () => {
|
|
129
|
-
expect(sampleTypeGlyph(well({ sampleType: 'qc' }))).toBe('Q')
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('should return the i glyph when the well is iQC', () => {
|
|
133
|
-
expect(sampleTypeGlyph(well({ sampleType: 'iqc' }))).toBe('i')
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('should return no glyph when the well is a sample', () => {
|
|
137
|
-
expect(sampleTypeGlyph(well({ sampleType: 'sample' }))).toBeNull()
|
|
138
|
-
})
|
|
139
|
-
|
|
140
127
|
it('formats labels and badges for optional overlays', () => {
|
|
141
128
|
expect(wellLabel(well({ metadata: { label: 'Dose A' } }), true)).toBe('Dose A')
|
|
142
129
|
expect(wellLabel(well({ metadata: { label: 'Dose A' } }), false)).toBeUndefined()
|
|
143
130
|
|
|
144
131
|
expect(wellBadge(well({ metadata: { injectionCount: 3 } }), true)).toEqual({
|
|
145
132
|
text: '3',
|
|
146
|
-
color: '
|
|
133
|
+
color: '#6366f1',
|
|
147
134
|
})
|
|
148
135
|
expect(wellBadge(well({ metadata: { customMethod: 'LCMS-2' } }), true)).toEqual({
|
|
149
|
-
text: '',
|
|
150
|
-
color: '
|
|
136
|
+
text: '+',
|
|
137
|
+
color: '#ec4899',
|
|
151
138
|
})
|
|
152
139
|
expect(wellBadge(well({ metadata: { injectionCount: 1 } }), true)).toBeNull()
|
|
153
140
|
})
|
|
@@ -4,9 +4,9 @@ import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
4
4
|
import WellPlate from '../../components/WellPlate.vue'
|
|
5
5
|
import type { Well } from '../../types'
|
|
6
6
|
|
|
7
|
-
// `showSampleTypeIndicator` is deprecated (3a):
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// `showSampleTypeIndicator` is deprecated (3a): wells carry no letters, color
|
|
8
|
+
// encodes the type. The warning fires once per module, so the order of the
|
|
9
|
+
// first two tests matters.
|
|
10
10
|
|
|
11
11
|
function mountPlate(sampleType: string, showSampleTypeIndicator: boolean) {
|
|
12
12
|
const wells: Record<string, Partial<Well>> = {
|
|
@@ -48,11 +48,11 @@ describe('WellPlate showSampleTypeIndicator (deprecated)', () => {
|
|
|
48
48
|
expect(firstWellText('blank')).toBe('')
|
|
49
49
|
})
|
|
50
50
|
|
|
51
|
-
it('should
|
|
52
|
-
expect(firstWellText('qc')).toBe('
|
|
51
|
+
it('should render no letter for a QC well when showSampleTypeIndicator is on', () => {
|
|
52
|
+
expect(firstWellText('qc')).toBe('')
|
|
53
53
|
})
|
|
54
54
|
|
|
55
|
-
it('should
|
|
56
|
-
expect(firstWellText('iqc')).toBe('
|
|
55
|
+
it('should render no letter for an iQC well when showSampleTypeIndicator is on', () => {
|
|
56
|
+
expect(firstWellText('iqc')).toBe('')
|
|
57
57
|
})
|
|
58
58
|
})
|
|
@@ -60,7 +60,7 @@ function parseComponentExports(): string[] {
|
|
|
60
60
|
const componentsDir = join(SDK_ROOT, 'src', 'components')
|
|
61
61
|
const content = readFileSync(join(componentsDir, 'index.ts'), 'utf8')
|
|
62
62
|
return [...content.matchAll(
|
|
63
|
-
/export\s+\{\s*default\s+as\s+(\w+)(?:\s*,\s*[^}]*)?\s*\}\s+from\s+['"]\.\/([\w-]+)\.vue['"]/g,
|
|
63
|
+
/export\s+\{\s*(?:\/\*\*.*?\*\/\s*)?default\s+as\s+(\w+)(?:\s*,\s*[^}]*)?\s*\}\s+from\s+['"]\.\/([\w-]+)\.vue['"]/g,
|
|
64
64
|
)]
|
|
65
65
|
.filter((match) => existsSync(join(componentsDir, `${match[2]}.vue`)))
|
|
66
66
|
.map(match => match[1])
|
|
@@ -121,7 +121,7 @@ function parseDeprecatedComponentExports(): Record<string, string> {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
const exportMatch = line.match(
|
|
124
|
-
/export\s+\{\s*default\s+as\s+(\w+)(?:\s*,\s*[^}]*)?\s*\}\s+from\s+['"]\.\/([\w-]+)\.vue['"]/,
|
|
124
|
+
/export\s+\{\s*(?:\/\*\*.*?\*\/\s*)?default\s+as\s+(\w+)(?:\s*,\s*[^}]*)?\s*\}\s+from\s+['"]\.\/([\w-]+)\.vue['"]/,
|
|
125
125
|
)
|
|
126
126
|
if (!exportMatch) continue
|
|
127
127
|
|
|
@@ -184,12 +184,6 @@ describe('SDK control refresh style rules', () => {
|
|
|
184
184
|
expect(rule).toMatch(/flex-wrap:\s*wrap/)
|
|
185
185
|
})
|
|
186
186
|
|
|
187
|
-
it('should keep the badge text off white when scanning well-plate.css (7a)', () => {
|
|
188
|
-
const css = stripComments(readFileSync(component('well-plate.css'), 'utf-8'))
|
|
189
|
-
const rule = /\.mint-well-plate__badge\s*\{([^}]*)\}/.exec(css)?.[1] ?? ''
|
|
190
|
-
expect(rule).toMatch(/color:\s*var\(--bg-card\)/)
|
|
191
|
-
})
|
|
192
|
-
|
|
193
187
|
it('should pad resource picker rows so two text lines fit the 44px row when scanning resource-picker-list.css (8b)', () => {
|
|
194
188
|
// 16.25px title + 1px gap + 15px meta + 10px padding + 1px border = 43.25px, under min-height.
|
|
195
189
|
const css = stripComments(readFileSync(component('resource-picker-list.css'), 'utf-8'))
|
|
@@ -25,8 +25,14 @@ describe('menus and app shell styles', () => {
|
|
|
25
25
|
expect(css('app-top-bar.css')).toMatch(/\.mint-topbar\s*\{[^}]*padding:\s*0\.625rem 0\.75rem;/)
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// Shrinking the tile beside the identity also shrank structured icons inside their solid chip, drawing a ring.
|
|
29
|
+
it('should keep the 32px plugin tile beside the static plugin identity (5a)', () => {
|
|
30
|
+
expect(css('app-top-bar.css')).not.toMatch(/:has\(\+ \.mint-topbar__plugin-identity\)/)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
// A slotted 36px logo shrank to 32px wide but kept its height, drawing a 32x36 tile.
|
|
34
|
+
it('should fit slotted identity content inside the 32px tile (5a)', () => {
|
|
35
|
+
expect(css('app-top-bar.css')).toMatch(/\.mint-topbar__identity > \*\s*\{[^}]*max-width:\s*100%;[^}]*max-height:\s*100%;/)
|
|
30
36
|
})
|
|
31
37
|
|
|
32
38
|
// 15px x 1.15 + 12px x 1.2 = 31.7px, so the title group fits the 32px control row.
|
|
@@ -1,31 +1,78 @@
|
|
|
1
1
|
import { dirname, resolve } from 'node:path'
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { beforeAll, describe, expect, it } from 'vitest'
|
|
4
4
|
import ts from 'typescript'
|
|
5
5
|
|
|
6
6
|
const TEMPLATES_INDEX = resolve(dirname(fileURLToPath(import.meta.url)), '../../templates/index.ts')
|
|
7
|
+
const COMPONENTS_INDEX = resolve(dirname(fileURLToPath(import.meta.url)), '../../components/index.ts')
|
|
8
|
+
const COMPOSABLES_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../../composables')
|
|
7
9
|
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// Composables removed in MINT 1.4 with the templates and sample grouping.
|
|
11
|
+
const REMOVED_COMPOSABLE_FILES = [
|
|
12
|
+
'useBioTemplateComponents.ts',
|
|
13
|
+
'useBioTemplateControls.ts',
|
|
14
|
+
'useBioTemplatePackWorkspace.ts',
|
|
15
|
+
'useBioTemplatePresetWorkspace.ts',
|
|
16
|
+
'useBioTemplateWorkspace.ts',
|
|
17
|
+
'useTemplateCollection.ts',
|
|
18
|
+
'useExperimentSave.ts',
|
|
19
|
+
'useGroupAssignment.ts',
|
|
20
|
+
].map(file => resolve(COMPOSABLES_DIR, file))
|
|
21
|
+
|
|
22
|
+
// One program for every file: building one per file took longer than the test timeout on CI.
|
|
23
|
+
let program: ts.Program
|
|
24
|
+
let checker: ts.TypeChecker
|
|
25
|
+
|
|
26
|
+
beforeAll(() => {
|
|
27
|
+
program = ts.createProgram([TEMPLATES_INDEX, COMPONENTS_INDEX, ...REMOVED_COMPOSABLE_FILES], {
|
|
12
28
|
target: ts.ScriptTarget.ES2022,
|
|
13
29
|
module: ts.ModuleKind.ESNext,
|
|
14
30
|
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
15
31
|
noEmit: true,
|
|
16
32
|
})
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
checker = program.getTypeChecker()
|
|
34
|
+
}, 60_000)
|
|
35
|
+
|
|
36
|
+
function exportsOf(file: string): ts.Symbol[] {
|
|
37
|
+
return checker.getExportsOfModule(checker.getSymbolAtLocation(program.getSourceFile(file)!)!)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isDeprecated(symbol: ts.Symbol): boolean {
|
|
41
|
+
return symbol.getJsDocTags(checker).some(tag => tag.name === 'deprecated')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// TypeScript ignores @deprecated on `export { ... } from` blocks; an import
|
|
45
|
+
// site only shows the deprecation when the original declaration carries it.
|
|
46
|
+
function undeprecatedValueExports(file: string): string[] {
|
|
47
|
+
return exportsOf(file)
|
|
21
48
|
.map(symbol => (symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol))
|
|
22
49
|
.filter(symbol => symbol.flags & ts.SymbolFlags.Value)
|
|
23
|
-
.filter(symbol => !symbol
|
|
50
|
+
.filter(symbol => !isDeprecated(symbol))
|
|
24
51
|
.map(symbol => symbol.getName())
|
|
25
52
|
}
|
|
26
53
|
|
|
27
54
|
describe('templates deprecation', () => {
|
|
55
|
+
it('should expose deprecated component tags to TypeScript consumers', () => {
|
|
56
|
+
const names = ['BioTemplateRenderer', 'SmartGroupModal', 'DoseDesignWorkspaceView']
|
|
57
|
+
const tagged = exportsOf(COMPONENTS_INDEX).filter(isDeprecated).map(symbol => symbol.getName())
|
|
58
|
+
|
|
59
|
+
expect(names.filter(name => !tagged.includes(name))).toEqual([])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should not tag components that stay public in MINT 1.4', () => {
|
|
63
|
+
const names = ['SampleSelector', 'SequenceProgressBar', 'InstrumentStateBadge']
|
|
64
|
+
const tagged = exportsOf(COMPONENTS_INDEX).filter(isDeprecated).map(symbol => symbol.getName())
|
|
65
|
+
|
|
66
|
+
expect(names.filter(name => tagged.includes(name))).toEqual([])
|
|
67
|
+
})
|
|
68
|
+
|
|
28
69
|
it('should tag every template value export when the templates move out in MINT 1.4', () => {
|
|
29
|
-
expect(
|
|
70
|
+
expect(undeprecatedValueExports(TEMPLATES_INDEX)).toEqual([])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should tag every value export of the template and grouping composables when they are removed in MINT 1.4', () => {
|
|
74
|
+
const untagged = REMOVED_COMPOSABLE_FILES.flatMap(undeprecatedValueExports)
|
|
75
|
+
|
|
76
|
+
expect(untagged).toEqual([])
|
|
30
77
|
})
|
|
31
78
|
})
|
|
@@ -144,8 +144,11 @@ const hasPlatformPageSelector = computed(() =>
|
|
|
144
144
|
props.pageSelector === undefined && (plugin.value?.nav_items?.length ?? 0) > 1,
|
|
145
145
|
)
|
|
146
146
|
const hasPageSelector = computed(() => (props.pageSelector?.length ?? 0) > 1 || hasPlatformPageSelector.value)
|
|
147
|
-
/** Integrated plugins show their name
|
|
148
|
-
|
|
147
|
+
/** Integrated plugins show their name as a static identity in place of the title; the page selector replaces it. */
|
|
148
|
+
// Platforms before 1.3 send no display_name.
|
|
149
|
+
const identityName = computed(() =>
|
|
150
|
+
isIntegrated.value && !hasPageSelector.value ? plugin.value?.display_name || plugin.value?.name : undefined,
|
|
151
|
+
)
|
|
149
152
|
const hasPillNav = computed(() => !!props.pillNav?.length)
|
|
150
153
|
const hasAccountMenu = computed(() => !!props.accountMenu?.length)
|
|
151
154
|
const hasTitleGroup = computed(
|
|
@@ -304,9 +307,7 @@ const effectiveCurrentPageSelectorId = computed(() =>
|
|
|
304
307
|
<!-- Left: static plugin identity (not a control) -->
|
|
305
308
|
<span v-if="identityName" class="mint-topbar__plugin-identity">
|
|
306
309
|
<span class="mint-topbar__plugin-name">{{ identityName }}</span>
|
|
307
|
-
<span v-if="plugin?.version" class="mint-topbar__plugin-version">v{{ plugin.version }}</span>
|
|
308
310
|
</span>
|
|
309
|
-
<span v-if="identityName && hasPageSelector" class="mint-topbar__divider" aria-hidden="true" />
|
|
310
311
|
|
|
311
312
|
<!-- Left: page selector -->
|
|
312
313
|
<AppTopBarPageSelectorInternal
|
|
@@ -56,7 +56,9 @@ const defaultIconPaths = [
|
|
|
56
56
|
<div class="mint-empty-state__body">
|
|
57
57
|
<h3 v-if="title" class="mint-empty-state__title">{{ title }}</h3>
|
|
58
58
|
<p v-if="description" class="mint-empty-state__description">{{ description }}</p>
|
|
59
|
-
<
|
|
59
|
+
<div v-if="$slots.default" class="mint-empty-state__content">
|
|
60
|
+
<slot />
|
|
61
|
+
</div>
|
|
60
62
|
</div>
|
|
61
63
|
<div v-if="actionLabel" class="mint-empty-state__action">
|
|
62
64
|
<BaseButton variant="secondary" @click="emit('action')">
|
|
@@ -31,7 +31,6 @@ const emit = defineEmits<{
|
|
|
31
31
|
}>()
|
|
32
32
|
|
|
33
33
|
const isDragOver = ref(false)
|
|
34
|
-
const dragCount = ref(0)
|
|
35
34
|
const inputRef = ref<HTMLInputElement>()
|
|
36
35
|
const selectedFiles = ref<File[]>([])
|
|
37
36
|
const folderName = ref<string | null>(null)
|
|
@@ -45,14 +44,9 @@ watch(() => [props.files, props.rootName] as const, ([files, rootName]) => {
|
|
|
45
44
|
const isFolder = computed(() => props.mode === 'folder')
|
|
46
45
|
const isMixed = computed(() => props.mode === 'file+folder')
|
|
47
46
|
|
|
48
|
-
const
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
return `Release to add ${dragCount.value} file${dragCount.value === 1 ? '' : 's'}`
|
|
52
|
-
}
|
|
53
|
-
if (isMixed.value) return 'Drop files or a folder, or click to choose files'
|
|
54
|
-
if (isFolder.value) return 'Drop a folder, or click to choose'
|
|
55
|
-
return props.multiple ? 'Drop files, or click to choose' : 'Drop a file, or click to choose'
|
|
47
|
+
const acceptLabel = computed(() => {
|
|
48
|
+
if (!props.accept) return isFolder.value ? 'a folder' : 'any file'
|
|
49
|
+
return props.accept.split(',').map(a => a.trim().replace('.', '').toUpperCase()).join(', ')
|
|
56
50
|
})
|
|
57
51
|
|
|
58
52
|
const maxSizeLabel = computed(() => {
|
|
@@ -69,12 +63,6 @@ const maxSizeLabel = computed(() => {
|
|
|
69
63
|
return `${props.maxSize} bytes`
|
|
70
64
|
})
|
|
71
65
|
|
|
72
|
-
/** Accepted formats exactly as the caller wrote them, plus the size cap. */
|
|
73
|
-
const formatsLabel = computed(() => [
|
|
74
|
-
props.accept?.split(',').map(a => a.trim()).filter(Boolean).join(' '),
|
|
75
|
-
maxSizeLabel.value && `max ${maxSizeLabel.value}`,
|
|
76
|
-
].filter(Boolean).join(' · '))
|
|
77
|
-
|
|
78
66
|
const fileTypeMap: Record<string, { color: string; label: string }> = {
|
|
79
67
|
csv: { color: '#10B981', label: 'CSV' },
|
|
80
68
|
tsv: { color: '#10B981', label: 'TSV' },
|
|
@@ -272,7 +260,6 @@ function handleDragOver(event: DragEvent) {
|
|
|
272
260
|
event.preventDefault()
|
|
273
261
|
if (!props.disabled) {
|
|
274
262
|
isDragOver.value = true
|
|
275
|
-
dragCount.value = event.dataTransfer?.items?.length ?? 0
|
|
276
263
|
}
|
|
277
264
|
}
|
|
278
265
|
|
|
@@ -359,37 +346,34 @@ function formatFileSize(bytes: number): string {
|
|
|
359
346
|
/>
|
|
360
347
|
|
|
361
348
|
<div class="mint-file-uploader__content">
|
|
362
|
-
<
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
</svg>
|
|
387
|
-
</span>
|
|
349
|
+
<svg
|
|
350
|
+
v-if="isFolder"
|
|
351
|
+
class="mint-file-uploader__icon"
|
|
352
|
+
fill="none"
|
|
353
|
+
stroke="currentColor"
|
|
354
|
+
stroke-width="1.5"
|
|
355
|
+
stroke-linecap="round"
|
|
356
|
+
stroke-linejoin="round"
|
|
357
|
+
viewBox="0 0 24 24"
|
|
358
|
+
>
|
|
359
|
+
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
|
|
360
|
+
</svg>
|
|
361
|
+
<svg
|
|
362
|
+
v-else
|
|
363
|
+
class="mint-file-uploader__icon"
|
|
364
|
+
fill="none"
|
|
365
|
+
stroke="currentColor"
|
|
366
|
+
stroke-width="1.5"
|
|
367
|
+
stroke-linecap="round"
|
|
368
|
+
stroke-linejoin="round"
|
|
369
|
+
viewBox="0 0 24 24"
|
|
370
|
+
>
|
|
371
|
+
<path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3" /><path d="m16 16-4-4-4 4" /><path d="M12 12v9" />
|
|
372
|
+
</svg>
|
|
388
373
|
|
|
389
|
-
<
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
</div>
|
|
374
|
+
<p class="mint-file-uploader__text">
|
|
375
|
+
{{ isMixed ? 'Drop files or a folder, or click to choose files' : `Drop ${acceptLabel} or click` }}
|
|
376
|
+
</p>
|
|
393
377
|
</div>
|
|
394
378
|
</div>
|
|
395
379
|
|
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import type { HeatmapConfig } from '../types'
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Group color for the built-in sample types when `sampleColors` has no entry.
|
|
5
|
-
* Color encodes the group and shape encodes the type (3a): blank and QC render
|
|
6
|
-
* neutral markers and ignore it; iQC takes it like a sample.
|
|
7
|
-
*/
|
|
3
|
+
/** Color for the built-in sample types when `sampleColors` has no entry. */
|
|
8
4
|
export const DEFAULT_SAMPLE_TYPE_COLORS: Record<string, string> = {
|
|
9
|
-
sample: '
|
|
10
|
-
control: '
|
|
11
|
-
blank: '
|
|
12
|
-
qc: '
|
|
13
|
-
iqc: '
|
|
5
|
+
sample: '#10b981',
|
|
6
|
+
control: '#3b82f6',
|
|
7
|
+
blank: '#f97316',
|
|
8
|
+
qc: '#8b5cf6',
|
|
9
|
+
iqc: '#ec4899',
|
|
14
10
|
}
|
|
15
11
|
|
|
16
12
|
export const HEATMAP_COLORS: Record<string, string[]> = {
|