@morscherlab/mint-sdk 1.0.15 → 1.0.17
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/{BaseSelect-DksaKYq_.js → BaseSelect-ekgr9fDo.js} +4 -1
- package/dist/BaseSelect-ekgr9fDo.js.map +1 -0
- package/dist/{ExperimentSelectorModal-DIFyL5ta.js → ExperimentSelectorModal-BOzDs8TU.js} +2 -2
- package/dist/{ExperimentSelectorModal-CHsU-LIh.js → ExperimentSelectorModal-CX0oBzpV.js} +2 -2
- package/dist/{ExperimentSelectorModal-CHsU-LIh.js.map → ExperimentSelectorModal-CX0oBzpV.js.map} +1 -1
- package/dist/{SettingsModal-LEKI6Ebl.js → SettingsModal-BTyXD0uP.js} +3 -3
- package/dist/{SettingsModal-LEKI6Ebl.js.map → SettingsModal-BTyXD0uP.js.map} +1 -1
- package/dist/SettingsModal-DXcSKk9D.js +5 -0
- package/dist/__tests__/components/MobileSupportGate.test.d.ts +1 -0
- package/dist/__tests__/composables/useMobileSupportGate.test.d.ts +1 -0
- package/dist/components/AutoGroupModal.vue.d.ts +6 -0
- package/dist/components/BaseInput.vue.d.ts +1 -0
- package/dist/components/MobileSupportGate.vue.d.ts +40 -0
- package/dist/components/SampleSelector.colors.d.ts +2 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +6 -6
- package/dist/{components-Cyk8QEyL.js → components-CzdeV1xe.js} +1122 -535
- package/dist/components-CzdeV1xe.js.map +1 -0
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.js +7 -7
- package/dist/composables/useMobileSupportGate.d.ts +14 -0
- package/dist/{composables-D9mexHSW.js → composables-Da-4XOe2.js} +3 -3
- package/dist/{composables-D9mexHSW.js.map → composables-Da-4XOe2.js.map} +1 -1
- package/dist/index.js +10 -10
- package/dist/install.js +5 -5
- package/dist/styles.css +4004 -2537
- package/dist/templates/index.js +3 -3
- package/dist/{templates-Do43ZIMb.js → templates-Dnf8UNxg.js} +2 -2
- package/dist/{templates-Do43ZIMb.js.map → templates-Dnf8UNxg.js.map} +1 -1
- package/dist/{useControlSchema-0n8Bcftq.js → useControlSchema-Dkm-W_lg.js} +2 -2
- package/dist/{useControlSchema-0n8Bcftq.js.map → useControlSchema-Dkm-W_lg.js.map} +1 -1
- package/dist/{useFormBuilder-COfYWDuC.js → useFormBuilder-BOJ52N4M.js} +2 -2
- package/dist/{useFormBuilder-COfYWDuC.js.map → useFormBuilder-BOJ52N4M.js.map} +1 -1
- package/dist/{useProtocolTemplates-DODHlhxr.js → useProtocolTemplates-r2GOnnH1.js} +55 -5
- package/dist/useProtocolTemplates-r2GOnnH1.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/MobileSupportGate.test.ts +120 -0
- package/src/__tests__/components/SampleSelector.test.ts +119 -0
- package/src/__tests__/composables/useMobileSupportGate.test.ts +74 -0
- package/src/components/AutoGroupModal.story.vue +46 -0
- package/src/components/AutoGroupModal.vue +578 -2
- package/src/components/BaseInput.vue +2 -0
- package/src/components/MobileSupportGate.story.vue +52 -0
- package/src/components/MobileSupportGate.vue +115 -0
- package/src/components/SampleSelector.colors.ts +7 -2
- package/src/components/SampleSelector.story.vue +45 -1
- package/src/components/SampleSelector.vue +32 -6
- package/src/components/index.ts +1 -0
- package/src/composables/index.ts +8 -0
- package/src/composables/useMobileSupportGate.ts +80 -0
- package/src/styles/components/auto-group-modal.css +758 -0
- package/src/styles/components/mobile-support-gate.css +119 -0
- package/src/styles/components/sample-selector.css +23 -9
- package/dist/BaseSelect-DksaKYq_.js.map +0 -1
- package/dist/SettingsModal-L7Ejny45.js +0 -5
- package/dist/components-Cyk8QEyL.js.map +0 -1
- package/dist/useProtocolTemplates-DODHlhxr.js.map +0 -1
|
@@ -10,12 +10,23 @@ import AlertBox from './AlertBox.vue'
|
|
|
10
10
|
import { useAutoGroup, parseCSV } from '../composables/useAutoGroup'
|
|
11
11
|
import { classKey } from '../composables/autoGroup'
|
|
12
12
|
import { useApi } from '../composables/useApi'
|
|
13
|
+
import { useTextSearch } from '../composables/useTextSearch'
|
|
14
|
+
import { useSampleGroups } from '../composables/useSampleGroups'
|
|
15
|
+
import {
|
|
16
|
+
createSampleGroup,
|
|
17
|
+
SAMPLE_GROUP_COLOR_OPTIONS,
|
|
18
|
+
} from './SampleSelector.colors'
|
|
13
19
|
import type { WizardStep } from '../types/components'
|
|
14
20
|
import type { AutoGroupResult, MergeSuggestion, ColumnRole } from '../types/auto-group'
|
|
21
|
+
import type { SampleGroup } from '../types'
|
|
22
|
+
|
|
23
|
+
type GroupingWorkflow = 'auto' | 'manual'
|
|
15
24
|
|
|
16
25
|
export interface Props {
|
|
17
26
|
modelValue: boolean
|
|
18
27
|
samples?: string[]
|
|
28
|
+
groups?: SampleGroup[]
|
|
29
|
+
initialMode?: GroupingWorkflow
|
|
19
30
|
experimentId?: number
|
|
20
31
|
/** Pre-fetched design data — bypasses API fetch when provided */
|
|
21
32
|
designData?: Record<string, unknown>
|
|
@@ -23,6 +34,8 @@ export interface Props {
|
|
|
23
34
|
|
|
24
35
|
const props = withDefaults(defineProps<Props>(), {
|
|
25
36
|
samples: () => [],
|
|
37
|
+
groups: () => [],
|
|
38
|
+
initialMode: 'auto',
|
|
26
39
|
experimentId: undefined,
|
|
27
40
|
designData: undefined,
|
|
28
41
|
})
|
|
@@ -41,6 +54,15 @@ const csvFileName = ref('')
|
|
|
41
54
|
const experimentLoading = ref(false)
|
|
42
55
|
const experimentError = ref<string | null>(null)
|
|
43
56
|
const csvError = ref<string | null>(null)
|
|
57
|
+
const activeWorkflow = ref<GroupingWorkflow>('auto')
|
|
58
|
+
const manualDraftGroups = ref<SampleGroup[]>([])
|
|
59
|
+
const manualSearchQuery = ref('')
|
|
60
|
+
const manualUngroupedOnly = ref(true)
|
|
61
|
+
const manualSelectedSamples = ref<string[]>([])
|
|
62
|
+
const manualGroupName = ref('')
|
|
63
|
+
const manualSubGroupName = ref('')
|
|
64
|
+
const manualColorOptions = SAMPLE_GROUP_COLOR_OPTIONS
|
|
65
|
+
const manualSelectedColor = ref(manualColorOptions[0])
|
|
44
66
|
|
|
45
67
|
// Workspace step refs
|
|
46
68
|
const openPopoverIdx = ref<number | null>(null)
|
|
@@ -91,6 +113,194 @@ const totalQc = computed(() =>
|
|
|
91
113
|
(autoGroup.qcGroups.value ?? []).reduce((acc, g) => acc + g.samples.length, 0),
|
|
92
114
|
)
|
|
93
115
|
|
|
116
|
+
function cloneGroups(groups: SampleGroup[]): SampleGroup[] {
|
|
117
|
+
return groups.map(group => ({
|
|
118
|
+
...group,
|
|
119
|
+
samples: [...group.samples],
|
|
120
|
+
}))
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function manualResetSelection() {
|
|
124
|
+
manualSearchQuery.value = ''
|
|
125
|
+
manualUngroupedOnly.value = true
|
|
126
|
+
manualSelectedSamples.value = []
|
|
127
|
+
manualGroupName.value = ''
|
|
128
|
+
manualSubGroupName.value = ''
|
|
129
|
+
manualSelectedColor.value = manualColorOptions[0]
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function manualToggleSample(sample: string) {
|
|
133
|
+
if (manualSelectedSet.value.has(sample)) {
|
|
134
|
+
manualSelectedSamples.value = manualSelectedSamples.value.filter(item => item !== sample)
|
|
135
|
+
} else {
|
|
136
|
+
manualSelectedSamples.value = [...manualSelectedSamples.value, sample]
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function manualSelectVisibleSamples() {
|
|
141
|
+
const next = new Set(manualSelectedSamples.value)
|
|
142
|
+
for (const sample of manualFilteredSamples.value) {
|
|
143
|
+
next.add(sample)
|
|
144
|
+
}
|
|
145
|
+
manualSelectedSamples.value = [...next]
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function manualClearSamples() {
|
|
149
|
+
manualSelectedSamples.value = []
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function manualToggleVisibleSamples() {
|
|
153
|
+
if (manualAllVisibleSelected.value) {
|
|
154
|
+
manualSelectedSamples.value = manualSelectedSamples.value
|
|
155
|
+
.filter(sample => !manualVisibleSet.value.has(sample))
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
manualSelectVisibleSamples()
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function manualGetSampleGroup(sample: string): SampleGroup | undefined {
|
|
163
|
+
return manualDraftGroups.value.find(group => group.samples.includes(sample))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function manualGetSampleStatus(sample: string): string {
|
|
167
|
+
return manualGetSampleGroup(sample)?.name.replace('/', ' · ') || 'unassigned'
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function manualGetGroupPrimaryName(name: string): string {
|
|
171
|
+
return name.split('/')[0]
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function manualGetGroupSecondaryName(name: string): string | null {
|
|
175
|
+
const separatorIndex = name.indexOf('/')
|
|
176
|
+
return separatorIndex === -1 ? null : name.slice(separatorIndex + 1)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function manualChooseColor(color: string) {
|
|
180
|
+
manualSelectedColor.value = color
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function manualUseExistingGroupAsTarget(name: string) {
|
|
184
|
+
const existingGroup = manualDraftGroups.value.find(group => group.name === name)
|
|
185
|
+
if (existingGroup) {
|
|
186
|
+
manualSelectedColor.value = existingGroup.color
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const separatorIndex = name.indexOf('/')
|
|
190
|
+
if (separatorIndex === -1) {
|
|
191
|
+
manualGroupName.value = name
|
|
192
|
+
manualSubGroupName.value = ''
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
manualGroupName.value = name.slice(0, separatorIndex)
|
|
197
|
+
manualSubGroupName.value = name.slice(separatorIndex + 1)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function manualAssignSamplesToGroup() {
|
|
201
|
+
const target = manualTargetGroupName.value
|
|
202
|
+
if (!manualCanAssign.value || !target) return
|
|
203
|
+
|
|
204
|
+
const selected = manualAssignableSamples.value
|
|
205
|
+
const selectedSet = new Set(selected)
|
|
206
|
+
if (selectedSet.size === 0) return
|
|
207
|
+
|
|
208
|
+
let nextGroups = manualDraftGroups.value.map(group => ({
|
|
209
|
+
...group,
|
|
210
|
+
samples: group.samples.filter(sample => !selectedSet.has(sample)),
|
|
211
|
+
}))
|
|
212
|
+
|
|
213
|
+
const targetIndex = nextGroups.findIndex(group => group.name === target)
|
|
214
|
+
if (targetIndex === -1) {
|
|
215
|
+
const newGroup = createSampleGroup(target, nextGroups, manualSelectedColor.value)
|
|
216
|
+
if (!newGroup) return
|
|
217
|
+
nextGroups = [...nextGroups, { ...newGroup, samples: selected }]
|
|
218
|
+
} else {
|
|
219
|
+
const group = nextGroups[targetIndex]
|
|
220
|
+
nextGroups[targetIndex] = {
|
|
221
|
+
...group,
|
|
222
|
+
color: manualSelectedColor.value,
|
|
223
|
+
samples: [...group.samples, ...selected.filter(sample => !group.samples.includes(sample))],
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
manualDraftGroups.value = nextGroups
|
|
228
|
+
manualSelectedSamples.value = []
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const manualSamples = computed(() =>
|
|
232
|
+
props.samples.length > 0 ? props.samples : autoGroup.samples.value,
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
const manualGroupsModel = computed({
|
|
236
|
+
get: () => manualDraftGroups.value,
|
|
237
|
+
set: (value: SampleGroup[]) => {
|
|
238
|
+
manualDraftGroups.value = value
|
|
239
|
+
},
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
const manualSampleGroups = useSampleGroups({
|
|
243
|
+
samples: () => manualSamples.value,
|
|
244
|
+
groups: manualGroupsModel,
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
const manualHierarchicalGroups = manualSampleGroups.hierarchicalGroups
|
|
248
|
+
const manualShowHierarchy = manualSampleGroups.showHierarchy
|
|
249
|
+
const manualUngroupedSamples = manualSampleGroups.ungroupedSamples
|
|
250
|
+
const manualSamplePool = computed(() =>
|
|
251
|
+
manualUngroupedOnly.value ? manualUngroupedSamples.value : manualSamples.value,
|
|
252
|
+
)
|
|
253
|
+
const manualSampleSearch = useTextSearch({
|
|
254
|
+
items: () => manualSamplePool.value,
|
|
255
|
+
query: manualSearchQuery,
|
|
256
|
+
getText: sample => sample,
|
|
257
|
+
})
|
|
258
|
+
const manualFilteredSamples = manualSampleSearch.filteredItems
|
|
259
|
+
const manualSelectedSet = computed(() => new Set(manualSelectedSamples.value))
|
|
260
|
+
const manualVisibleSet = computed(() => new Set(manualFilteredSamples.value))
|
|
261
|
+
const manualAllVisibleSelected = computed(() =>
|
|
262
|
+
manualFilteredSamples.value.length > 0
|
|
263
|
+
&& manualFilteredSamples.value.every(sample => manualSelectedSet.value.has(sample)),
|
|
264
|
+
)
|
|
265
|
+
const manualSomeVisibleSelected = computed(() =>
|
|
266
|
+
manualFilteredSamples.value.some(sample => manualSelectedSet.value.has(sample))
|
|
267
|
+
&& !manualAllVisibleSelected.value,
|
|
268
|
+
)
|
|
269
|
+
const manualAssignableSamples = computed(() =>
|
|
270
|
+
manualSelectedSamples.value.filter(sample => manualSamples.value.includes(sample)),
|
|
271
|
+
)
|
|
272
|
+
const manualTargetGroupName = computed(() => {
|
|
273
|
+
const major = manualGroupName.value.trim()
|
|
274
|
+
const sub = manualSubGroupName.value.trim()
|
|
275
|
+
if (!major) return ''
|
|
276
|
+
return sub ? `${major}/${sub}` : major
|
|
277
|
+
})
|
|
278
|
+
const manualCanAssign = computed(() =>
|
|
279
|
+
manualAssignableSamples.value.length > 0 && manualTargetGroupName.value.length > 0,
|
|
280
|
+
)
|
|
281
|
+
const manualGroupedSamples = computed(() => {
|
|
282
|
+
const validSamples = new Set(manualSamples.value)
|
|
283
|
+
return new Set(
|
|
284
|
+
manualDraftGroups.value
|
|
285
|
+
.flatMap(group => group.samples)
|
|
286
|
+
.filter(sample => validSamples.has(sample)),
|
|
287
|
+
)
|
|
288
|
+
})
|
|
289
|
+
const manualGroupedCount = computed(() => manualGroupedSamples.value.size)
|
|
290
|
+
const manualUngroupedCount = computed(() =>
|
|
291
|
+
Math.max(manualSamples.value.length - manualGroupedCount.value, 0),
|
|
292
|
+
)
|
|
293
|
+
const manualResult = computed<AutoGroupResult>(() => {
|
|
294
|
+
const groups = cloneGroups(manualDraftGroups.value)
|
|
295
|
+
return {
|
|
296
|
+
groups,
|
|
297
|
+
experimentalGroups: groups,
|
|
298
|
+
qcGroups: [],
|
|
299
|
+
metadata: [],
|
|
300
|
+
excludedSamples: [],
|
|
301
|
+
}
|
|
302
|
+
})
|
|
303
|
+
|
|
94
304
|
function acceptSuggestion(s: MergeSuggestion) {
|
|
95
305
|
autoGroup.mergeColumns(autoGroup.activeClassKey.value, s.columnIndices)
|
|
96
306
|
}
|
|
@@ -200,6 +410,9 @@ function mergeWithNext() {
|
|
|
200
410
|
// experiment) where users paste their own sample list or upload a CSV.
|
|
201
411
|
watch(() => props.modelValue, (open) => {
|
|
202
412
|
if (open) {
|
|
413
|
+
activeWorkflow.value = props.initialMode
|
|
414
|
+
manualDraftGroups.value = cloneGroups(props.groups)
|
|
415
|
+
manualResetSelection()
|
|
203
416
|
autoGroup.reset()
|
|
204
417
|
currentStep.value = 0
|
|
205
418
|
csvFileName.value = ''
|
|
@@ -221,6 +434,17 @@ watch(() => props.modelValue, (open) => {
|
|
|
221
434
|
}
|
|
222
435
|
}, { immediate: true })
|
|
223
436
|
|
|
437
|
+
watch(manualSamples, (samples) => {
|
|
438
|
+
manualSelectedSamples.value = manualSelectedSamples.value.filter(sample => samples.includes(sample))
|
|
439
|
+
})
|
|
440
|
+
|
|
441
|
+
watch(manualTargetGroupName, (target) => {
|
|
442
|
+
if (!target) return
|
|
443
|
+
const existingGroup = manualDraftGroups.value.find(group => group.name === target)
|
|
444
|
+
if (!existingGroup) return
|
|
445
|
+
manualSelectedColor.value = existingGroup.color
|
|
446
|
+
})
|
|
447
|
+
|
|
224
448
|
// Wizard steps: three steps, unconditional
|
|
225
449
|
const allSteps: WizardStep[] = [
|
|
226
450
|
{ id: 'input', label: 'Input' },
|
|
@@ -271,6 +495,11 @@ function handleApply() {
|
|
|
271
495
|
emit('update:modelValue', false)
|
|
272
496
|
}
|
|
273
497
|
|
|
498
|
+
function handleManualApply() {
|
|
499
|
+
emit('apply', manualResult.value)
|
|
500
|
+
emit('update:modelValue', false)
|
|
501
|
+
}
|
|
502
|
+
|
|
274
503
|
function handleCancel() {
|
|
275
504
|
emit('update:modelValue', false)
|
|
276
505
|
}
|
|
@@ -380,15 +609,62 @@ const isFirstStep = computed(() => currentStep.value === 0)
|
|
|
380
609
|
<template>
|
|
381
610
|
<BaseModal
|
|
382
611
|
:model-value="modelValue"
|
|
383
|
-
title="
|
|
384
|
-
size="lg"
|
|
612
|
+
title="Group Samples"
|
|
613
|
+
:size="activeWorkflow === 'manual' ? 'xl' : 'lg'"
|
|
385
614
|
:close-on-overlay="false"
|
|
386
615
|
:close-on-escape="false"
|
|
387
616
|
@update:model-value="emit('update:modelValue', $event)"
|
|
388
617
|
@close="handleCancel"
|
|
389
618
|
>
|
|
390
619
|
<div class="mint-auto-group">
|
|
620
|
+
<div
|
|
621
|
+
:class="[
|
|
622
|
+
'mint-auto-group__workflow-bar',
|
|
623
|
+
activeWorkflow === 'manual' ? 'mint-auto-group__workflow-bar--manual' : '',
|
|
624
|
+
]"
|
|
625
|
+
>
|
|
626
|
+
<div class="mint-auto-group__workflow-tabs" role="tablist" aria-label="Grouping workflow">
|
|
627
|
+
<button
|
|
628
|
+
type="button"
|
|
629
|
+
:class="[
|
|
630
|
+
'mint-auto-group__workflow-tab',
|
|
631
|
+
activeWorkflow === 'auto' ? 'mint-auto-group__workflow-tab--active' : '',
|
|
632
|
+
]"
|
|
633
|
+
role="tab"
|
|
634
|
+
:aria-selected="activeWorkflow === 'auto'"
|
|
635
|
+
@click="activeWorkflow = 'auto'"
|
|
636
|
+
>
|
|
637
|
+
Auto
|
|
638
|
+
</button>
|
|
639
|
+
<button
|
|
640
|
+
type="button"
|
|
641
|
+
:class="[
|
|
642
|
+
'mint-auto-group__workflow-tab',
|
|
643
|
+
activeWorkflow === 'manual' ? 'mint-auto-group__workflow-tab--active' : '',
|
|
644
|
+
]"
|
|
645
|
+
role="tab"
|
|
646
|
+
:aria-selected="activeWorkflow === 'manual'"
|
|
647
|
+
@click="activeWorkflow = 'manual'"
|
|
648
|
+
>
|
|
649
|
+
Manual
|
|
650
|
+
</button>
|
|
651
|
+
</div>
|
|
652
|
+
|
|
653
|
+
<div v-if="activeWorkflow === 'manual'" class="mint-auto-group__manual-summary">
|
|
654
|
+
<div class="mint-auto-group__manual-summary-text">
|
|
655
|
+
<span class="mint-auto-group__manual-eyebrow">Manual workflow</span>
|
|
656
|
+
<strong>Assign selected samples to cohorts</strong>
|
|
657
|
+
</div>
|
|
658
|
+
<div class="mint-auto-group__manual-stats" aria-label="Manual grouping status">
|
|
659
|
+
<span><strong>{{ manualGroupedCount }}</strong> grouped</span>
|
|
660
|
+
<span><strong>{{ manualUngroupedCount }}</strong> unassigned</span>
|
|
661
|
+
<span><strong>{{ manualDraftGroups.length }}</strong> cohorts</span>
|
|
662
|
+
</div>
|
|
663
|
+
</div>
|
|
664
|
+
</div>
|
|
665
|
+
|
|
391
666
|
<StepWizard
|
|
667
|
+
v-if="activeWorkflow === 'auto'"
|
|
392
668
|
ref="wizardRef"
|
|
393
669
|
v-model="currentStep"
|
|
394
670
|
:steps="dynamicSteps"
|
|
@@ -840,7 +1116,307 @@ const isFirstStep = computed(() => currentStep.value === 0)
|
|
|
840
1116
|
</div>
|
|
841
1117
|
</template>
|
|
842
1118
|
</StepWizard>
|
|
1119
|
+
|
|
1120
|
+
<div v-else class="mint-auto-group__manual-mode">
|
|
1121
|
+
<div class="mint-auto-group__manual-grid">
|
|
1122
|
+
<section class="mint-auto-group__manual-panel" aria-label="Manual sample selection">
|
|
1123
|
+
<div class="mint-auto-group__manual-panel-head">
|
|
1124
|
+
<div>
|
|
1125
|
+
<span class="mint-auto-group__manual-step">1</span>
|
|
1126
|
+
<h3>Samples</h3>
|
|
1127
|
+
</div>
|
|
1128
|
+
<div class="mint-auto-group__manual-panel-meta">
|
|
1129
|
+
<span>{{ manualFilteredSamples.length }} of {{ manualSamplePool.length }}</span>
|
|
1130
|
+
<label class="mint-auto-group__manual-filter">
|
|
1131
|
+
<input
|
|
1132
|
+
v-model="manualUngroupedOnly"
|
|
1133
|
+
type="checkbox"
|
|
1134
|
+
class="mint-auto-group__manual-checkbox"
|
|
1135
|
+
/>
|
|
1136
|
+
<span>Ungrouped only</span>
|
|
1137
|
+
</label>
|
|
1138
|
+
</div>
|
|
1139
|
+
</div>
|
|
1140
|
+
|
|
1141
|
+
<div class="mint-auto-group__manual-search">
|
|
1142
|
+
<svg class="mint-auto-group__manual-search-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1143
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
1144
|
+
</svg>
|
|
1145
|
+
<input
|
|
1146
|
+
v-model="manualSearchQuery"
|
|
1147
|
+
type="text"
|
|
1148
|
+
aria-label="Search samples for manual grouping"
|
|
1149
|
+
placeholder="Search sample keyword..."
|
|
1150
|
+
class="mint-auto-group__manual-search-input"
|
|
1151
|
+
/>
|
|
1152
|
+
</div>
|
|
1153
|
+
|
|
1154
|
+
<label class="mint-auto-group__manual-selectbar">
|
|
1155
|
+
<input
|
|
1156
|
+
type="checkbox"
|
|
1157
|
+
:checked="manualAllVisibleSelected"
|
|
1158
|
+
:indeterminate="manualSomeVisibleSelected"
|
|
1159
|
+
:disabled="manualFilteredSamples.length === 0"
|
|
1160
|
+
class="mint-auto-group__manual-checkbox"
|
|
1161
|
+
@change="manualToggleVisibleSamples"
|
|
1162
|
+
/>
|
|
1163
|
+
<span>Select all matching</span>
|
|
1164
|
+
<span class="mint-auto-group__manual-selectbar-count">
|
|
1165
|
+
{{ manualFilteredSamples.length }} shown
|
|
1166
|
+
</span>
|
|
1167
|
+
</label>
|
|
1168
|
+
|
|
1169
|
+
<div class="mint-auto-group__manual-list">
|
|
1170
|
+
<label
|
|
1171
|
+
v-for="sample in manualFilteredSamples"
|
|
1172
|
+
:key="sample"
|
|
1173
|
+
:class="[
|
|
1174
|
+
'mint-auto-group__manual-sample',
|
|
1175
|
+
manualSelectedSet.has(sample) ? 'mint-auto-group__manual-sample--selected' : '',
|
|
1176
|
+
]"
|
|
1177
|
+
:title="sample"
|
|
1178
|
+
>
|
|
1179
|
+
<input
|
|
1180
|
+
type="checkbox"
|
|
1181
|
+
:checked="manualSelectedSet.has(sample)"
|
|
1182
|
+
class="mint-auto-group__manual-checkbox"
|
|
1183
|
+
@change="manualToggleSample(sample)"
|
|
1184
|
+
/>
|
|
1185
|
+
<span class="mint-auto-group__manual-sample-name">{{ sample }}</span>
|
|
1186
|
+
<span
|
|
1187
|
+
:class="[
|
|
1188
|
+
'mint-auto-group__manual-status',
|
|
1189
|
+
manualGetSampleGroup(sample) ? '' : 'mint-auto-group__manual-status--empty',
|
|
1190
|
+
]"
|
|
1191
|
+
>
|
|
1192
|
+
<span
|
|
1193
|
+
v-if="manualGetSampleGroup(sample)"
|
|
1194
|
+
class="mint-auto-group__manual-dot"
|
|
1195
|
+
:style="{ backgroundColor: manualGetSampleGroup(sample)?.color }"
|
|
1196
|
+
aria-hidden="true"
|
|
1197
|
+
/>
|
|
1198
|
+
{{ manualGetSampleStatus(sample) }}
|
|
1199
|
+
</span>
|
|
1200
|
+
<span class="mint-auto-group__manual-tooltip">{{ sample }}</span>
|
|
1201
|
+
</label>
|
|
1202
|
+
|
|
1203
|
+
<div v-if="manualFilteredSamples.length === 0" class="mint-auto-group__manual-empty">
|
|
1204
|
+
No samples
|
|
1205
|
+
</div>
|
|
1206
|
+
</div>
|
|
1207
|
+
|
|
1208
|
+
<div class="mint-auto-group__manual-selection-footer">
|
|
1209
|
+
<span><strong>{{ manualSelectedSamples.length }}</strong> selected</span>
|
|
1210
|
+
<button
|
|
1211
|
+
type="button"
|
|
1212
|
+
class="mint-auto-group__manual-link"
|
|
1213
|
+
:disabled="manualSelectedSamples.length === 0"
|
|
1214
|
+
@click="manualClearSamples"
|
|
1215
|
+
>
|
|
1216
|
+
Clear
|
|
1217
|
+
</button>
|
|
1218
|
+
</div>
|
|
1219
|
+
</section>
|
|
1220
|
+
|
|
1221
|
+
<section class="mint-auto-group__manual-panel" aria-label="Manual cohort assignment">
|
|
1222
|
+
<div class="mint-auto-group__manual-panel-head">
|
|
1223
|
+
<div>
|
|
1224
|
+
<span class="mint-auto-group__manual-step">2</span>
|
|
1225
|
+
<h3>Cohort target</h3>
|
|
1226
|
+
</div>
|
|
1227
|
+
</div>
|
|
1228
|
+
|
|
1229
|
+
<div
|
|
1230
|
+
:class="[
|
|
1231
|
+
'mint-auto-group__manual-assignment',
|
|
1232
|
+
manualSelectedSamples.length === 0 ? 'mint-auto-group__manual-assignment--empty' : '',
|
|
1233
|
+
]"
|
|
1234
|
+
>
|
|
1235
|
+
<div class="mint-auto-group__manual-selected-summary">
|
|
1236
|
+
<span
|
|
1237
|
+
class="mint-auto-group__manual-dot"
|
|
1238
|
+
:style="{ backgroundColor: manualSelectedColor }"
|
|
1239
|
+
aria-hidden="true"
|
|
1240
|
+
/>
|
|
1241
|
+
<span>
|
|
1242
|
+
{{ manualSelectedSamples.length > 0
|
|
1243
|
+
? `${manualSelectedSamples.length} selected`
|
|
1244
|
+
: 'No sample selected' }}
|
|
1245
|
+
</span>
|
|
1246
|
+
</div>
|
|
1247
|
+
|
|
1248
|
+
<div class="mint-auto-group__manual-fields">
|
|
1249
|
+
<label class="mint-auto-group__manual-field">
|
|
1250
|
+
<span>Group</span>
|
|
1251
|
+
<div class="mint-auto-group__manual-combo">
|
|
1252
|
+
<span
|
|
1253
|
+
class="mint-auto-group__manual-dot"
|
|
1254
|
+
:style="{ backgroundColor: manualSelectedColor }"
|
|
1255
|
+
aria-hidden="true"
|
|
1256
|
+
/>
|
|
1257
|
+
<BaseInput
|
|
1258
|
+
v-model="manualGroupName"
|
|
1259
|
+
list="mint-auto-group-manual-groups"
|
|
1260
|
+
placeholder="Type or pick..."
|
|
1261
|
+
size="sm"
|
|
1262
|
+
class="mint-auto-group__manual-combo-input"
|
|
1263
|
+
/>
|
|
1264
|
+
<svg class="mint-auto-group__manual-combo-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1265
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9l6 6 6-6" />
|
|
1266
|
+
</svg>
|
|
1267
|
+
<datalist id="mint-auto-group-manual-groups">
|
|
1268
|
+
<option
|
|
1269
|
+
v-for="group in manualDraftGroups"
|
|
1270
|
+
:key="`manual-group-option-${group.name}`"
|
|
1271
|
+
:value="manualGetGroupPrimaryName(group.name)"
|
|
1272
|
+
/>
|
|
1273
|
+
</datalist>
|
|
1274
|
+
</div>
|
|
1275
|
+
</label>
|
|
1276
|
+
|
|
1277
|
+
<label class="mint-auto-group__manual-field">
|
|
1278
|
+
<span>Subgroup <small>optional</small></span>
|
|
1279
|
+
<div class="mint-auto-group__manual-combo">
|
|
1280
|
+
<BaseInput
|
|
1281
|
+
v-model="manualSubGroupName"
|
|
1282
|
+
list="mint-auto-group-manual-subgroups"
|
|
1283
|
+
placeholder="e.g. Day 7"
|
|
1284
|
+
size="sm"
|
|
1285
|
+
class="mint-auto-group__manual-combo-input"
|
|
1286
|
+
/>
|
|
1287
|
+
<svg class="mint-auto-group__manual-combo-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1288
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 9l6 6 6-6" />
|
|
1289
|
+
</svg>
|
|
1290
|
+
<datalist id="mint-auto-group-manual-subgroups">
|
|
1291
|
+
<option
|
|
1292
|
+
v-for="group in manualDraftGroups"
|
|
1293
|
+
:key="`manual-subgroup-option-${group.name}`"
|
|
1294
|
+
:value="manualGetGroupSecondaryName(group.name) || group.name"
|
|
1295
|
+
/>
|
|
1296
|
+
</datalist>
|
|
1297
|
+
</div>
|
|
1298
|
+
</label>
|
|
1299
|
+
</div>
|
|
1300
|
+
|
|
1301
|
+
<div class="mint-auto-group__manual-field">
|
|
1302
|
+
<span>Color</span>
|
|
1303
|
+
<div class="mint-auto-group__manual-swatches">
|
|
1304
|
+
<button
|
|
1305
|
+
v-for="color in manualColorOptions"
|
|
1306
|
+
:key="color"
|
|
1307
|
+
type="button"
|
|
1308
|
+
:class="[
|
|
1309
|
+
'mint-auto-group__manual-swatch',
|
|
1310
|
+
manualSelectedColor === color ? 'mint-auto-group__manual-swatch--active' : '',
|
|
1311
|
+
]"
|
|
1312
|
+
:style="{ backgroundColor: color }"
|
|
1313
|
+
:aria-label="`Use color ${color}`"
|
|
1314
|
+
@click="manualChooseColor(color)"
|
|
1315
|
+
/>
|
|
1316
|
+
</div>
|
|
1317
|
+
</div>
|
|
1318
|
+
|
|
1319
|
+
<div class="mint-auto-group__manual-target">
|
|
1320
|
+
<span>Target cohort</span>
|
|
1321
|
+
<code>{{ manualTargetGroupName || 'Group/Subgroup' }}</code>
|
|
1322
|
+
</div>
|
|
1323
|
+
|
|
1324
|
+
<BaseButton
|
|
1325
|
+
variant="primary"
|
|
1326
|
+
size="sm"
|
|
1327
|
+
class="mint-auto-group__manual-assign"
|
|
1328
|
+
:disabled="!manualCanAssign"
|
|
1329
|
+
@click="manualAssignSamplesToGroup"
|
|
1330
|
+
>
|
|
1331
|
+
<svg class="mint-auto-group__manual-btn-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
1332
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14m-6-6 6 6-6 6" />
|
|
1333
|
+
</svg>
|
|
1334
|
+
Assign
|
|
1335
|
+
</BaseButton>
|
|
1336
|
+
</div>
|
|
1337
|
+
|
|
1338
|
+
<div class="mint-auto-group__manual-cohorts">
|
|
1339
|
+
<div class="mint-auto-group__manual-cohorts-head">
|
|
1340
|
+
<span>Cohorts so far</span>
|
|
1341
|
+
<span>{{ manualDraftGroups.length }} groups</span>
|
|
1342
|
+
</div>
|
|
1343
|
+
|
|
1344
|
+
<template v-if="manualShowHierarchy">
|
|
1345
|
+
<template
|
|
1346
|
+
v-for="majorGroup in manualHierarchicalGroups"
|
|
1347
|
+
:key="`manual-major-${majorGroup.name}`"
|
|
1348
|
+
>
|
|
1349
|
+
<button
|
|
1350
|
+
type="button"
|
|
1351
|
+
class="mint-auto-group__manual-chip mint-auto-group__manual-chip--major"
|
|
1352
|
+
:title="majorGroup.name"
|
|
1353
|
+
@click="manualUseExistingGroupAsTarget(majorGroup.subGroups[0]?.name || majorGroup.name)"
|
|
1354
|
+
>
|
|
1355
|
+
<span
|
|
1356
|
+
class="mint-auto-group__manual-chip-dot"
|
|
1357
|
+
:style="{ backgroundColor: majorGroup.color }"
|
|
1358
|
+
aria-hidden="true"
|
|
1359
|
+
/>
|
|
1360
|
+
<span class="mint-auto-group__manual-chip-main">{{ majorGroup.name }}</span>
|
|
1361
|
+
<span class="mint-auto-group__manual-chip-count">{{ majorGroup.allSamples.length }}</span>
|
|
1362
|
+
</button>
|
|
1363
|
+
|
|
1364
|
+
<button
|
|
1365
|
+
v-for="subGroup in majorGroup.subGroups"
|
|
1366
|
+
:key="`manual-sub-${subGroup.name}`"
|
|
1367
|
+
type="button"
|
|
1368
|
+
class="mint-auto-group__manual-chip mint-auto-group__manual-chip--sub"
|
|
1369
|
+
:title="subGroup.name"
|
|
1370
|
+
@click="manualUseExistingGroupAsTarget(subGroup.name)"
|
|
1371
|
+
>
|
|
1372
|
+
<span class="mint-auto-group__manual-chip-sub">
|
|
1373
|
+
{{ manualGetGroupSecondaryName(subGroup.name) || subGroup.name }}
|
|
1374
|
+
</span>
|
|
1375
|
+
<span class="mint-auto-group__manual-chip-count">{{ subGroup.samples.length }}</span>
|
|
1376
|
+
</button>
|
|
1377
|
+
</template>
|
|
1378
|
+
</template>
|
|
1379
|
+
|
|
1380
|
+
<template v-else>
|
|
1381
|
+
<button
|
|
1382
|
+
v-for="group in manualDraftGroups"
|
|
1383
|
+
:key="group.name"
|
|
1384
|
+
type="button"
|
|
1385
|
+
class="mint-auto-group__manual-chip"
|
|
1386
|
+
:title="group.name"
|
|
1387
|
+
@click="manualUseExistingGroupAsTarget(group.name)"
|
|
1388
|
+
>
|
|
1389
|
+
<span
|
|
1390
|
+
class="mint-auto-group__manual-chip-dot"
|
|
1391
|
+
:style="{ backgroundColor: group.color }"
|
|
1392
|
+
aria-hidden="true"
|
|
1393
|
+
/>
|
|
1394
|
+
<span class="mint-auto-group__manual-chip-main">{{ group.name }}</span>
|
|
1395
|
+
<span class="mint-auto-group__manual-chip-count">{{ group.samples.length }}</span>
|
|
1396
|
+
</button>
|
|
1397
|
+
</template>
|
|
1398
|
+
|
|
1399
|
+
<div v-if="manualDraftGroups.length === 0" class="mint-auto-group__manual-empty">
|
|
1400
|
+
No cohorts yet
|
|
1401
|
+
</div>
|
|
1402
|
+
</div>
|
|
1403
|
+
</section>
|
|
1404
|
+
</div>
|
|
1405
|
+
|
|
1406
|
+
</div>
|
|
843
1407
|
</div>
|
|
1408
|
+
|
|
1409
|
+
<template v-if="activeWorkflow === 'manual'" #footer>
|
|
1410
|
+
<div class="mint-auto-group__nav mint-auto-group__nav--manual">
|
|
1411
|
+
<BaseButton variant="secondary" @click="handleCancel">
|
|
1412
|
+
Cancel
|
|
1413
|
+
</BaseButton>
|
|
1414
|
+
<div class="mint-auto-group__nav-spacer" />
|
|
1415
|
+
<BaseButton variant="primary" @click="handleManualApply">
|
|
1416
|
+
Apply
|
|
1417
|
+
</BaseButton>
|
|
1418
|
+
</div>
|
|
1419
|
+
</template>
|
|
844
1420
|
</BaseModal>
|
|
845
1421
|
</template>
|
|
846
1422
|
|
|
@@ -15,6 +15,7 @@ interface Props {
|
|
|
15
15
|
min?: number
|
|
16
16
|
max?: number
|
|
17
17
|
step?: number
|
|
18
|
+
list?: string
|
|
18
19
|
ariaDescribedby?: string
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -54,6 +55,7 @@ function handleInput(event: Event) {
|
|
|
54
55
|
:min="min"
|
|
55
56
|
:max="max"
|
|
56
57
|
:step="step"
|
|
58
|
+
:list="list"
|
|
57
59
|
:aria-invalid="error || undefined"
|
|
58
60
|
:aria-describedby="ariaDescribedby || undefined"
|
|
59
61
|
:class="[
|