@morscherlab/mint-sdk 1.3.0 → 1.3.2
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/__tests__/components/AutoGroupModal.anatomy.test.d.ts +1 -0
- package/dist/__tests__/components/AutoGroupModal.wiring.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/fieldEdits.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/golden.test.d.ts +1 -0
- package/dist/components/AutoGroupModal.adapter.d.ts +27 -4
- package/dist/components/PlateEditor.vue.d.ts +2 -0
- package/dist/components/SmartGroup.types.d.ts +22 -0
- package/dist/components/SmartGroupFieldRecipe.groups.d.ts +10 -2
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +3 -0
- package/dist/components/index.js +1 -1
- package/dist/{components-vHfxV2PB.js → components-CEePoD3W.js} +502 -264
- package/dist/components-CEePoD3W.js.map +1 -0
- package/dist/composables/autoGroup/classify.d.ts +7 -1
- package/dist/composables/autoGroup/columns.d.ts +16 -1
- package/dist/composables/autoGroup/index.d.ts +5 -7
- package/dist/composables/autoGroup/tokenize.d.ts +36 -1
- package/dist/composables/index.js +2 -2
- package/dist/composables/useAutoGroup.d.ts +13 -2
- package/dist/{composables-D6uqp0UF.js → composables-BbHozlA7.js} +2 -2
- package/dist/{composables-D6uqp0UF.js.map → composables-BbHozlA7.js.map} +1 -1
- package/dist/index.js +3 -3
- package/dist/install.js +1 -1
- package/dist/permissions.js +2 -2
- package/dist/permissions.js.map +1 -1
- package/dist/styles.css +71 -82
- package/dist/types/auto-group.d.ts +1 -1
- package/dist/{validation-Dmp9Q5W0.js → validation-CukfLjy-.js} +323 -187
- package/dist/validation-CukfLjy-.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/components/AutoGroupModal.anatomy.test.ts +86 -0
- package/src/__tests__/components/AutoGroupModal.wiring.test.ts +53 -0
- package/src/__tests__/components/PlateEditor.test.ts +10 -0
- package/src/__tests__/components/SampleSelector.test.ts +14 -16
- package/src/__tests__/components/SmartGroupFieldRecipe.groups.test.ts +23 -0
- package/src/__tests__/components/SmartGroupFieldRecipe.test.ts +176 -0
- package/src/__tests__/composables/autoGroup/columns.test.ts +100 -11
- package/src/__tests__/composables/autoGroup/fieldEdits.test.ts +74 -0
- package/src/__tests__/composables/autoGroup/fingerprint.test.ts +9 -0
- package/src/__tests__/composables/autoGroup/golden.test.ts +187 -0
- package/src/__tests__/composables/autoGroup/groupByOrder.test.ts +7 -7
- package/src/__tests__/composables/autoGroup/tokenLength.test.ts +7 -0
- package/src/__tests__/composables/autoGroup/tokenize.test.ts +78 -1
- package/src/__tests__/composables/useAutoGroup.test.ts +2 -1
- package/src/__tests__/fixtures/auto-group/share-exp021.txt +103 -0
- package/src/__tests__/fixtures/auto-group/share-exp031.txt +39 -0
- package/src/__tests__/fixtures/auto-group/share-exp114.txt +123 -0
- package/src/__tests__/fixtures/auto-group/share-exp214.txt +63 -0
- package/src/__tests__/utils/permissions.test.ts +15 -0
- package/src/components/AutoGroupModal.adapter.ts +80 -9
- package/src/components/AutoGroupModal.vue +41 -8
- package/src/components/PlateEditor.vue +3 -0
- package/src/components/SmartGroup.types.ts +23 -0
- package/src/components/SmartGroupFieldRecipe.groups.ts +14 -2
- package/src/components/SmartGroupFieldRecipe.vue +289 -169
- package/src/composables/autoGroup/classify.ts +26 -7
- package/src/composables/autoGroup/columns.ts +144 -36
- package/src/composables/autoGroup/compose.ts +8 -10
- package/src/composables/autoGroup/fingerprint.ts +1 -1
- package/src/composables/autoGroup/index.ts +5 -12
- package/src/composables/autoGroup/tokenLength.ts +8 -0
- package/src/composables/autoGroup/tokenize.ts +148 -1
- package/src/composables/autoGroup/vocab.json +5 -11
- package/src/composables/useAutoGroup.ts +92 -79
- package/src/permissions.ts +3 -2
- package/src/styles/components/smart-group.css +71 -82
- package/src/types/auto-group.ts +1 -0
- package/dist/components-vHfxV2PB.js.map +0 -1
- package/dist/composables/autoGroup/replicatePreGroup.d.ts +0 -36
- package/dist/validation-Dmp9Q5W0.js.map +0 -1
- package/src/composables/autoGroup/replicatePreGroup.ts +0 -124
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
exp114_250730_YY_HEK_DFMO_Put_blank_01
|
|
2
|
+
exp114_250730_YY_HEK_DFMO_Put_blank_05
|
|
3
|
+
exp114_250730_YY_HEK_DFMO_Put_blank_05_b
|
|
4
|
+
exp114_250730_YY_HEK_DFMO_Put_blank_119
|
|
5
|
+
exp114_250730_YY_HEK_DFMO_Put_blank_120
|
|
6
|
+
exp114_250730_YY_HEK_DFMO_Put_blank_33
|
|
7
|
+
exp114_250730_YY_HEK_DFMO_Put_blank_58b_postCrash
|
|
8
|
+
exp114_250730_YY_HEK_DFMO_Put_blank_61
|
|
9
|
+
exp114_250730_YY_HEK_DFMO_Put_blank_90
|
|
10
|
+
exp114_250730_YY_HEK_DFMO_Put_DigestControl_91
|
|
11
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_1-1_35
|
|
12
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_1-2_44
|
|
13
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_1-3_52
|
|
14
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_2-1_36
|
|
15
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_2-2_45
|
|
16
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_2-3_53
|
|
17
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_3-1_37
|
|
18
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_3-2_46
|
|
19
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_3-3_54
|
|
20
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_4-1_09
|
|
21
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_4-1_38
|
|
22
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_4-2_47
|
|
23
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_4-3_55
|
|
24
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_5-1_39
|
|
25
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_5-2_48
|
|
26
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_5-3_56
|
|
27
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_6-1_40
|
|
28
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_6-2_49
|
|
29
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_6-3_57
|
|
30
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_7-1_41
|
|
31
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_7-2_50
|
|
32
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_7-3_rep2_89
|
|
33
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_8-1_42
|
|
34
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_8-3_59
|
|
35
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_8-3_59_20250731221931
|
|
36
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_9-1_43
|
|
37
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_9-2_51
|
|
38
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_9-3_60
|
|
39
|
+
exp114_250730_YY_HEK_DFMO_Put_HMW_DigestControl_34
|
|
40
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_1-1_63
|
|
41
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_1-2_72
|
|
42
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_1-3_80
|
|
43
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_2-1_64
|
|
44
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_2-2_73
|
|
45
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_2-3_81
|
|
46
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_3-1_65
|
|
47
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_3-2_74
|
|
48
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_3-3_82
|
|
49
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_4-1_66
|
|
50
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_4-3_83
|
|
51
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_5-1_67
|
|
52
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_5-2_75
|
|
53
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_5-3_84
|
|
54
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_6-1_68
|
|
55
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_6-2_76
|
|
56
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_6-3_85
|
|
57
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_7-1_69
|
|
58
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_7-2_77
|
|
59
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_7-3_86
|
|
60
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_8-1_70
|
|
61
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_8-2_78
|
|
62
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_8-3_87
|
|
63
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_9-1_71
|
|
64
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_9-2_79
|
|
65
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_9-3_88
|
|
66
|
+
exp114_250730_YY_HEK_DFMO_Put_MMW_DigestControl_62
|
|
67
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_1-1_06
|
|
68
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_1-1_250ng_03
|
|
69
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_1-1_500ng_04
|
|
70
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_1-1_92
|
|
71
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_1-2_101
|
|
72
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_1-2_15
|
|
73
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_1-3_110
|
|
74
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_1-3_24
|
|
75
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_2-1_07
|
|
76
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_2-1_93
|
|
77
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_2-2_102
|
|
78
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_2-2_16
|
|
79
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_2-3_111
|
|
80
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_2-3_25
|
|
81
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_3-1_08
|
|
82
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_3-1_94
|
|
83
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_3-2_103
|
|
84
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_3-2_17
|
|
85
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_3-3_112
|
|
86
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_3-3_26
|
|
87
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_4-1_09
|
|
88
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_4-1_95
|
|
89
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_4-2_104
|
|
90
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_4-2_18
|
|
91
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_4-3_113
|
|
92
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_4-3_27
|
|
93
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_5-1_10
|
|
94
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_5-1_96
|
|
95
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_5-2_105
|
|
96
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_5-2_19
|
|
97
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_5-3_114
|
|
98
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_5-3_28
|
|
99
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_6-1_11
|
|
100
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_6-1_97
|
|
101
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_6-2_106
|
|
102
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_6-2_20
|
|
103
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_6-3_115
|
|
104
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_6-3_29
|
|
105
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_7-1_12
|
|
106
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_7-1_98
|
|
107
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_7-2_107
|
|
108
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_7-2_21
|
|
109
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_7-3_116
|
|
110
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_7-3_30
|
|
111
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_8-1_13
|
|
112
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_8-1_99
|
|
113
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_8-2_108
|
|
114
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_8-2_22
|
|
115
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_8-3_117
|
|
116
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_8-3_31
|
|
117
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_9-1_100
|
|
118
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_9-1_14
|
|
119
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_9-2_109
|
|
120
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_9-2_23
|
|
121
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_9-3_118
|
|
122
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_9-3_32
|
|
123
|
+
exp114_250730_YY_HEK_DFMO_Put_tRNA_DigControl_02
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
exp214_251220_SK_RNA_blank_002
|
|
2
|
+
exp214_251220_SK_RNA_blank_002_B
|
|
3
|
+
exp214_251220_SK_RNA_blank_015
|
|
4
|
+
exp214_251220_SK_RNA_blank_029
|
|
5
|
+
exp214_251220_SK_RNA_blank_043
|
|
6
|
+
exp214_251220_SK_RNA_blank_057
|
|
7
|
+
exp214_251220_SK_RNA_NB1_AML_D6_C1_019
|
|
8
|
+
exp214_251220_SK_RNA_NB1_AML_D6_C2_061
|
|
9
|
+
exp214_251220_SK_RNA_NB1_AML_D6_C3_014
|
|
10
|
+
exp214_251220_SK_RNA_NB1_AML_D6_H1_034
|
|
11
|
+
exp214_251220_SK_RNA_NB1_AML_D6_H2_056
|
|
12
|
+
exp214_251220_SK_RNA_NB1_AML_D6_H3_038
|
|
13
|
+
exp214_251220_SK_RNA_NB1_AML_D6_L1_036
|
|
14
|
+
exp214_251220_SK_RNA_NB1_AML_D6_L2_005
|
|
15
|
+
exp214_251220_SK_RNA_NB1_AML_D6_L3_050
|
|
16
|
+
exp214_251220_SK_RNA_NB1_D1_A1_053
|
|
17
|
+
exp214_251220_SK_RNA_NB1_D1_A2_052
|
|
18
|
+
exp214_251220_SK_RNA_NB1_D1_A3_054
|
|
19
|
+
exp214_251220_SK_RNA_NB1_D1_AS1_011
|
|
20
|
+
exp214_251220_SK_RNA_NB1_D1_AS2_007
|
|
21
|
+
exp214_251220_SK_RNA_NB1_D1_AS3_033
|
|
22
|
+
exp214_251220_SK_RNA_NB1_D1_C1_030
|
|
23
|
+
exp214_251220_SK_RNA_NB1_D1_C2_055
|
|
24
|
+
exp214_251220_SK_RNA_NB1_D1_C3_031
|
|
25
|
+
exp214_251220_SK_RNA_NB1_D1_S1_003
|
|
26
|
+
exp214_251220_SK_RNA_NB1_D1_S2_044
|
|
27
|
+
exp214_251220_SK_RNA_NB1_D1_S3_035
|
|
28
|
+
exp214_251220_SK_RNA_NB1_D3_A1_021
|
|
29
|
+
exp214_251220_SK_RNA_NB1_D3_A2_028
|
|
30
|
+
exp214_251220_SK_RNA_NB1_D3_A3_013
|
|
31
|
+
exp214_251220_SK_RNA_NB1_D3_AS1_027
|
|
32
|
+
exp214_251220_SK_RNA_NB1_D3_AS2_049
|
|
33
|
+
exp214_251220_SK_RNA_NB1_D3_AS3_025
|
|
34
|
+
exp214_251220_SK_RNA_NB1_D3_C1_045
|
|
35
|
+
exp214_251220_SK_RNA_NB1_D3_C2_023
|
|
36
|
+
exp214_251220_SK_RNA_NB1_D3_C3_059
|
|
37
|
+
exp214_251220_SK_RNA_NB1_D3_S1_032
|
|
38
|
+
exp214_251220_SK_RNA_NB1_D3_S2_008
|
|
39
|
+
exp214_251220_SK_RNA_NB1_D3_S3_016
|
|
40
|
+
exp214_251220_SK_RNA_NB1_D6_A1_037
|
|
41
|
+
exp214_251220_SK_RNA_NB1_D6_A2_017
|
|
42
|
+
exp214_251220_SK_RNA_NB1_D6_A3_012
|
|
43
|
+
exp214_251220_SK_RNA_NB1_D6_AS1_024
|
|
44
|
+
exp214_251220_SK_RNA_NB1_D6_AS2_060
|
|
45
|
+
exp214_251220_SK_RNA_NB1_D6_AS3_048
|
|
46
|
+
exp214_251220_SK_RNA_NB1_D6_AzaC1_004
|
|
47
|
+
exp214_251220_SK_RNA_NB1_D6_AzaC2_010
|
|
48
|
+
exp214_251220_SK_RNA_NB1_D6_AzaC3_051
|
|
49
|
+
exp214_251220_SK_RNA_NB1_D6_C1_039
|
|
50
|
+
exp214_251220_SK_RNA_NB1_D6_C2_022
|
|
51
|
+
exp214_251220_SK_RNA_NB1_D6_C3_046
|
|
52
|
+
exp214_251220_SK_RNA_NB1_D6_Ona1_058
|
|
53
|
+
exp214_251220_SK_RNA_NB1_D6_Ona2_020
|
|
54
|
+
exp214_251220_SK_RNA_NB1_D6_Ona3_042
|
|
55
|
+
exp214_251220_SK_RNA_NB1_D6_S1_041
|
|
56
|
+
exp214_251220_SK_RNA_NB1_D6_S2_026
|
|
57
|
+
exp214_251220_SK_RNA_NB1_D6_S3_006
|
|
58
|
+
exp214_251220_SK_RNA_NB1_D6_STC1_018
|
|
59
|
+
exp214_251220_SK_RNA_NB1_D6_STC2_009
|
|
60
|
+
exp214_251220_SK_RNA_NB1_D6_STC3_040
|
|
61
|
+
exp214_251220_SK_RNA_NB1_DigControl_047
|
|
62
|
+
exp214_251220_SK_RNA_NucleosideMix_10nM_001
|
|
63
|
+
exp214_251220_SK_RNA_NucleosideMix_10nM_062
|
|
@@ -39,6 +39,21 @@ describe('permission helpers', () => {
|
|
|
39
39
|
expect(canAccessPlugin(member, 'reports')).toBe(false)
|
|
40
40
|
})
|
|
41
41
|
|
|
42
|
+
it.each(['none', '', {}, { dose: true }, 42])(
|
|
43
|
+
'should deny every plugin when plugin_access is malformed (%j)',
|
|
44
|
+
(access) => {
|
|
45
|
+
const user = { role: 'member', role_obj: { slug: 'member', plugin_access: access as never } }
|
|
46
|
+
|
|
47
|
+
expect(canAccessPlugin(user, 'dose')).toBe(false)
|
|
48
|
+
},
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
it('should allow every plugin when the user is an administrator', () => {
|
|
52
|
+
const admin = { role: 'admin', role_obj: { slug: 'admin', plugin_access: ['dose'] } }
|
|
53
|
+
|
|
54
|
+
expect(canAccessPlugin(admin, 'reports')).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
42
57
|
it('allows notice publishers to enter the admin panel', () => {
|
|
43
58
|
const publisher = {
|
|
44
59
|
role: 'notice-publisher',
|
|
@@ -4,17 +4,85 @@
|
|
|
4
4
|
* `AutoGroupResult`. Framework-free so the mapping is unit-testable in isolation
|
|
5
5
|
* from `AutoGroupModal.vue`.
|
|
6
6
|
*/
|
|
7
|
-
import type { AutoGroupResult, ClassSchema, ColumnInfo, ColumnRole } from '../types/auto-group'
|
|
7
|
+
import type { AutoGroupResult, ClassSchema, ColumnInfo, ColumnRole, SampleClass } 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'
|
|
11
|
-
import type { SmartGroupSeed } from './SmartGroup.types'
|
|
11
|
+
import type { FieldRecipeAnatomyToken, FieldRecipeQcChip, SmartGroupSeed } from './SmartGroup.types'
|
|
12
|
+
import { GROUPING_FIELD_ROLES, classKey, isExperimentalKind } from '../composables/autoGroup'
|
|
13
|
+
import type { ExperimentHeader } from '../composables/autoGroup'
|
|
14
|
+
|
|
15
|
+
const PREVIEW_VALUES = 3
|
|
16
|
+
|
|
17
|
+
/** Value preview of a field card: the first three values, then the count of the rest. */
|
|
18
|
+
export function previewValues(values: string[]): string {
|
|
19
|
+
const shown = values.slice(0, PREVIEW_VALUES).join(', ')
|
|
20
|
+
return values.length > PREVIEW_VALUES ? `${shown} … +${values.length - PREVIEW_VALUES}` : shown
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Experiment line of the modal header: `exp157 · 2025-10-13 · LT`. */
|
|
24
|
+
export function experimentLabel(header: ExperimentHeader): string {
|
|
25
|
+
return [header.code, header.date, header.operator].filter(Boolean).join(' · ')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Names of all columns of the experimental classes. A field cannot take one
|
|
30
|
+
* of them: the engine merges fields by name.
|
|
31
|
+
*/
|
|
32
|
+
export function engineColumnNames(schemas: Record<string, ClassSchema>): string[] {
|
|
33
|
+
const names = Object.values(schemas)
|
|
34
|
+
.filter(schema => isExperimentalKind(schema.classKind))
|
|
35
|
+
.flatMap(schema => schema.columns.map(column => column.displayName ?? column.name))
|
|
36
|
+
return [...new Set(names)]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** One chip for each QC-type class, with its sample count. */
|
|
40
|
+
export function engineQcChips(classes: SampleClass[]): FieldRecipeQcChip[] {
|
|
41
|
+
return classes
|
|
42
|
+
.filter(c => !isExperimentalKind(c.kind))
|
|
43
|
+
.map(c => ({ name: c.label, count: c.members.length }))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The tokens of one study sample with the role the engine gave each of them,
|
|
48
|
+
* so the user sees how the names were read. The sample is the first member of
|
|
49
|
+
* the largest experimental class. `cards` supplies the on/off state and the
|
|
50
|
+
* layer of the tokens that are grouping fields.
|
|
51
|
+
*/
|
|
52
|
+
export function engineAnatomy(
|
|
53
|
+
classes: SampleClass[],
|
|
54
|
+
schemas: Record<string, ClassSchema>,
|
|
55
|
+
tokenized: string[][],
|
|
56
|
+
cards: { fields: SmartGroupField[]; layers: (number | null)[] },
|
|
57
|
+
): FieldRecipeAnatomyToken[] {
|
|
58
|
+
const cls = classes
|
|
59
|
+
.filter(c => isExperimentalKind(c.kind) && schemas[classKey(c)])
|
|
60
|
+
.sort((a, b) => b.members.length - a.members.length)[0]
|
|
61
|
+
if (!cls) return []
|
|
62
|
+
const tokens = tokenized[cls.members[0]] ?? []
|
|
63
|
+
return schemas[classKey(cls)].columns.flatMap((column): FieldRecipeAnatomyToken[] => {
|
|
64
|
+
const text = column.sourceIndices.map(index => tokens[index] ?? '').join('_')
|
|
65
|
+
if (!text) return []
|
|
66
|
+
const name = column.displayName ?? column.name
|
|
67
|
+
const card = cards.fields.findIndex(field => field.key === name)
|
|
68
|
+
if (card === -1) {
|
|
69
|
+
// A token that is not a field shows an inferred name only when the
|
|
70
|
+
// name says something (Date, Injection #); `Username` or `Matrix 2` on a
|
|
71
|
+
// constant token does not.
|
|
72
|
+
const caption = /^(Field \d+|Username.*|.* \d+)$/.test(name) ? roleLabel(column.role).toLowerCase() : name
|
|
73
|
+
return [{ text, caption, state: 'fixed' }]
|
|
74
|
+
}
|
|
75
|
+
const layer = cards.layers[card]
|
|
76
|
+
return [layer === null ? { text, caption: name, state: 'off' } : { text, caption: String(layer), state: 'on' }]
|
|
77
|
+
})
|
|
78
|
+
}
|
|
12
79
|
|
|
13
80
|
const ROLE_LABELS: Record<ColumnRole, string> = {
|
|
14
81
|
'constant': 'Constant',
|
|
15
82
|
'factor': 'Factor',
|
|
16
83
|
'replicate': 'Replicate',
|
|
17
84
|
'run-order': 'Run',
|
|
85
|
+
'id': 'Sample ID',
|
|
18
86
|
'numeric': 'Numeric',
|
|
19
87
|
'class-tag': 'Class tag',
|
|
20
88
|
'ignore': 'Ignore',
|
|
@@ -26,14 +94,15 @@ export function roleLabel(role?: ColumnRole): string {
|
|
|
26
94
|
}
|
|
27
95
|
|
|
28
96
|
/**
|
|
29
|
-
* Columns worth offering as grouping toggles: factors
|
|
30
|
-
*
|
|
97
|
+
* Columns worth offering as grouping toggles: factors, numerics, replicates and
|
|
98
|
+
* sample IDs (the last two are off by default), plus any column already enabled
|
|
99
|
+
* in `groupBy` (so a manual group-by choice still shows).
|
|
31
100
|
* Preserves the engine's column order. `groupBy` is the schema's ORDERED layer
|
|
32
101
|
* list — entry N is hierarchy layer N of the composed group names.
|
|
33
102
|
*/
|
|
34
103
|
export function candidateColumns(columns: ColumnInfo[], groupBy: readonly number[]): ColumnInfo[] {
|
|
35
104
|
const enabled = new Set(groupBy)
|
|
36
|
-
return columns.filter(c =>
|
|
105
|
+
return columns.filter(c => GROUPING_FIELD_ROLES.has(c.role) || enabled.has(c.index))
|
|
37
106
|
}
|
|
38
107
|
|
|
39
108
|
/**
|
|
@@ -50,8 +119,8 @@ export function engineFieldsToCards(
|
|
|
50
119
|
fields: candidates.map(c => ({
|
|
51
120
|
key: c.displayName ?? c.name,
|
|
52
121
|
role: roleLabel(c.role),
|
|
53
|
-
vals: c.uniqueValues
|
|
54
|
-
rep: c.role === 'replicate',
|
|
122
|
+
vals: previewValues(c.uniqueValues),
|
|
123
|
+
rep: c.role === 'replicate' || c.role === 'id',
|
|
55
124
|
})),
|
|
56
125
|
enabled: candidates.map(c => groupBy.includes(c.index)),
|
|
57
126
|
layers: candidates.map(c => {
|
|
@@ -81,6 +150,8 @@ export function engineSchemasToCards(
|
|
|
81
150
|
const enabled = new Set<string>()
|
|
82
151
|
|
|
83
152
|
for (const [, schema] of ordered) {
|
|
153
|
+
// An overlay class (QC, blank, standard) is one group; its columns are not fields.
|
|
154
|
+
if (!isExperimentalKind(schema.classKind)) continue
|
|
84
155
|
for (const index of schema.groupBy) {
|
|
85
156
|
const column = schema.columns.find(candidate => candidate.index === index)
|
|
86
157
|
if (!column) continue
|
|
@@ -101,8 +172,8 @@ export function engineSchemasToCards(
|
|
|
101
172
|
fields: cards.map(([key, column]) => ({
|
|
102
173
|
key,
|
|
103
174
|
role: roleLabel(column.role),
|
|
104
|
-
vals: column.uniqueValues
|
|
105
|
-
rep: column.role === 'replicate',
|
|
175
|
+
vals: previewValues(column.uniqueValues),
|
|
176
|
+
rep: column.role === 'replicate' || column.role === 'id',
|
|
106
177
|
})),
|
|
107
178
|
enabled: cards.map(([key]) => enabled.has(key)),
|
|
108
179
|
layers: cards.map(([key]) => {
|
|
@@ -17,15 +17,19 @@ import { useAutoGroup, parseCSV } from '../composables/useAutoGroup'
|
|
|
17
17
|
import { useApi } from '../composables/useApi'
|
|
18
18
|
import { readFileAsText } from '../composables/useFileImport'
|
|
19
19
|
import {
|
|
20
|
+
engineAnatomy,
|
|
21
|
+
engineColumnNames,
|
|
20
22
|
engineGroupsToRail,
|
|
23
|
+
engineQcChips,
|
|
21
24
|
engineSchemasToCards,
|
|
22
25
|
engineToQcRoute,
|
|
26
|
+
experimentLabel,
|
|
23
27
|
manualSamplesToResult,
|
|
24
28
|
qcRouteToEngine,
|
|
25
29
|
seedFromGroups,
|
|
26
30
|
} from './AutoGroupModal.adapter'
|
|
27
31
|
import { SAMPLE_GROUP_COLOR_OPTIONS } from './SampleSelector.colors'
|
|
28
|
-
import type
|
|
32
|
+
import { mostlySingletons, type QcRoute } from './SmartGroupFieldRecipe.groups'
|
|
29
33
|
import type { ManualSample } from './SmartGroupManual.cohorts'
|
|
30
34
|
import type { ManualPaletteEntry } from './SmartGroup.types'
|
|
31
35
|
import type { AutoGroupResult } from '../types/auto-group'
|
|
@@ -95,13 +99,32 @@ const qcSampleCount = computed(() =>
|
|
|
95
99
|
.filter(c => c.kind !== 'biological' && c.kind !== 'unknown')
|
|
96
100
|
.reduce((acc, c) => acc + c.members.length, 0),
|
|
97
101
|
)
|
|
98
|
-
const
|
|
99
|
-
qcRouting.value !== 'mix' && qcSampleCount.value > 0
|
|
100
|
-
? { name: 'QC_Pool', count: qcSampleCount.value }
|
|
101
|
-
: null,
|
|
102
|
-
)
|
|
102
|
+
const qcChips = computed(() => (qcRouting.value === 'mix' ? [] : engineQcChips(autoGroup.classes.value)))
|
|
103
103
|
const fieldRecipeRoute = computed<QcRoute>(() => engineToQcRoute(qcRouting.value))
|
|
104
104
|
const headerCount = computed(() => autoGroup.samples.value.length)
|
|
105
|
+
const headerLabel = computed(() => experimentLabel(autoGroup.experiment.value))
|
|
106
|
+
const reservedNames = computed(() => engineColumnNames(autoGroup.schemas.value))
|
|
107
|
+
// The example name is shown for pasted names only: CSV and experiment input
|
|
108
|
+
// have columns, not name tokens.
|
|
109
|
+
const anatomy = computed(() =>
|
|
110
|
+
autoGroup.inputMode.value === 'paste'
|
|
111
|
+
? engineAnatomy(autoGroup.classes.value, autoGroup.schemas.value, autoGroup.tokenized.value, fieldCards.value)
|
|
112
|
+
: [],
|
|
113
|
+
)
|
|
114
|
+
// A design with no replicate in the names gives groups of one sample. Each
|
|
115
|
+
// field that is on then shows how many groups remain when the user switches
|
|
116
|
+
// it off, so the user can find the field that is not part of the condition.
|
|
117
|
+
// The rule is the one of the summary callout, so the two show together.
|
|
118
|
+
const fieldHints = computed(() => {
|
|
119
|
+
const needsHelp = mostlySingletons(railGroups.value)
|
|
120
|
+
// ponytail: one full composeGroups for each field that is on (a few ms for
|
|
121
|
+
// 200 samples). Count distinct group keys instead if a large batch is slow.
|
|
122
|
+
return fieldCards.value.fields.map((field, index) => {
|
|
123
|
+
if (!needsHelp || !fieldCards.value.enabled[index]) return null
|
|
124
|
+
const remaining = autoGroup.groupCountWithout(field.key)
|
|
125
|
+
return `off → ${remaining} ${remaining === 1 ? 'group' : 'groups'}`
|
|
126
|
+
})
|
|
127
|
+
})
|
|
105
128
|
|
|
106
129
|
// The cards describe the batch, not one class, so edits fan out to every class
|
|
107
130
|
// exposing the field — otherwise a multi-class batch (e.g. a Liver class and a
|
|
@@ -116,6 +139,11 @@ function onMoveField(cardIndex: number, direction: 'up' | 'down') {
|
|
|
116
139
|
if (!field) return
|
|
117
140
|
autoGroup.moveGroupByField(field.key, direction)
|
|
118
141
|
}
|
|
142
|
+
function onRenameField(cardIndex: number, name: string) {
|
|
143
|
+
const field = fieldCards.value.fields[cardIndex]
|
|
144
|
+
if (!field) return
|
|
145
|
+
autoGroup.renameGroupByField(field.key, name)
|
|
146
|
+
}
|
|
119
147
|
function onRouteChange(route: QcRoute) {
|
|
120
148
|
setQcRouting(qcRouteToEngine(route))
|
|
121
149
|
}
|
|
@@ -257,15 +285,20 @@ watch(
|
|
|
257
285
|
:enabled="fieldCards.enabled"
|
|
258
286
|
:layers="fieldCards.layers"
|
|
259
287
|
:groups="railGroups"
|
|
260
|
-
:qc-
|
|
288
|
+
:qc-chips="qcChips"
|
|
261
289
|
:route="fieldRecipeRoute"
|
|
262
290
|
:header-count="headerCount"
|
|
263
291
|
:qc-count="qcSampleCount"
|
|
264
292
|
:can-download-template="autoGroup.canDownloadTemplate.value"
|
|
265
293
|
:loading="experimentLoading"
|
|
266
|
-
experiment-label=""
|
|
294
|
+
:experiment-label="headerLabel"
|
|
295
|
+
:anatomy="anatomy"
|
|
296
|
+
:hints="fieldHints"
|
|
297
|
+
:reserved-names="reservedNames"
|
|
298
|
+
renamable
|
|
267
299
|
@toggle="onToggleField"
|
|
268
300
|
@move="onMoveField"
|
|
301
|
+
@rename="onRenameField"
|
|
269
302
|
@update:route="onRouteChange"
|
|
270
303
|
@csv-file="onCsvFile"
|
|
271
304
|
@download-template="onDownloadTemplate"
|
|
@@ -92,6 +92,8 @@ interface Props {
|
|
|
92
92
|
/** No editing controls; well clicks still emit `well-click`. */
|
|
93
93
|
readonly?: boolean
|
|
94
94
|
size?: WellPlateSize
|
|
95
|
+
/** Footer text while nothing is selected; replaces the built-in selection hint. */
|
|
96
|
+
hint?: string
|
|
95
97
|
}
|
|
96
98
|
|
|
97
99
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -446,6 +448,7 @@ function assign(groupId: string | undefined) {
|
|
|
446
448
|
|
|
447
449
|
const hint = computed(() => {
|
|
448
450
|
if (dragMode.value) return 'Drag a filled well onto another to move or swap it.'
|
|
451
|
+
if (props.hint) return props.hint
|
|
449
452
|
if (!props.groups) return ''
|
|
450
453
|
if (railOn.value) {
|
|
451
454
|
return inspectorOn.value
|
|
@@ -35,6 +35,16 @@ export interface FieldRecipeQcChip {
|
|
|
35
35
|
count: number
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** One token of the example sample name, with the role it was read as. */
|
|
39
|
+
export interface FieldRecipeAnatomyToken {
|
|
40
|
+
/** The token as written in the sample name. */
|
|
41
|
+
text: string
|
|
42
|
+
/** The layer number for a grouped field; the field name or role for any other token. */
|
|
43
|
+
caption: string
|
|
44
|
+
/** `on`: a grouped field. `off`: a field that is switched off. `fixed`: not a grouping field. */
|
|
45
|
+
state: 'on' | 'off' | 'fixed'
|
|
46
|
+
}
|
|
47
|
+
|
|
38
48
|
export interface SmartGroupFieldRecipeProps {
|
|
39
49
|
/** Sample records behind the experiment (one row per real sample). */
|
|
40
50
|
samples?: SmartGroupSampleRecord[]
|
|
@@ -75,6 +85,19 @@ export interface SmartGroupFieldRecipeProps {
|
|
|
75
85
|
canDownloadTemplate?: boolean
|
|
76
86
|
/** Show a loading state in the main body (e.g. while experiment data loads). */
|
|
77
87
|
loading?: boolean
|
|
88
|
+
/** Tokens of one example sample name; shown above the fields when not empty. */
|
|
89
|
+
anatomy?: FieldRecipeAnatomyToken[]
|
|
90
|
+
/**
|
|
91
|
+
* Per-card hint for a field that is on, for example the groups that remain
|
|
92
|
+
* when the user switches it off. `null` shows no hint for that card.
|
|
93
|
+
*/
|
|
94
|
+
hints?: (string | null)[]
|
|
95
|
+
/** One chip for each QC class; when set it replaces the single `qcChip`. */
|
|
96
|
+
qcChips?: FieldRecipeQcChip[]
|
|
97
|
+
/** Show the rename control on each field; a new name emits `rename`. */
|
|
98
|
+
renamable?: boolean
|
|
99
|
+
/** Names that a rename must refuse, in addition to the names of the other fields. */
|
|
100
|
+
reservedNames?: string[]
|
|
78
101
|
}
|
|
79
102
|
|
|
80
103
|
export interface FieldRecipeApplyPayload {
|
|
@@ -34,8 +34,11 @@ export const QC_POOL_NAME = 'QC_Pool'
|
|
|
34
34
|
/** How QC samples are routed relative to the biological groups. */
|
|
35
35
|
export type QcRoute = 'Overlay only' | 'Exclude' | 'Mix into groups'
|
|
36
36
|
|
|
37
|
-
/**
|
|
38
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Kind of inline summary callout to render. `warn`: a replicate field is on.
|
|
39
|
+
* `singletons`: no replicate field is on, but half or more of the groups have one sample.
|
|
40
|
+
*/
|
|
41
|
+
export type FieldRecipeSummaryKind = 'mute' | 'warn' | 'singletons' | 'info'
|
|
39
42
|
|
|
40
43
|
export interface FieldRecipeSummary {
|
|
41
44
|
kind: FieldRecipeSummaryKind
|
|
@@ -99,6 +102,14 @@ export function sampleTotalFor(route: QcRoute, realCount = 9, qcCount = 3): numb
|
|
|
99
102
|
return route === 'Exclude' ? realCount : realCount + qcCount
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
/**
|
|
106
|
+
* True when half or more of the groups have one sample: the design has no
|
|
107
|
+
* replicate in the names, or a field that is on is not part of the condition.
|
|
108
|
+
*/
|
|
109
|
+
export function mostlySingletons(groups: FieldRecipeGroup[]): boolean {
|
|
110
|
+
return groups.length > 1 && groups.filter(group => group.count === 1).length * 2 >= groups.length
|
|
111
|
+
}
|
|
112
|
+
|
|
102
113
|
/** Decide which inline summary callout the Field Recipe should show. */
|
|
103
114
|
export function fieldRecipeSummary(input: {
|
|
104
115
|
activeFactors: string[]
|
|
@@ -108,5 +119,6 @@ export function fieldRecipeSummary(input: {
|
|
|
108
119
|
const groupCount = input.baseGroups.length
|
|
109
120
|
if (input.activeFactors.length === 0) return { kind: 'mute', groupCount }
|
|
110
121
|
if (input.repOn) return { kind: 'warn', groupCount }
|
|
122
|
+
if (mostlySingletons(input.baseGroups)) return { kind: 'singletons', groupCount }
|
|
111
123
|
return { kind: 'info', groupCount }
|
|
112
124
|
}
|