@morscherlab/mint-sdk 1.1.9 → 1.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/AutoGroupModal.adapter.d.ts +12 -1
- package/dist/components/index.js +1 -1
- package/dist/{components-BbtUpCTv.js → components-HXV-ZkBg.js} +39 -18
- package/dist/{components-BbtUpCTv.js.map → components-HXV-ZkBg.js.map} +1 -1
- package/dist/composables/index.js +1 -1
- package/dist/index.js +2 -2
- package/dist/install.js +1 -1
- package/dist/{useProtocolTemplates-BWfNaynA.js → useProtocolTemplates-D3IZKLqr.js} +7 -8
- package/dist/useProtocolTemplates-D3IZKLqr.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/AutoGroupModal.adapter.test.ts +31 -1
- package/src/__tests__/components/SampleSelector.test.ts +35 -0
- package/src/__tests__/composables/autoGroup/groupByOrder.test.ts +31 -2
- package/src/components/AutoGroupModal.adapter.ts +52 -1
- package/src/components/AutoGroupModal.vue +6 -4
- package/src/composables/useAutoGroup.ts +11 -7
- package/dist/useProtocolTemplates-BWfNaynA.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morscherlab/mint-sdk",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.10",
|
|
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",
|
|
@@ -3,13 +3,14 @@ import {
|
|
|
3
3
|
candidateColumns,
|
|
4
4
|
engineFieldsToCards,
|
|
5
5
|
engineGroupsToRail,
|
|
6
|
+
engineSchemasToCards,
|
|
6
7
|
engineToQcRoute,
|
|
7
8
|
manualSamplesToResult,
|
|
8
9
|
qcRouteToEngine,
|
|
9
10
|
roleLabel,
|
|
10
11
|
seedFromGroups,
|
|
11
12
|
} from '../../components/AutoGroupModal.adapter'
|
|
12
|
-
import type { ColumnInfo } from '../../types/auto-group'
|
|
13
|
+
import type { ClassSchema, ColumnInfo } from '../../types/auto-group'
|
|
13
14
|
import type { SampleGroup } from '../../types'
|
|
14
15
|
import type { ManualSample } from '../../components/SmartGroupManual.cohorts'
|
|
15
16
|
|
|
@@ -85,6 +86,35 @@ describe('engineFieldsToCards', () => {
|
|
|
85
86
|
})
|
|
86
87
|
})
|
|
87
88
|
|
|
89
|
+
describe('engineSchemasToCards', () => {
|
|
90
|
+
it('should expose enabled fields from another schema when the active schema has none enabled', () => {
|
|
91
|
+
const schemas: Record<string, ClassSchema> = {
|
|
92
|
+
active: {
|
|
93
|
+
classKind: 'unknown',
|
|
94
|
+
columns: [col({ index: 0, name: 'Token 1', role: 'factor', uniqueValues: ['5916', '5918'] })],
|
|
95
|
+
groupBy: [],
|
|
96
|
+
},
|
|
97
|
+
other: {
|
|
98
|
+
classKind: 'unknown',
|
|
99
|
+
columns: [
|
|
100
|
+
col({ index: 0, name: 'Token 1', role: 'constant', uniqueValues: ['sample'] }),
|
|
101
|
+
col({ index: 1, name: 'Token 2', role: 'factor', uniqueValues: ['1x', '2x'] }),
|
|
102
|
+
],
|
|
103
|
+
groupBy: [1],
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
expect(engineSchemasToCards(schemas, 'active')).toEqual({
|
|
108
|
+
fields: [
|
|
109
|
+
{ key: 'Token 1', role: 'Factor', vals: '5916, 5918', rep: false },
|
|
110
|
+
{ key: 'Token 2', role: 'Factor', vals: '1x, 2x', rep: false },
|
|
111
|
+
],
|
|
112
|
+
enabled: [false, true],
|
|
113
|
+
layers: [null, 1],
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
88
118
|
describe('engineGroupsToRail', () => {
|
|
89
119
|
it('should map groups to {name, count, color} using sample length', () => {
|
|
90
120
|
const groups: SampleGroup[] = [{ name: 'Control', color: '#111', samples: ['a', 'b'] }]
|
|
@@ -246,6 +246,41 @@ describe('AutoGroupModal manual workflow', () => {
|
|
|
246
246
|
})
|
|
247
247
|
})
|
|
248
248
|
|
|
249
|
+
describe('AutoGroupModal multi-schema auto workflow', () => {
|
|
250
|
+
it('should keep apply enabled when another sample schema still has a grouping field', async () => {
|
|
251
|
+
const samples = [
|
|
252
|
+
...['5916', '5918', '5919', '5920', '5921'].map((id, index) =>
|
|
253
|
+
`${id}_batch_project_matrix_subject_${String(index + 1).padStart(3, '0')}`,
|
|
254
|
+
),
|
|
255
|
+
...Array.from({ length: 5 }, (_, index) =>
|
|
256
|
+
`sample_1x_${String(index + 1).padStart(3, '0')}`,
|
|
257
|
+
),
|
|
258
|
+
...Array.from({ length: 5 }, (_, index) =>
|
|
259
|
+
`sample_2x_${String(index + 6).padStart(3, '0')}`,
|
|
260
|
+
),
|
|
261
|
+
'IQC_Pool_001',
|
|
262
|
+
'IQC_Pool_002',
|
|
263
|
+
'IQC_Pool_003',
|
|
264
|
+
]
|
|
265
|
+
const wrapper = mount(AutoGroupModal, {
|
|
266
|
+
props: {
|
|
267
|
+
modelValue: true,
|
|
268
|
+
samples,
|
|
269
|
+
},
|
|
270
|
+
global: {
|
|
271
|
+
plugins: [createPinia()],
|
|
272
|
+
stubs: {
|
|
273
|
+
Teleport: true,
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
await wrapper.find('.sg-switch').trigger('click')
|
|
279
|
+
|
|
280
|
+
expect(wrapper.find('.sg-foot .mint-button--primary').attributes('disabled')).toBeUndefined()
|
|
281
|
+
})
|
|
282
|
+
})
|
|
283
|
+
|
|
249
284
|
describe('SampleSelector — QC overlay regression', () => {
|
|
250
285
|
it('keeps QC samples in the emitted groups via the concat compat rule', async () => {
|
|
251
286
|
const exp: SampleGroup[] = [
|
|
@@ -115,6 +115,16 @@ describe('useAutoGroup — field edits span every class exposing the field', ()
|
|
|
115
115
|
expect(Object.values(auto.schemas.value).map(s => s.groupBy)).toEqual([[2, 1], [2, 1]])
|
|
116
116
|
})
|
|
117
117
|
|
|
118
|
+
it('should move a field from a non-active schema when the active schema has it disabled', () => {
|
|
119
|
+
const auto = twoClassEngine()
|
|
120
|
+
const activeKey = auto.activeClassKey.value
|
|
121
|
+
auto.toggleGroupBy(activeKey, 2)
|
|
122
|
+
|
|
123
|
+
auto.moveGroupByField('Token 3', 'up')
|
|
124
|
+
|
|
125
|
+
expect(Object.values(auto.schemas.value).map(schema => schema.groupBy)).toEqual([[1], [2, 1]])
|
|
126
|
+
})
|
|
127
|
+
|
|
118
128
|
it('should converge previously divergent class orders to the active schema order', () => {
|
|
119
129
|
const auto = threeFactorClassEngine()
|
|
120
130
|
const inactiveKey = Object.keys(auto.schemas.value).find(k => k !== auto.activeClassKey.value)!
|
|
@@ -178,12 +188,12 @@ describe('useAutoGroup — field edits span every class exposing the field', ()
|
|
|
178
188
|
expect(Object.values(auto.schemas.value).map(s => s.groupBy)).toEqual([[2], [2]])
|
|
179
189
|
})
|
|
180
190
|
|
|
181
|
-
it('should
|
|
191
|
+
it('should disable a batch field when any class still has it enabled', () => {
|
|
182
192
|
const auto = twoClassEngine()
|
|
183
193
|
// Diverge the classes: disable Timepoint in the non-active class only.
|
|
184
194
|
const inactiveKey = Object.keys(auto.schemas.value).find(k => k !== auto.activeClassKey.value)!
|
|
185
195
|
auto.toggleGroupBy(inactiveKey, 1)
|
|
186
|
-
// The card reads ON
|
|
196
|
+
// The batch card reads ON while either class uses Timepoint.
|
|
187
197
|
auto.toggleGroupByField('Timepoint')
|
|
188
198
|
expect(Object.values(auto.schemas.value).every(s => !s.groupBy.includes(1))).toBe(true)
|
|
189
199
|
})
|
|
@@ -195,6 +205,25 @@ describe('useAutoGroup — field edits span every class exposing the field', ()
|
|
|
195
205
|
expect(Object.values(auto.schemas.value).map(s => s.groupBy)).toEqual([[2, 1], [2, 1]])
|
|
196
206
|
})
|
|
197
207
|
|
|
208
|
+
it('should not enable a same-named constant column when re-enabling a batch field', () => {
|
|
209
|
+
const auto = twoClassEngine()
|
|
210
|
+
const activeKey = auto.activeClassKey.value
|
|
211
|
+
const active = auto.schemas.value[activeKey]
|
|
212
|
+
auto.schemas.value = {
|
|
213
|
+
...auto.schemas.value,
|
|
214
|
+
[activeKey]: {
|
|
215
|
+
...active,
|
|
216
|
+
columns: active.columns.map(column => column.index === 2 ? { ...column, role: 'constant' } : column),
|
|
217
|
+
groupBy: active.groupBy.filter(index => index !== 2),
|
|
218
|
+
},
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
auto.toggleGroupByField('Token 3')
|
|
222
|
+
auto.toggleGroupByField('Token 3')
|
|
223
|
+
|
|
224
|
+
expect(Object.values(auto.schemas.value).map(schema => schema.groupBy)).toEqual([[1], [1, 2]])
|
|
225
|
+
})
|
|
226
|
+
|
|
198
227
|
it('should ignore a field name that no class exposes', () => {
|
|
199
228
|
const auto = twoClassEngine()
|
|
200
229
|
auto.moveGroupByField('Nonexistent', 'up')
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* `AutoGroupResult`. Framework-free so the mapping is unit-testable in isolation
|
|
5
5
|
* from `AutoGroupModal.vue`.
|
|
6
6
|
*/
|
|
7
|
-
import type { AutoGroupResult, ColumnInfo, ColumnRole } from '../types/auto-group'
|
|
7
|
+
import type { AutoGroupResult, ClassSchema, ColumnInfo, ColumnRole } from '../types/auto-group'
|
|
8
8
|
import type { SampleGroup } from '../types'
|
|
9
9
|
import type { FieldRecipeGroup, QcRoute, SmartGroupField } from './SmartGroupFieldRecipe.groups'
|
|
10
10
|
import type { ManualSample } from './SmartGroupManual.cohorts'
|
|
@@ -61,6 +61,57 @@ export function engineFieldsToCards(
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Build the batch-wide field recipe from every class schema. The active schema
|
|
66
|
+
* stays first for a stable card order; fields and enabled hierarchy layers that
|
|
67
|
+
* only exist in another token-length/class split are appended instead of being
|
|
68
|
+
* hidden from the controlled UI.
|
|
69
|
+
*/
|
|
70
|
+
export function engineSchemasToCards(
|
|
71
|
+
schemas: Record<string, ClassSchema>,
|
|
72
|
+
activeClassKey: string,
|
|
73
|
+
): { fields: SmartGroupField[]; enabled: boolean[]; layers: (number | null)[] } {
|
|
74
|
+
const entries = Object.entries(schemas)
|
|
75
|
+
const active = entries.find(([key]) => key === activeClassKey)
|
|
76
|
+
const ordered = active
|
|
77
|
+
? [active, ...entries.filter(([key]) => key !== activeClassKey)]
|
|
78
|
+
: entries
|
|
79
|
+
const candidates = new Map<string, ColumnInfo>()
|
|
80
|
+
const enabledOrder: string[] = []
|
|
81
|
+
const enabled = new Set<string>()
|
|
82
|
+
|
|
83
|
+
for (const [, schema] of ordered) {
|
|
84
|
+
for (const index of schema.groupBy) {
|
|
85
|
+
const column = schema.columns.find(candidate => candidate.index === index)
|
|
86
|
+
if (!column) continue
|
|
87
|
+
const identity = column.displayName ?? column.name
|
|
88
|
+
if (!enabled.has(identity)) {
|
|
89
|
+
enabled.add(identity)
|
|
90
|
+
enabledOrder.push(identity)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
for (const column of candidateColumns(schema.columns, schema.groupBy)) {
|
|
94
|
+
const identity = column.displayName ?? column.name
|
|
95
|
+
if (!candidates.has(identity)) candidates.set(identity, column)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const cards = [...candidates.entries()]
|
|
100
|
+
return {
|
|
101
|
+
fields: cards.map(([key, column]) => ({
|
|
102
|
+
key,
|
|
103
|
+
role: roleLabel(column.role),
|
|
104
|
+
vals: column.uniqueValues.join(', '),
|
|
105
|
+
rep: column.role === 'replicate',
|
|
106
|
+
})),
|
|
107
|
+
enabled: cards.map(([key]) => enabled.has(key)),
|
|
108
|
+
layers: cards.map(([key]) => {
|
|
109
|
+
const position = enabledOrder.indexOf(key)
|
|
110
|
+
return position === -1 ? null : position + 1
|
|
111
|
+
}),
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
64
115
|
/** Map engine sample groups to the live preview rail shape. */
|
|
65
116
|
export function engineGroupsToRail(groups: SampleGroup[]): FieldRecipeGroup[] {
|
|
66
117
|
return groups.map(g => ({ name: g.name, count: g.samples.length, color: g.color }))
|
|
@@ -17,8 +17,8 @@ import { useAutoGroup, parseCSV } from '../composables/useAutoGroup'
|
|
|
17
17
|
import { useApi } from '../composables/useApi'
|
|
18
18
|
import { readFileAsText } from '../composables/useFileImport'
|
|
19
19
|
import {
|
|
20
|
-
engineFieldsToCards,
|
|
21
20
|
engineGroupsToRail,
|
|
21
|
+
engineSchemasToCards,
|
|
22
22
|
engineToQcRoute,
|
|
23
23
|
manualSamplesToResult,
|
|
24
24
|
qcRouteToEngine,
|
|
@@ -82,9 +82,11 @@ function setQcRouting(value: QcRouting) {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// ── Auto mode (Field Recipe) ─────────────────────────────────────────
|
|
85
|
-
// The
|
|
86
|
-
|
|
87
|
-
const fieldCards = computed(() =>
|
|
85
|
+
// The recipe represents the whole batch, including fields that only exist in
|
|
86
|
+
// another class or token-length schema split.
|
|
87
|
+
const fieldCards = computed(() =>
|
|
88
|
+
engineSchemasToCards(autoGroup.schemas.value, autoGroup.activeClassKey.value),
|
|
89
|
+
)
|
|
88
90
|
const railGroups = computed(() =>
|
|
89
91
|
engineGroupsToRail(autoGroup.result.value.experimentalGroups ?? autoGroup.groups.value),
|
|
90
92
|
)
|
|
@@ -311,7 +311,10 @@ export function useAutoGroup() {
|
|
|
311
311
|
// the same field at a different position.
|
|
312
312
|
function schemasExposing(fieldKey: string): { key: string; index: number }[] {
|
|
313
313
|
return Object.entries(schemas.value).flatMap(([key, schema]) => {
|
|
314
|
-
const col = schema.columns.find(c =>
|
|
314
|
+
const col = schema.columns.find(c =>
|
|
315
|
+
(c.displayName ?? c.name) === fieldKey
|
|
316
|
+
&& (c.role === 'factor' || c.role === 'replicate' || schema.groupBy.includes(c.index)),
|
|
317
|
+
)
|
|
315
318
|
return col ? [{ key, index: col.index }] : []
|
|
316
319
|
})
|
|
317
320
|
}
|
|
@@ -322,15 +325,14 @@ export function useAutoGroup() {
|
|
|
322
325
|
}
|
|
323
326
|
|
|
324
327
|
/**
|
|
325
|
-
* Toggle a grouping field across every class exposing it. The
|
|
326
|
-
*
|
|
327
|
-
*
|
|
328
|
+
* Toggle a grouping field across every class exposing it. The batch card is
|
|
329
|
+
* on when any schema groups by the field, so clicking it disables all such
|
|
330
|
+
* schemas; only a fully disabled field is enabled across the batch.
|
|
328
331
|
*/
|
|
329
332
|
function toggleGroupByField(fieldKey: string) {
|
|
330
333
|
const targets = schemasExposing(fieldKey)
|
|
331
334
|
if (targets.length === 0) return
|
|
332
|
-
const
|
|
333
|
-
const enable = !(schemas.value[active.key]?.groupBy.includes(active.index) ?? false)
|
|
335
|
+
const enable = !targets.some(({ key, index }) => schemas.value[key]?.groupBy.includes(index))
|
|
334
336
|
for (const { key, index } of targets) {
|
|
335
337
|
const schema = schemas.value[key]
|
|
336
338
|
if (!schema || schema.groupBy.includes(index) === enable) continue
|
|
@@ -347,7 +349,9 @@ export function useAutoGroup() {
|
|
|
347
349
|
function moveGroupByField(fieldKey: string, direction: 'up' | 'down') {
|
|
348
350
|
const targets = schemasExposing(fieldKey)
|
|
349
351
|
if (targets.length === 0) return
|
|
350
|
-
const active = targets.find(target =>
|
|
352
|
+
const active = targets.find(target =>
|
|
353
|
+
target.key === activeClassKey.value && schemas.value[target.key]?.groupBy.includes(target.index),
|
|
354
|
+
) ?? targets.find(target => schemas.value[target.key]?.groupBy.includes(target.index)) ?? targets[0]
|
|
351
355
|
const activeSchema = schemas.value[active.key]
|
|
352
356
|
if (!activeSchema) return
|
|
353
357
|
const from = activeSchema.groupBy.indexOf(active.index)
|