@morscherlab/mld-sdk 0.7.6 → 0.7.7
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.vue.js +2 -2
- package/dist/components/AutoGroupModal.vue.js.map +1 -1
- package/dist/components/NumberInput.vue.js +53 -50
- package/dist/components/NumberInput.vue.js.map +1 -1
- package/dist/composables/useAutoGroup.d.ts +1 -0
- package/dist/composables/useAutoGroup.js +24 -4
- package/dist/composables/useAutoGroup.js.map +1 -1
- package/dist/styles.css +46 -18
- package/package.json +1 -1
- package/src/__tests__/composables/useAutoGroup.test.ts +133 -0
- package/src/components/AutoGroupModal.story.vue +100 -32
- package/src/components/AutoGroupModal.vue +2 -2
- package/src/components/NumberInput.vue +33 -31
- package/src/composables/useAutoGroup.ts +34 -5
- package/src/styles/components/app-sidebar.css +1 -1
- package/src/styles/components/auto-group-modal.css +5 -4
- package/src/styles/components/modal.css +6 -0
- package/src/styles/components/number-input.css +7 -5
- package/src/styles/components/step-wizard.css +5 -0
package/dist/styles.css
CHANGED
|
@@ -1205,7 +1205,7 @@ html.dark .focus\:ring-offset-2:focus {
|
|
|
1205
1205
|
transform: translateX(0.75rem);
|
|
1206
1206
|
}
|
|
1207
1207
|
.mld-sidebar__sections > .mld-collapsible-card .mld-collapsible-card__content {
|
|
1208
|
-
padding: 0
|
|
1208
|
+
padding: 0.75rem;
|
|
1209
1209
|
display: flex;
|
|
1210
1210
|
flex-direction: column;
|
|
1211
1211
|
gap: 0.75rem;
|
|
@@ -3285,6 +3285,9 @@ html.dark .mld-checkbox__native:focus-visible + .mld-checkbox__box {
|
|
|
3285
3285
|
.mld-modal__container {
|
|
3286
3286
|
position: relative;
|
|
3287
3287
|
width: 100%;
|
|
3288
|
+
max-height: calc(100vh - 2rem);
|
|
3289
|
+
display: flex;
|
|
3290
|
+
flex-direction: column;
|
|
3288
3291
|
background-color: var(--bg-card);
|
|
3289
3292
|
border-radius: var(--mld-radius-lg);
|
|
3290
3293
|
box-shadow: var(--mld-shadow-lg);
|
|
@@ -3342,6 +3345,9 @@ html.dark .mld-checkbox__native:focus-visible + .mld-checkbox__box {
|
|
|
3342
3345
|
/* Modal Body */
|
|
3343
3346
|
.mld-modal__body {
|
|
3344
3347
|
padding: 1rem 1.5rem;
|
|
3348
|
+
flex: 1;
|
|
3349
|
+
min-height: 0;
|
|
3350
|
+
overflow-y: auto;
|
|
3345
3351
|
}
|
|
3346
3352
|
/* Modal Footer */
|
|
3347
3353
|
.mld-modal__footer {
|
|
@@ -3399,6 +3405,11 @@ html.dark .mld-checkbox__native:focus-visible + .mld-checkbox__box {
|
|
|
3399
3405
|
.mld-number-input--disabled {
|
|
3400
3406
|
opacity: 0.5;
|
|
3401
3407
|
}
|
|
3408
|
+
.mld-number-input__buttons {
|
|
3409
|
+
display: flex;
|
|
3410
|
+
flex-shrink: 0;
|
|
3411
|
+
border-left: 1px solid var(--border-color);
|
|
3412
|
+
}
|
|
3402
3413
|
.mld-number-input__button {
|
|
3403
3414
|
display: flex;
|
|
3404
3415
|
align-items: center;
|
|
@@ -3421,9 +3432,6 @@ html.dark .mld-checkbox__native:focus-visible + .mld-checkbox__box {
|
|
|
3421
3432
|
.mld-number-input__button--decrement {
|
|
3422
3433
|
border-right: 1px solid var(--border-color);
|
|
3423
3434
|
}
|
|
3424
|
-
.mld-number-input__button--increment {
|
|
3425
|
-
border-left: 1px solid var(--border-color);
|
|
3426
|
-
}
|
|
3427
3435
|
.mld-number-input__button--sm {
|
|
3428
3436
|
width: 1.75rem;
|
|
3429
3437
|
}
|
|
@@ -3440,7 +3448,7 @@ html.dark .mld-checkbox__native:focus-visible + .mld-checkbox__box {
|
|
|
3440
3448
|
.mld-number-input__input {
|
|
3441
3449
|
flex: 1;
|
|
3442
3450
|
min-width: 0;
|
|
3443
|
-
text-align:
|
|
3451
|
+
text-align: left;
|
|
3444
3452
|
background-color: var(--bg-secondary);
|
|
3445
3453
|
color: var(--text-primary);
|
|
3446
3454
|
border: none;
|
|
@@ -5959,7 +5967,10 @@ html.dark .mld-toggle__track:focus-visible {
|
|
|
5959
5967
|
}
|
|
5960
5968
|
/* AutoGroupModal - Smart grouping wizard */
|
|
5961
5969
|
.mld-auto-group {
|
|
5962
|
-
|
|
5970
|
+
flex: 1;
|
|
5971
|
+
display: flex;
|
|
5972
|
+
flex-direction: column;
|
|
5973
|
+
min-height: 0;
|
|
5963
5974
|
}
|
|
5964
5975
|
/* --- Mode toggle --- */
|
|
5965
5976
|
.mld-auto-group__mode-toggle {
|
|
@@ -6301,9 +6312,7 @@ html.dark .mld-toggle__track:focus-visible {
|
|
|
6301
6312
|
.mld-auto-group__preview-groups {
|
|
6302
6313
|
display: flex;
|
|
6303
6314
|
flex-direction: column;
|
|
6304
|
-
gap: 0.
|
|
6305
|
-
max-height: 320px;
|
|
6306
|
-
overflow-y: auto;
|
|
6315
|
+
gap: 0.5rem;
|
|
6307
6316
|
}
|
|
6308
6317
|
.mld-auto-group__preview-group {
|
|
6309
6318
|
border: 1px solid var(--border-color);
|
|
@@ -10887,9 +10896,12 @@ html.dark .mld-settings-modal__option-btn--active {
|
|
|
10887
10896
|
flex-direction: column;
|
|
10888
10897
|
gap: 1.5rem;
|
|
10889
10898
|
outline: none;
|
|
10899
|
+
flex: 1;
|
|
10900
|
+
min-height: 0;
|
|
10890
10901
|
}
|
|
10891
10902
|
/* Progress indicator */
|
|
10892
10903
|
.mld-wizard__progress {
|
|
10904
|
+
flex-shrink: 0;
|
|
10893
10905
|
}
|
|
10894
10906
|
.mld-wizard__steps-indicator {
|
|
10895
10907
|
display: flex;
|
|
@@ -10970,6 +10982,7 @@ html.dark .mld-settings-modal__option-btn--active {
|
|
|
10970
10982
|
.mld-wizard__body {
|
|
10971
10983
|
flex: 1;
|
|
10972
10984
|
min-height: 0;
|
|
10985
|
+
overflow-y: auto;
|
|
10973
10986
|
}
|
|
10974
10987
|
/* Navigation */
|
|
10975
10988
|
.mld-wizard__navigation {
|
|
@@ -10978,6 +10991,7 @@ html.dark .mld-settings-modal__option-btn--active {
|
|
|
10978
10991
|
gap: 0.5rem;
|
|
10979
10992
|
padding-top: 1rem;
|
|
10980
10993
|
border-top: 1px solid var(--border-color);
|
|
10994
|
+
flex-shrink: 0;
|
|
10981
10995
|
}
|
|
10982
10996
|
.mld-wizard__nav-btn {
|
|
10983
10997
|
padding: 0.5rem 1rem;
|
|
@@ -13469,6 +13483,9 @@ html.dark .mld-radio-option__native:focus-visible + .mld-radio-option__circle {
|
|
|
13469
13483
|
.mld-modal__container {
|
|
13470
13484
|
position: relative;
|
|
13471
13485
|
width: 100%;
|
|
13486
|
+
max-height: calc(100vh - 2rem);
|
|
13487
|
+
display: flex;
|
|
13488
|
+
flex-direction: column;
|
|
13472
13489
|
background-color: var(--bg-card);
|
|
13473
13490
|
border-radius: var(--mld-radius-lg);
|
|
13474
13491
|
box-shadow: var(--mld-shadow-lg);
|
|
@@ -13528,6 +13545,9 @@ html.dark .mld-radio-option__native:focus-visible + .mld-radio-option__circle {
|
|
|
13528
13545
|
/* Modal Body */
|
|
13529
13546
|
.mld-modal__body {
|
|
13530
13547
|
padding: 1rem 1.5rem;
|
|
13548
|
+
flex: 1;
|
|
13549
|
+
min-height: 0;
|
|
13550
|
+
overflow-y: auto;
|
|
13531
13551
|
}
|
|
13532
13552
|
|
|
13533
13553
|
/* Modal Footer */
|
|
@@ -15249,6 +15269,11 @@ to { transform: rotate(360deg);
|
|
|
15249
15269
|
.mld-number-input--disabled {
|
|
15250
15270
|
opacity: 0.5;
|
|
15251
15271
|
}
|
|
15272
|
+
.mld-number-input__buttons {
|
|
15273
|
+
display: flex;
|
|
15274
|
+
flex-shrink: 0;
|
|
15275
|
+
border-left: 1px solid var(--border-color);
|
|
15276
|
+
}
|
|
15252
15277
|
.mld-number-input__button {
|
|
15253
15278
|
display: flex;
|
|
15254
15279
|
align-items: center;
|
|
@@ -15271,9 +15296,6 @@ to { transform: rotate(360deg);
|
|
|
15271
15296
|
.mld-number-input__button--decrement {
|
|
15272
15297
|
border-right: 1px solid var(--border-color);
|
|
15273
15298
|
}
|
|
15274
|
-
.mld-number-input__button--increment {
|
|
15275
|
-
border-left: 1px solid var(--border-color);
|
|
15276
|
-
}
|
|
15277
15299
|
.mld-number-input__button--sm {
|
|
15278
15300
|
width: 1.75rem;
|
|
15279
15301
|
}
|
|
@@ -15290,7 +15312,7 @@ to { transform: rotate(360deg);
|
|
|
15290
15312
|
.mld-number-input__input {
|
|
15291
15313
|
flex: 1;
|
|
15292
15314
|
min-width: 0;
|
|
15293
|
-
text-align:
|
|
15315
|
+
text-align: left;
|
|
15294
15316
|
background-color: var(--bg-secondary);
|
|
15295
15317
|
color: var(--text-primary);
|
|
15296
15318
|
border: none;
|
|
@@ -16829,7 +16851,7 @@ html.dark .mld-settings-modal__option-btn--active {
|
|
|
16829
16851
|
transform: translateX(0.75rem);
|
|
16830
16852
|
}
|
|
16831
16853
|
.mld-sidebar__sections > .mld-collapsible-card .mld-collapsible-card__content {
|
|
16832
|
-
padding: 0
|
|
16854
|
+
padding: 0.75rem;
|
|
16833
16855
|
display: flex;
|
|
16834
16856
|
flex-direction: column;
|
|
16835
16857
|
gap: 0.75rem;
|
|
@@ -19454,10 +19476,13 @@ to {
|
|
|
19454
19476
|
flex-direction: column;
|
|
19455
19477
|
gap: 1.5rem;
|
|
19456
19478
|
outline: none;
|
|
19479
|
+
flex: 1;
|
|
19480
|
+
min-height: 0;
|
|
19457
19481
|
}
|
|
19458
19482
|
|
|
19459
19483
|
/* Progress indicator */
|
|
19460
19484
|
.mld-wizard__progress {
|
|
19485
|
+
flex-shrink: 0;
|
|
19461
19486
|
}
|
|
19462
19487
|
.mld-wizard__steps-indicator {
|
|
19463
19488
|
display: flex;
|
|
@@ -19540,6 +19565,7 @@ to {
|
|
|
19540
19565
|
.mld-wizard__body {
|
|
19541
19566
|
flex: 1;
|
|
19542
19567
|
min-height: 0;
|
|
19568
|
+
overflow-y: auto;
|
|
19543
19569
|
}
|
|
19544
19570
|
|
|
19545
19571
|
/* Navigation */
|
|
@@ -19549,6 +19575,7 @@ to {
|
|
|
19549
19575
|
gap: 0.5rem;
|
|
19550
19576
|
padding-top: 1rem;
|
|
19551
19577
|
border-top: 1px solid var(--border-color);
|
|
19578
|
+
flex-shrink: 0;
|
|
19552
19579
|
}
|
|
19553
19580
|
.mld-wizard__nav-btn {
|
|
19554
19581
|
padding: 0.5rem 1rem;
|
|
@@ -19612,7 +19639,10 @@ to {
|
|
|
19612
19639
|
}
|
|
19613
19640
|
/* AutoGroupModal - Smart grouping wizard */
|
|
19614
19641
|
.mld-auto-group {
|
|
19615
|
-
|
|
19642
|
+
flex: 1;
|
|
19643
|
+
display: flex;
|
|
19644
|
+
flex-direction: column;
|
|
19645
|
+
min-height: 0;
|
|
19616
19646
|
}
|
|
19617
19647
|
|
|
19618
19648
|
/* --- Mode toggle --- */
|
|
@@ -19960,9 +19990,7 @@ to {
|
|
|
19960
19990
|
.mld-auto-group__preview-groups {
|
|
19961
19991
|
display: flex;
|
|
19962
19992
|
flex-direction: column;
|
|
19963
|
-
gap: 0.
|
|
19964
|
-
max-height: 320px;
|
|
19965
|
-
overflow-y: auto;
|
|
19993
|
+
gap: 0.5rem;
|
|
19966
19994
|
}
|
|
19967
19995
|
.mld-auto-group__preview-group {
|
|
19968
19996
|
border: 1px solid var(--border-color);
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest'
|
|
|
2
2
|
import {
|
|
3
3
|
analyzeDelimiter,
|
|
4
4
|
detectOutliers,
|
|
5
|
+
classifyOutlierAction,
|
|
5
6
|
extractColumns,
|
|
6
7
|
parseCSV,
|
|
7
8
|
parseCSVLine,
|
|
@@ -373,3 +374,135 @@ describe('computeGroups', () => {
|
|
|
373
374
|
expect(treatGroup?.samples).toHaveLength(3)
|
|
374
375
|
})
|
|
375
376
|
})
|
|
377
|
+
|
|
378
|
+
describe('classifyOutlierAction', () => {
|
|
379
|
+
it('should return qc for samples containing QC keywords', () => {
|
|
380
|
+
expect(classifyOutlierAction('LT_13102025_EQC_Jurkat_1_2', '_')).toBe('qc')
|
|
381
|
+
expect(classifyOutlierAction('LT_13102025_IQC_Pool_1', '_')).toBe('qc')
|
|
382
|
+
expect(classifyOutlierAction('Blank_001', '_')).toBe('qc')
|
|
383
|
+
expect(classifyOutlierAction('STD_Low_1', '_')).toBe('qc')
|
|
384
|
+
expect(classifyOutlierAction('test_EQC_Jurkat_02', '_')).toBe('qc')
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
it('should match case-insensitively', () => {
|
|
388
|
+
expect(classifyOutlierAction('LT_eqc_Pool', '_')).toBe('qc')
|
|
389
|
+
expect(classifyOutlierAction('LT_EQC_Pool', '_')).toBe('qc')
|
|
390
|
+
expect(classifyOutlierAction('LT_Eqc_Pool', '_')).toBe('qc')
|
|
391
|
+
expect(classifyOutlierAction('BLANK_001', '_')).toBe('qc')
|
|
392
|
+
expect(classifyOutlierAction('Test_Sample', '_')).toBe('qc')
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
it('should match against individual segments only', () => {
|
|
396
|
+
// "eqc" embedded inside a segment should NOT match
|
|
397
|
+
expect(classifyOutlierAction('LT_SEQC123_Pool', '_')).toBe('include')
|
|
398
|
+
// "std" as standalone segment should match
|
|
399
|
+
expect(classifyOutlierAction('LT_std_Pool', '_')).toBe('qc')
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
it('should return include for regular experimental samples', () => {
|
|
403
|
+
expect(classifyOutlierAction('LT_13102025_212_WT_Glu', '_')).toBe('include')
|
|
404
|
+
expect(classifyOutlierAction('Control_Rep1', '_')).toBe('include')
|
|
405
|
+
expect(classifyOutlierAction('Treatment_High_Rep3', '_')).toBe('include')
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
it('should work with different delimiters', () => {
|
|
409
|
+
expect(classifyOutlierAction('LT-EQC-Pool', '-')).toBe('qc')
|
|
410
|
+
expect(classifyOutlierAction('blank.001', '.')).toBe('qc')
|
|
411
|
+
expect(classifyOutlierAction('Control-Rep1', '-')).toBe('include')
|
|
412
|
+
})
|
|
413
|
+
})
|
|
414
|
+
|
|
415
|
+
describe('integration: mixed experimental + QC + test samples', () => {
|
|
416
|
+
// Simulates the real dataset pattern:
|
|
417
|
+
// 154 experimental (11 fields), ~15 QC (6-7 fields), 3 test (4 fields)
|
|
418
|
+
const experimental = [
|
|
419
|
+
'LT_13102025_212_WT_Glu_3_20_S_091123_T1_33',
|
|
420
|
+
'LT_13102025_213_WT_Glu_3_20_S_091123_T1_34',
|
|
421
|
+
'LT_13102025_214_KI_Glu_3_20_S_091123_T1_35',
|
|
422
|
+
'LT_13102025_215_KI_Glu_6_40_L_091123_T1_36',
|
|
423
|
+
'LT_13102025_216_WT_Glu_6_40_L_091123_T1_37',
|
|
424
|
+
'LT_13102025_217_WT_Glu_6_40_L_091123_T2_38',
|
|
425
|
+
'LT_13102025_218_KI_Glu_3_20_S_091123_T2_39',
|
|
426
|
+
]
|
|
427
|
+
const qcSamples = [
|
|
428
|
+
'LT_13102025_EQC_Jurkat_1_2',
|
|
429
|
+
'LT_13102025_IQC_Pool_3',
|
|
430
|
+
]
|
|
431
|
+
const testSamples = [
|
|
432
|
+
'test_EQC_Jurkat_02',
|
|
433
|
+
]
|
|
434
|
+
const allLines = [...experimental, ...qcSamples, ...testSamples]
|
|
435
|
+
|
|
436
|
+
it('should detect dominantFieldCount=11 and flag QC/test as outliers', () => {
|
|
437
|
+
const analysis = analyzeDelimiter(allLines)
|
|
438
|
+
expect(analysis.delimiter).toBe('_')
|
|
439
|
+
expect(analysis.dominantFieldCount).toBe(11)
|
|
440
|
+
|
|
441
|
+
const outliers = detectOutliers(allLines, '_', analysis.dominantFieldCount)
|
|
442
|
+
// QC samples (6 fields) and test samples (4 fields) are outliers
|
|
443
|
+
expect(outliers).toHaveLength(qcSamples.length + testSamples.length)
|
|
444
|
+
|
|
445
|
+
// Experimental samples should NOT be flagged
|
|
446
|
+
const outlierIndices = new Set(outliers.map(o => o.index))
|
|
447
|
+
for (let i = 0; i < experimental.length; i++) {
|
|
448
|
+
expect(outlierIndices.has(i)).toBe(false)
|
|
449
|
+
}
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('should auto-classify QC/test outliers with smart defaults', () => {
|
|
453
|
+
const analysis = analyzeDelimiter(allLines)
|
|
454
|
+
const outliers = detectOutliers(allLines, '_', analysis.dominantFieldCount)
|
|
455
|
+
|
|
456
|
+
for (const outlier of outliers) {
|
|
457
|
+
outlier.action = classifyOutlierAction(outlier.sample, '_')
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// All QC and test samples should be classified as 'qc'
|
|
461
|
+
expect(outliers.every(o => o.action === 'qc')).toBe(true)
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
it('should produce 11 columns from conforming samples', () => {
|
|
465
|
+
const analysis = analyzeDelimiter(allLines)
|
|
466
|
+
const outliers = detectOutliers(allLines, '_', analysis.dominantFieldCount)
|
|
467
|
+
const conforming = allLines.filter(
|
|
468
|
+
(_, i) => !outliers.some(o => o.index === i)
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
const conformingFieldCounts = conforming.map(s => s.split('_').length)
|
|
472
|
+
const effectiveMinFieldCount = Math.min(...conformingFieldCounts)
|
|
473
|
+
|
|
474
|
+
const columns = extractColumns(conforming, '_', effectiveMinFieldCount)
|
|
475
|
+
expect(columns).toHaveLength(11)
|
|
476
|
+
})
|
|
477
|
+
|
|
478
|
+
it('should auto-disable constant columns (cardinality 1)', () => {
|
|
479
|
+
const analysis = analyzeDelimiter(allLines)
|
|
480
|
+
const outliers = detectOutliers(allLines, '_', analysis.dominantFieldCount)
|
|
481
|
+
const conforming = allLines.filter(
|
|
482
|
+
(_, i) => !outliers.some(o => o.index === i)
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
const conformingFieldCounts = conforming.map(s => s.split('_').length)
|
|
486
|
+
const effectiveMinFieldCount = Math.min(...conformingFieldCounts)
|
|
487
|
+
|
|
488
|
+
const columns = extractColumns(conforming, '_', effectiveMinFieldCount)
|
|
489
|
+
|
|
490
|
+
// Auto-disable: only enable columns with cardinality > 1
|
|
491
|
+
const enabled = new Set(
|
|
492
|
+
columns.filter(f => f.cardinality > 1).map(f => f.index)
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
// Constant columns (LT, 13102025, Glu, 091123) should be disabled
|
|
496
|
+
const constantCols = columns.filter(c => c.cardinality === 1)
|
|
497
|
+
for (const col of constantCols) {
|
|
498
|
+
expect(enabled.has(col.index)).toBe(false)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Variable columns should be enabled
|
|
502
|
+
const variableCols = columns.filter(c => c.cardinality > 1)
|
|
503
|
+
for (const col of variableCols) {
|
|
504
|
+
expect(enabled.has(col.index)).toBe(true)
|
|
505
|
+
}
|
|
506
|
+
expect(variableCols.length).toBeGreaterThan(0)
|
|
507
|
+
})
|
|
508
|
+
})
|
|
@@ -19,6 +19,29 @@ const withOutliersSamples = [
|
|
|
19
19
|
'Ctrl_Brain_D1_Rep1', 'Ctrl_Brain_D1_Rep2',
|
|
20
20
|
]
|
|
21
21
|
|
|
22
|
+
// Real-world mixed dataset: 11-field experimental + 6-field QC + 4-field test
|
|
23
|
+
// This exercises the dominantFieldCount outlier fix
|
|
24
|
+
const mixedFieldCountSamples = [
|
|
25
|
+
// Experimental (11 fields each)
|
|
26
|
+
'LT_13102025_212_WT_Glu_3_20_S_091123_T1_33',
|
|
27
|
+
'LT_13102025_213_WT_Glu_3_20_S_091123_T1_34',
|
|
28
|
+
'LT_13102025_214_KI_Glu_3_20_S_091123_T1_35',
|
|
29
|
+
'LT_13102025_215_KI_Glu_6_40_L_091123_T1_36',
|
|
30
|
+
'LT_13102025_216_WT_Glu_6_40_L_091123_T1_37',
|
|
31
|
+
'LT_13102025_217_WT_Glu_6_40_L_091123_T2_38',
|
|
32
|
+
'LT_13102025_218_KI_Glu_3_20_S_091123_T2_39',
|
|
33
|
+
'LT_13102025_219_KI_Glu_6_40_L_091123_T2_40',
|
|
34
|
+
'LT_13102025_220_WT_Glu_3_20_S_091123_T2_41',
|
|
35
|
+
'LT_13102025_221_WT_Glu_6_40_L_091123_T2_42',
|
|
36
|
+
// QC samples (6 fields)
|
|
37
|
+
'LT_13102025_EQC_Jurkat_1_2',
|
|
38
|
+
'LT_13102025_EQC_Jurkat_2_8',
|
|
39
|
+
'LT_13102025_IQC_Pool_1_5',
|
|
40
|
+
// Test samples (4 fields)
|
|
41
|
+
'test_EQC_Jurkat_02',
|
|
42
|
+
'test_Blank_Run_01',
|
|
43
|
+
]
|
|
44
|
+
|
|
22
45
|
// Hyphen-delimited samples
|
|
23
46
|
const hyphenSamples = [
|
|
24
47
|
'WT-Vehicle-1', 'WT-Vehicle-2', 'WT-Vehicle-3',
|
|
@@ -35,10 +58,10 @@ function handleApply(result: AutoGroupResult) {
|
|
|
35
58
|
console.log('AutoGroupModal result:', result)
|
|
36
59
|
}
|
|
37
60
|
|
|
38
|
-
function initState() {
|
|
61
|
+
function initState(samples: string[] = proteomicsSamples) {
|
|
39
62
|
return {
|
|
40
|
-
isOpen:
|
|
41
|
-
samples
|
|
63
|
+
isOpen: true,
|
|
64
|
+
samples,
|
|
42
65
|
lastResult: null as AutoGroupResult | null,
|
|
43
66
|
}
|
|
44
67
|
}
|
|
@@ -46,7 +69,7 @@ function initState() {
|
|
|
46
69
|
|
|
47
70
|
<template>
|
|
48
71
|
<Story title="Lab/AutoGroupModal">
|
|
49
|
-
<Variant title="Playground" :init-state="
|
|
72
|
+
<Variant title="Playground" :init-state="() => ({ isOpen: false, samples: proteomicsSamples, lastResult: null as AutoGroupResult | null })">
|
|
50
73
|
<template #default="{ state }">
|
|
51
74
|
<div style="padding: 2rem;">
|
|
52
75
|
<button
|
|
@@ -80,53 +103,98 @@ function initState() {
|
|
|
80
103
|
</template>
|
|
81
104
|
<template #controls="{ state }">
|
|
82
105
|
<HstSelect
|
|
83
|
-
:model-value="state.samples === proteomicsSamples ? 'proteomics' : state.samples === withOutliersSamples ? 'outliers' : 'hyphen'"
|
|
106
|
+
:model-value="state.samples === proteomicsSamples ? 'proteomics' : state.samples === withOutliersSamples ? 'outliers' : state.samples === mixedFieldCountSamples ? 'mixed' : 'hyphen'"
|
|
84
107
|
title="Sample Set"
|
|
85
108
|
:options="[
|
|
86
109
|
{ value: 'proteomics', label: 'Proteomics (clean)' },
|
|
87
110
|
{ value: 'outliers', label: 'With QC/Blanks' },
|
|
111
|
+
{ value: 'mixed', label: 'Mixed Field Counts (11 + 6 + 4)' },
|
|
88
112
|
{ value: 'hyphen', label: 'Hyphen-delimited' },
|
|
89
113
|
]"
|
|
90
114
|
@update:model-value="(v: string) => {
|
|
91
115
|
if (v === 'proteomics') state.samples = proteomicsSamples
|
|
92
116
|
else if (v === 'outliers') state.samples = withOutliersSamples
|
|
117
|
+
else if (v === 'mixed') state.samples = mixedFieldCountSamples
|
|
93
118
|
else state.samples = hyphenSamples
|
|
94
119
|
}"
|
|
95
120
|
/>
|
|
96
121
|
</template>
|
|
97
122
|
</Variant>
|
|
98
123
|
|
|
99
|
-
<Variant title="Pre-filled (Proteomics)">
|
|
100
|
-
<
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
124
|
+
<Variant title="Pre-filled (Proteomics)" :init-state="() => initState(proteomicsSamples)">
|
|
125
|
+
<template #default="{ state }">
|
|
126
|
+
<div style="padding: 2rem;">
|
|
127
|
+
<button
|
|
128
|
+
type="button"
|
|
129
|
+
style="padding: 0.5rem 1rem; background: var(--bg-card, #fff); border: 1px solid var(--border-color, #e5e7eb); border-radius: 0.375rem; cursor: pointer; font-size: 0.875rem;"
|
|
130
|
+
@click="state.isOpen = true"
|
|
131
|
+
>
|
|
132
|
+
Re-open Modal
|
|
133
|
+
</button>
|
|
134
|
+
<AutoGroupModal
|
|
135
|
+
v-model="state.isOpen"
|
|
136
|
+
:samples="state.samples"
|
|
137
|
+
@apply="handleApply"
|
|
138
|
+
/>
|
|
139
|
+
</div>
|
|
140
|
+
</template>
|
|
108
141
|
</Variant>
|
|
109
142
|
|
|
110
|
-
<Variant title="With Outliers (QC/Blanks)">
|
|
111
|
-
<
|
|
112
|
-
<
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
143
|
+
<Variant title="With Outliers (QC/Blanks)" :init-state="() => initState(withOutliersSamples)">
|
|
144
|
+
<template #default="{ state }">
|
|
145
|
+
<div style="padding: 2rem;">
|
|
146
|
+
<button
|
|
147
|
+
type="button"
|
|
148
|
+
style="padding: 0.5rem 1rem; background: var(--bg-card, #fff); border: 1px solid var(--border-color, #e5e7eb); border-radius: 0.375rem; cursor: pointer; font-size: 0.875rem;"
|
|
149
|
+
@click="state.isOpen = true"
|
|
150
|
+
>
|
|
151
|
+
Re-open Modal
|
|
152
|
+
</button>
|
|
153
|
+
<AutoGroupModal
|
|
154
|
+
v-model="state.isOpen"
|
|
155
|
+
:samples="state.samples"
|
|
156
|
+
@apply="handleApply"
|
|
157
|
+
/>
|
|
158
|
+
</div>
|
|
159
|
+
</template>
|
|
119
160
|
</Variant>
|
|
120
161
|
|
|
121
|
-
<Variant title="
|
|
122
|
-
<
|
|
123
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
162
|
+
<Variant title="Mixed Field Counts (Smart Outlier Detection)" :init-state="() => initState(mixedFieldCountSamples)">
|
|
163
|
+
<template #default="{ state }">
|
|
164
|
+
<div style="padding: 2rem;">
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
style="padding: 0.5rem 1rem; background: var(--bg-card, #fff); border: 1px solid var(--border-color, #e5e7eb); border-radius: 0.375rem; cursor: pointer; font-size: 0.875rem;"
|
|
168
|
+
@click="state.isOpen = true"
|
|
169
|
+
>
|
|
170
|
+
Re-open Modal
|
|
171
|
+
</button>
|
|
172
|
+
<AutoGroupModal
|
|
173
|
+
v-model="state.isOpen"
|
|
174
|
+
:samples="state.samples"
|
|
175
|
+
@apply="handleApply"
|
|
176
|
+
/>
|
|
177
|
+
</div>
|
|
178
|
+
</template>
|
|
179
|
+
</Variant>
|
|
180
|
+
|
|
181
|
+
<Variant title="Hyphen-Delimited" :init-state="() => initState(hyphenSamples)">
|
|
182
|
+
<template #default="{ state }">
|
|
183
|
+
<div style="padding: 2rem;">
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
style="padding: 0.5rem 1rem; background: var(--bg-card, #fff); border: 1px solid var(--border-color, #e5e7eb); border-radius: 0.375rem; cursor: pointer; font-size: 0.875rem;"
|
|
187
|
+
@click="state.isOpen = true"
|
|
188
|
+
>
|
|
189
|
+
Re-open Modal
|
|
190
|
+
</button>
|
|
191
|
+
<AutoGroupModal
|
|
192
|
+
v-model="state.isOpen"
|
|
193
|
+
:samples="state.samples"
|
|
194
|
+
@apply="handleApply"
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
</template>
|
|
130
198
|
</Variant>
|
|
131
199
|
|
|
132
200
|
<Variant title="Empty (Manual Paste)">
|
|
@@ -38,7 +38,7 @@ watch(() => props.modelValue, (open) => {
|
|
|
38
38
|
autoGroup.rawText.value = props.samples.join('\n')
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
-
})
|
|
41
|
+
}, { immediate: true })
|
|
42
42
|
|
|
43
43
|
// Dynamic steps: skip outlier step when no outliers
|
|
44
44
|
const allSteps: WizardStep[] = [
|
|
@@ -239,7 +239,7 @@ const isFirstStep = computed(() => currentStep.value === 0)
|
|
|
239
239
|
<div class="mld-auto-group__outlier-banner">
|
|
240
240
|
{{ autoGroup.outliers.value.length }} of {{ autoGroup.samples.value.length }}
|
|
241
241
|
samples have irregular structure
|
|
242
|
-
(fewer than {{ autoGroup.
|
|
242
|
+
(fewer than {{ autoGroup.dominantFieldCount.value }} fields, delimiter
|
|
243
243
|
<code>{{ autoGroup.delimiter.value }}</code>)
|
|
244
244
|
</div>
|
|
245
245
|
|
|
@@ -74,22 +74,6 @@ function increment() {
|
|
|
74
74
|
disabled ? 'mld-number-input--disabled' : '',
|
|
75
75
|
]"
|
|
76
76
|
>
|
|
77
|
-
<button
|
|
78
|
-
type="button"
|
|
79
|
-
aria-label="Decrease value"
|
|
80
|
-
:disabled="disabled || !canDecrement"
|
|
81
|
-
:class="[
|
|
82
|
-
'mld-number-input__button',
|
|
83
|
-
'mld-number-input__button--decrement',
|
|
84
|
-
`mld-number-input__button--${size}`,
|
|
85
|
-
]"
|
|
86
|
-
@click="decrement"
|
|
87
|
-
>
|
|
88
|
-
<svg class="mld-number-input__button-icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
|
89
|
-
<path d="M5 12h14" />
|
|
90
|
-
</svg>
|
|
91
|
-
</button>
|
|
92
|
-
|
|
93
77
|
<input
|
|
94
78
|
type="number"
|
|
95
79
|
:value="modelValue"
|
|
@@ -106,21 +90,39 @@ function increment() {
|
|
|
106
90
|
@input="handleInput"
|
|
107
91
|
/>
|
|
108
92
|
|
|
109
|
-
<
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
93
|
+
<div class="mld-number-input__buttons">
|
|
94
|
+
<button
|
|
95
|
+
type="button"
|
|
96
|
+
aria-label="Decrease value"
|
|
97
|
+
:disabled="disabled || !canDecrement"
|
|
98
|
+
:class="[
|
|
99
|
+
'mld-number-input__button',
|
|
100
|
+
'mld-number-input__button--decrement',
|
|
101
|
+
`mld-number-input__button--${size}`,
|
|
102
|
+
]"
|
|
103
|
+
@click="decrement"
|
|
104
|
+
>
|
|
105
|
+
<svg class="mld-number-input__button-icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
|
106
|
+
<path d="M5 12h14" />
|
|
107
|
+
</svg>
|
|
108
|
+
</button>
|
|
109
|
+
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
aria-label="Increase value"
|
|
113
|
+
:disabled="disabled || !canIncrement"
|
|
114
|
+
:class="[
|
|
115
|
+
'mld-number-input__button',
|
|
116
|
+
'mld-number-input__button--increment',
|
|
117
|
+
`mld-number-input__button--${size}`,
|
|
118
|
+
]"
|
|
119
|
+
@click="increment"
|
|
120
|
+
>
|
|
121
|
+
<svg class="mld-number-input__button-icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
|
122
|
+
<path d="M5 12h14" /><path d="M12 5v14" />
|
|
123
|
+
</svg>
|
|
124
|
+
</button>
|
|
125
|
+
</div>
|
|
124
126
|
</div>
|
|
125
127
|
</template>
|
|
126
128
|
|