@morscherlab/mint-sdk 1.1.7 → 1.1.9
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/RetiredStyles.test.d.ts +1 -0
- package/dist/__tests__/composables/autoGroup/groupByOrder.test.d.ts +1 -0
- package/dist/__tests__/composables/useSampleGroups.tree.test.d.ts +1 -0
- package/dist/components/AppTopBar.vue.d.ts +6 -11
- package/dist/components/AutoGroupModal.adapter.d.ts +10 -6
- package/dist/components/PluginIcon.vue.d.ts +2 -2
- package/dist/components/PluginWorkspaceView.props.d.ts +5 -7
- package/dist/components/PluginWorkspaceView.vue.d.ts +0 -3
- package/dist/components/SampleSelector.groups.d.ts +7 -7
- package/dist/components/SampleSelector.selection.d.ts +6 -12
- package/dist/components/SampleSelector.tree.d.ts +39 -0
- package/dist/components/SampleSelectorGroupNode.vue.d.ts +9 -0
- package/dist/components/SmartGroup.types.d.ts +5 -0
- package/dist/components/SmartGroupFieldRecipe.groups.d.ts +3 -2
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +2 -0
- package/dist/components/index.js +1 -1
- package/dist/{components-CDomhGSj.js → components-BbtUpCTv.js} +913 -943
- package/dist/components-BbtUpCTv.js.map +1 -0
- package/dist/composables/index.d.ts +1 -1
- package/dist/composables/index.js +1 -1
- package/dist/composables/useAutoGroup.d.ts +3 -0
- package/dist/composables/usePlatformContext.d.ts +3 -0
- package/dist/composables/useSampleGroups.d.ts +25 -0
- package/dist/index.js +2 -2
- package/dist/install.js +1 -1
- package/dist/styles.css +130 -557
- package/dist/types/platform.d.ts +1 -0
- package/dist/{useProtocolTemplates-BMt0LlXf.js → useProtocolTemplates-BWfNaynA.js} +199 -24
- package/dist/useProtocolTemplates-BWfNaynA.js.map +1 -0
- package/dist/utils/pluginIcon.d.ts +1 -1
- package/dist/utils/sampleGroupPath.d.ts +6 -0
- package/package.json +1 -1
- package/src/__tests__/components/AppAvatarMenu.test.ts +11 -0
- package/src/__tests__/components/AppTopBar.test.ts +84 -10
- package/src/__tests__/components/AppTopBarPageSelector.test.ts +1 -1
- package/src/__tests__/components/AutoGroupModal.adapter.test.ts +16 -6
- package/src/__tests__/components/PluginIcon.test.ts +19 -0
- package/src/__tests__/components/PluginWorkspaceView.test.ts +8 -0
- package/src/__tests__/components/RetiredStyles.test.ts +82 -0
- package/src/__tests__/components/SampleSelector.drag.test.ts +26 -0
- package/src/__tests__/components/SampleSelector.groups.test.ts +19 -6
- package/src/__tests__/components/SampleSelector.selection.test.ts +9 -23
- package/src/__tests__/components/SmartGroupFieldRecipe.groups.test.ts +5 -0
- package/src/__tests__/composables/autoGroup/compose.test.ts +9 -10
- package/src/__tests__/composables/autoGroup/groupByOrder.test.ts +249 -0
- package/src/__tests__/composables/autoGroup/integration.test.ts +3 -7
- package/src/__tests__/composables/useSampleGroups.tree.test.ts +145 -0
- package/src/components/AppTopBar.story.vue +9 -7
- package/src/components/AppTopBar.vue +79 -72
- package/src/components/AutoGroupModal.adapter.ts +17 -12
- package/src/components/AutoGroupModal.story.vue +89 -0
- package/src/components/AutoGroupModal.vue +16 -8
- package/src/components/PluginIcon.story.vue +11 -9
- package/src/components/PluginIcon.vue +3 -3
- package/src/components/PluginWorkspaceView.props.ts +5 -7
- package/src/components/PluginWorkspaceView.vue +0 -4
- package/src/components/SampleSelector.drag.ts +5 -5
- package/src/components/SampleSelector.groups.ts +15 -15
- package/src/components/SampleSelector.selection.ts +7 -46
- package/src/components/SampleSelector.story.vue +37 -0
- package/src/components/SampleSelector.tree.ts +48 -0
- package/src/components/SampleSelector.vue +52 -320
- package/src/components/SampleSelectorGroupNode.vue +197 -0
- package/src/components/SmartGroup.types.ts +5 -0
- package/src/components/SmartGroupFieldRecipe.groups.ts +6 -2
- package/src/components/SmartGroupFieldRecipe.vue +76 -4
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +2 -2
- package/src/composables/autoGroup/compose.ts +31 -10
- package/src/composables/index.ts +1 -0
- package/src/composables/useAutoGroup.ts +108 -5
- package/src/composables/useSampleGroups.ts +118 -33
- package/src/styles/components/app-avatar-menu.css +2 -1
- package/src/styles/components/app-plugin-switcher.css +0 -13
- package/src/styles/components/app-top-bar.css +13 -54
- package/src/styles/components/dose-calculator.css +0 -4
- package/src/styles/components/experiment-popover.css +2 -104
- package/src/styles/components/experiment-selector-modal.css +0 -7
- package/src/styles/components/plugin-icon.css +3 -0
- package/src/styles/components/protocol-step-editor.css +0 -6
- package/src/styles/components/reagent-editor.css +0 -5
- package/src/styles/components/sample-hierarchy-tree.css +0 -4
- package/src/styles/components/sample-selector.css +0 -96
- package/src/styles/components/schedule-calendar.css +0 -6
- package/src/styles/components/smart-group.css +22 -0
- package/src/styles/components/well-plate.css +1 -27
- package/src/types/platform.ts +1 -0
- package/src/utils/pluginIcon.ts +5 -2
- package/src/utils/sampleGroupPath.ts +19 -0
- package/dist/components-CDomhGSj.js.map +0 -1
- package/dist/useProtocolTemplates-BMt0LlXf.js.map +0 -1
|
@@ -5,10 +5,12 @@ import BaseButton from './BaseButton.vue'
|
|
|
5
5
|
import BaseInput from './BaseInput.vue'
|
|
6
6
|
import AutoGroupModal from './AutoGroupModal.vue'
|
|
7
7
|
import SampleSelectorSampleRow from './SampleSelectorSampleRow.vue'
|
|
8
|
+
import SampleSelectorGroupNode from './SampleSelectorGroupNode.vue'
|
|
9
|
+
import { collectNodeKeys, type SampleGroupTreeContext } from './SampleSelector.tree'
|
|
8
10
|
import type { SampleGroup } from '../types'
|
|
9
11
|
import type { AutoGroupResult } from '../types/auto-group'
|
|
10
12
|
import { useTextSearch } from '../composables/useTextSearch'
|
|
11
|
-
import { useSampleGroups
|
|
13
|
+
import { useSampleGroups } from '../composables/useSampleGroups'
|
|
12
14
|
import { useExpansionSet } from '../composables/useExpansionSet'
|
|
13
15
|
import { useExperimentSamples } from '../composables/useExperimentSamples'
|
|
14
16
|
import { useSampleSelectorDrag } from './SampleSelector.drag'
|
|
@@ -21,8 +23,7 @@ import {
|
|
|
21
23
|
} from './SampleSelector.colors'
|
|
22
24
|
import {
|
|
23
25
|
removeSampleFromGroup as removeSampleFromSampleGroup,
|
|
24
|
-
|
|
25
|
-
removeSampleMajorGroup,
|
|
26
|
+
removeSampleGroups,
|
|
26
27
|
} from './SampleSelector.groups'
|
|
27
28
|
|
|
28
29
|
interface Props {
|
|
@@ -105,8 +106,7 @@ const sampleGroups = useSampleGroups({
|
|
|
105
106
|
samples: () => resolvedSamples.value,
|
|
106
107
|
groups: internalGroups,
|
|
107
108
|
})
|
|
108
|
-
const
|
|
109
|
-
const showHierarchy = sampleGroups.showHierarchy
|
|
109
|
+
const groupTree = sampleGroups.groupTree
|
|
110
110
|
|
|
111
111
|
const groupingEnabled = computed(() => internalGroups.value.length > 0)
|
|
112
112
|
const ungroupedSamples = sampleGroups.ungroupedSamples
|
|
@@ -123,16 +123,12 @@ const {
|
|
|
123
123
|
isAllSelected,
|
|
124
124
|
toggleSelectAll,
|
|
125
125
|
toggleSample,
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
isGroupPartiallySelected,
|
|
130
|
-
isMajorGroupFullySelected,
|
|
131
|
-
isMajorGroupPartiallySelected,
|
|
126
|
+
toggleSamplesSelection,
|
|
127
|
+
isFullySelected,
|
|
128
|
+
isPartiallySelected,
|
|
132
129
|
} = useSampleSelectorSelection({
|
|
133
130
|
selected: () => props.modelValue,
|
|
134
131
|
samples: () => resolvedSamples.value,
|
|
135
|
-
findGroup: sampleGroups.findGroup,
|
|
136
132
|
emitSelected: samples => emit('update:modelValue', samples),
|
|
137
133
|
})
|
|
138
134
|
|
|
@@ -146,15 +142,7 @@ function isGroupExpanded(groupName: string): boolean {
|
|
|
146
142
|
}
|
|
147
143
|
|
|
148
144
|
function expandAllGroups() {
|
|
149
|
-
|
|
150
|
-
for (const major of hierarchicalGroups.value) {
|
|
151
|
-
expanded.push(`major:${major.name}`)
|
|
152
|
-
for (const sub of major.subGroups) {
|
|
153
|
-
expanded.push(sub.name)
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
expanded.push('__ungrouped__')
|
|
157
|
-
groupExpansion.setExpanded(expanded)
|
|
145
|
+
groupExpansion.setExpanded([...collectNodeKeys(groupTree.value), '__ungrouped__'])
|
|
158
146
|
}
|
|
159
147
|
|
|
160
148
|
function collapseAllGroups() {
|
|
@@ -177,12 +165,8 @@ function clearGroups() {
|
|
|
177
165
|
internalGroups.value = []
|
|
178
166
|
}
|
|
179
167
|
|
|
180
|
-
function
|
|
181
|
-
internalGroups.value =
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function deleteGroup(groupName: string) {
|
|
185
|
-
internalGroups.value = removeSampleGroup(internalGroups.value, groupName)
|
|
168
|
+
function deleteGroups(groupNames: string[]) {
|
|
169
|
+
internalGroups.value = removeSampleGroups(internalGroups.value, groupNames)
|
|
186
170
|
}
|
|
187
171
|
|
|
188
172
|
function removeSampleFromGroup(sample: string, groupName: string) {
|
|
@@ -190,15 +174,11 @@ function removeSampleFromGroup(sample: string, groupName: string) {
|
|
|
190
174
|
}
|
|
191
175
|
|
|
192
176
|
// Color picker
|
|
193
|
-
function openColorPicker(
|
|
177
|
+
function openColorPicker(groupNames: string[], event: Event) {
|
|
194
178
|
event.stopPropagation()
|
|
195
|
-
editingColor.value =
|
|
196
|
-
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
function openMajorGroupColorPicker(majorGroup: SampleMajorGroup, event: Event) {
|
|
200
|
-
event.stopPropagation()
|
|
201
|
-
editingColor.value = { kind: 'family', names: majorGroup.subGroups.map(sg => sg.name) }
|
|
179
|
+
editingColor.value = groupNames.length === 1
|
|
180
|
+
? { kind: 'single', name: groupNames[0] }
|
|
181
|
+
: { kind: 'family', names: groupNames }
|
|
202
182
|
colorPickerInput.value?.click()
|
|
203
183
|
}
|
|
204
184
|
|
|
@@ -219,6 +199,36 @@ const colorPickerSeed = computed(() => {
|
|
|
219
199
|
return getSampleGroupColorEditSeed(editingColor.value, getGroupColor)
|
|
220
200
|
})
|
|
221
201
|
|
|
202
|
+
// Bundle of handlers handed to every level of the recursive group tree.
|
|
203
|
+
const treeContext = computed<SampleGroupTreeContext>(() => ({
|
|
204
|
+
isExpanded: isGroupExpanded,
|
|
205
|
+
toggleExpanded: toggleGroupExpanded,
|
|
206
|
+
isSampleSelected: sample => props.modelValue.includes(sample),
|
|
207
|
+
isFullySelected,
|
|
208
|
+
isPartiallySelected,
|
|
209
|
+
toggleSamples: toggleSamplesSelection,
|
|
210
|
+
toggleSample,
|
|
211
|
+
removeGroups: deleteGroups,
|
|
212
|
+
removeSampleFromGroup,
|
|
213
|
+
openColorPicker,
|
|
214
|
+
draggingSample: () => draggingSample.value,
|
|
215
|
+
dragOverGroup: () => dragOverGroup.value,
|
|
216
|
+
draggingGroup: () => draggingGroup.value,
|
|
217
|
+
draggingGroupKind: () => draggingGroupKind.value,
|
|
218
|
+
reorderTarget: () => reorderTarget.value,
|
|
219
|
+
reorderPosition: () => reorderPosition.value,
|
|
220
|
+
onSampleDragStart: handleDragStart,
|
|
221
|
+
onSampleDragEnd: handleDragEnd,
|
|
222
|
+
onSampleDragOver: handleDragOver,
|
|
223
|
+
onSampleDragLeave: handleDragLeave,
|
|
224
|
+
onSampleDrop: handleDrop,
|
|
225
|
+
onGroupDragStart: handleGroupDragStart,
|
|
226
|
+
onGroupDragEnd: handleGroupDragEnd,
|
|
227
|
+
onGroupDragOver: handleGroupDragOver,
|
|
228
|
+
onGroupDragLeave: handleGroupDragLeave,
|
|
229
|
+
onGroupDrop: handleGroupDrop,
|
|
230
|
+
}))
|
|
231
|
+
|
|
222
232
|
// New group
|
|
223
233
|
function addNewGroup() {
|
|
224
234
|
const newGroup = createSampleGroup(newGroupName.value, internalGroups.value)
|
|
@@ -301,291 +311,13 @@ defineExpose({ handleSmartGroupApply })
|
|
|
301
311
|
|
|
302
312
|
<!-- Hierarchical Tree -->
|
|
303
313
|
<div class="mint-sample-selector__tree">
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
<!-- Major Group Header -->
|
|
312
|
-
<div
|
|
313
|
-
:class="[
|
|
314
|
-
'mint-sample-selector__major-header',
|
|
315
|
-
draggingGroup === majorGroup.name && draggingGroupKind === 'major' ? 'mint-sample-selector__header--dragging' : '',
|
|
316
|
-
reorderTarget === majorGroup.name && draggingGroupKind === 'major' && reorderPosition === 'before' ? 'mint-sample-selector__header--drag-over-before' : '',
|
|
317
|
-
reorderTarget === majorGroup.name && draggingGroupKind === 'major' && reorderPosition === 'after' ? 'mint-sample-selector__header--drag-over-after' : '',
|
|
318
|
-
]"
|
|
319
|
-
draggable="true"
|
|
320
|
-
@click="toggleGroupExpanded(`major:${majorGroup.name}`)"
|
|
321
|
-
@dragstart="handleGroupDragStart(majorGroup.name, 'major', $event)"
|
|
322
|
-
@dragend="handleGroupDragEnd"
|
|
323
|
-
@dragover="handleGroupDragOver(majorGroup.name, 'major', $event)"
|
|
324
|
-
@dragleave="handleGroupDragLeave($event)"
|
|
325
|
-
@drop="handleGroupDrop(majorGroup.name, 'major', $event)"
|
|
326
|
-
>
|
|
327
|
-
<svg
|
|
328
|
-
:class="[
|
|
329
|
-
'mint-sample-selector__chevron',
|
|
330
|
-
isGroupExpanded(`major:${majorGroup.name}`) ? 'mint-sample-selector__chevron--open' : '',
|
|
331
|
-
]"
|
|
332
|
-
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
|
333
|
-
>
|
|
334
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
335
|
-
</svg>
|
|
336
|
-
|
|
337
|
-
<input
|
|
338
|
-
type="checkbox"
|
|
339
|
-
:checked="isMajorGroupFullySelected(majorGroup)"
|
|
340
|
-
:indeterminate="isMajorGroupPartiallySelected(majorGroup)"
|
|
341
|
-
class="mint-sample-selector__checkbox"
|
|
342
|
-
:style="{ accentColor: majorGroup.color }"
|
|
343
|
-
@click.stop
|
|
344
|
-
@change="toggleMajorGroupSamples(majorGroup)"
|
|
345
|
-
/>
|
|
346
|
-
|
|
347
|
-
<button
|
|
348
|
-
type="button"
|
|
349
|
-
class="mint-sample-selector__color-dot mint-sample-selector__color-dot--large mint-sample-selector__color-dot--clickable"
|
|
350
|
-
:style="{ backgroundColor: majorGroup.color }"
|
|
351
|
-
@click.stop="openMajorGroupColorPicker(majorGroup, $event)"
|
|
352
|
-
title="Click to change color family"
|
|
353
|
-
/>
|
|
354
|
-
|
|
355
|
-
<span class="mint-sample-selector__major-name">{{ majorGroup.name }}</span>
|
|
356
|
-
|
|
357
|
-
<span
|
|
358
|
-
class="mint-sample-selector__count-badge"
|
|
359
|
-
:style="{ backgroundColor: majorGroup.color + '20', color: majorGroup.color }"
|
|
360
|
-
>
|
|
361
|
-
{{ majorGroup.allSamples.length }}
|
|
362
|
-
</span>
|
|
363
|
-
|
|
364
|
-
<button
|
|
365
|
-
type="button"
|
|
366
|
-
class="mint-sample-selector__delete-btn mint-sample-selector__delete-btn--hidden"
|
|
367
|
-
@click.stop="deleteMajorGroup(majorGroup)"
|
|
368
|
-
title="Remove major group"
|
|
369
|
-
>
|
|
370
|
-
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
371
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
372
|
-
</svg>
|
|
373
|
-
</button>
|
|
374
|
-
</div>
|
|
375
|
-
|
|
376
|
-
<!-- Sub Groups (collapsible) -->
|
|
377
|
-
<Transition name="mint-collapse">
|
|
378
|
-
<div
|
|
379
|
-
v-if="isGroupExpanded(`major:${majorGroup.name}`)"
|
|
380
|
-
class="mint-sample-selector__sub-groups"
|
|
381
|
-
:style="{ borderColor: majorGroup.color + '30' }"
|
|
382
|
-
>
|
|
383
|
-
<div
|
|
384
|
-
v-for="subGroup in majorGroup.subGroups"
|
|
385
|
-
:key="subGroup.name"
|
|
386
|
-
:class="[
|
|
387
|
-
'mint-sample-selector__sub-group',
|
|
388
|
-
dragOverGroup === subGroup.name ? 'mint-sample-selector__sub-group--drag-over' : '',
|
|
389
|
-
]"
|
|
390
|
-
@dragover="handleDragOver(subGroup.name, $event)"
|
|
391
|
-
@dragleave="handleDragLeave"
|
|
392
|
-
@drop="handleDrop(subGroup.name, $event)"
|
|
393
|
-
>
|
|
394
|
-
<!-- Sub Group Header -->
|
|
395
|
-
<div
|
|
396
|
-
:class="[
|
|
397
|
-
'mint-sample-selector__sub-header',
|
|
398
|
-
draggingGroup === subGroup.name && draggingGroupKind === 'sub' ? 'mint-sample-selector__header--dragging' : '',
|
|
399
|
-
reorderTarget === subGroup.name && draggingGroupKind === 'sub' && reorderPosition === 'before' ? 'mint-sample-selector__header--drag-over-before' : '',
|
|
400
|
-
reorderTarget === subGroup.name && draggingGroupKind === 'sub' && reorderPosition === 'after' ? 'mint-sample-selector__header--drag-over-after' : '',
|
|
401
|
-
]"
|
|
402
|
-
draggable="true"
|
|
403
|
-
@click="toggleGroupExpanded(subGroup.name)"
|
|
404
|
-
@dragstart="handleGroupDragStart(subGroup.name, 'sub', $event)"
|
|
405
|
-
@dragend="handleGroupDragEnd"
|
|
406
|
-
@dragover="handleGroupDragOver(subGroup.name, 'sub', $event)"
|
|
407
|
-
@dragleave="handleGroupDragLeave($event)"
|
|
408
|
-
@drop="handleGroupDrop(subGroup.name, 'sub', $event)"
|
|
409
|
-
>
|
|
410
|
-
<svg
|
|
411
|
-
:class="[
|
|
412
|
-
'mint-sample-selector__chevron mint-sample-selector__chevron--small',
|
|
413
|
-
isGroupExpanded(subGroup.name) ? 'mint-sample-selector__chevron--open' : '',
|
|
414
|
-
]"
|
|
415
|
-
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
|
416
|
-
>
|
|
417
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
418
|
-
</svg>
|
|
419
|
-
|
|
420
|
-
<input
|
|
421
|
-
type="checkbox"
|
|
422
|
-
:checked="isGroupFullySelected(subGroup.name)"
|
|
423
|
-
:indeterminate="isGroupPartiallySelected(subGroup.name)"
|
|
424
|
-
class="mint-sample-selector__checkbox mint-sample-selector__checkbox--small"
|
|
425
|
-
:style="{ accentColor: subGroup.displayColor }"
|
|
426
|
-
@click.stop
|
|
427
|
-
@change="toggleGroupSamples(subGroup.name)"
|
|
428
|
-
/>
|
|
429
|
-
|
|
430
|
-
<div
|
|
431
|
-
class="mint-sample-selector__color-dot"
|
|
432
|
-
:style="{ backgroundColor: subGroup.displayColor }"
|
|
433
|
-
aria-hidden="true"
|
|
434
|
-
/>
|
|
435
|
-
|
|
436
|
-
<span class="mint-sample-selector__sub-name">{{ subGroup.name }}</span>
|
|
437
|
-
|
|
438
|
-
<span
|
|
439
|
-
class="mint-sample-selector__count-badge mint-sample-selector__count-badge--small"
|
|
440
|
-
:style="{ backgroundColor: subGroup.displayBg, color: subGroup.displayColor }"
|
|
441
|
-
>
|
|
442
|
-
{{ subGroup.samples.length }}
|
|
443
|
-
</span>
|
|
444
|
-
|
|
445
|
-
<button
|
|
446
|
-
type="button"
|
|
447
|
-
class="mint-sample-selector__delete-btn mint-sample-selector__delete-btn--hidden"
|
|
448
|
-
@click.stop="deleteGroup(subGroup.name)"
|
|
449
|
-
title="Remove sub group"
|
|
450
|
-
>
|
|
451
|
-
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
452
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
453
|
-
</svg>
|
|
454
|
-
</button>
|
|
455
|
-
</div>
|
|
456
|
-
|
|
457
|
-
<!-- Samples (collapsible) -->
|
|
458
|
-
<Transition name="mint-collapse">
|
|
459
|
-
<div
|
|
460
|
-
v-if="isGroupExpanded(subGroup.name)"
|
|
461
|
-
class="mint-sample-selector__samples"
|
|
462
|
-
:style="{ borderColor: subGroup.displayBorder }"
|
|
463
|
-
>
|
|
464
|
-
<SampleSelectorSampleRow
|
|
465
|
-
v-for="sample in subGroup.samples"
|
|
466
|
-
:key="sample"
|
|
467
|
-
:sample="sample"
|
|
468
|
-
:selected="modelValue.includes(sample)"
|
|
469
|
-
:dragging="draggingSample === sample"
|
|
470
|
-
:accent-color="subGroup.displayColor"
|
|
471
|
-
checkbox-size="tiny"
|
|
472
|
-
removable
|
|
473
|
-
@dragstart="handleDragStart(sample, subGroup.name, $event)"
|
|
474
|
-
@dragend="handleDragEnd"
|
|
475
|
-
@toggle="toggleSample(sample)"
|
|
476
|
-
@remove="removeSampleFromGroup(sample, subGroup.name)"
|
|
477
|
-
/>
|
|
478
|
-
</div>
|
|
479
|
-
</Transition>
|
|
480
|
-
</div>
|
|
481
|
-
</div>
|
|
482
|
-
</Transition>
|
|
483
|
-
</div>
|
|
484
|
-
</template>
|
|
485
|
-
|
|
486
|
-
<!-- Flat Groups (when no meaningful hierarchy - e.g., level 1) -->
|
|
487
|
-
<template v-else>
|
|
488
|
-
<div
|
|
489
|
-
v-for="group in internalGroups"
|
|
490
|
-
:key="group.name"
|
|
491
|
-
:class="[
|
|
492
|
-
'mint-sample-selector__sub-group',
|
|
493
|
-
dragOverGroup === group.name ? 'mint-sample-selector__sub-group--drag-over' : '',
|
|
494
|
-
]"
|
|
495
|
-
@dragover="handleDragOver(group.name, $event)"
|
|
496
|
-
@dragleave="handleDragLeave"
|
|
497
|
-
@drop="handleDrop(group.name, $event)"
|
|
498
|
-
>
|
|
499
|
-
<!-- Group Header -->
|
|
500
|
-
<div
|
|
501
|
-
:class="[
|
|
502
|
-
'mint-sample-selector__major-header',
|
|
503
|
-
draggingGroup === group.name && draggingGroupKind === 'flat' ? 'mint-sample-selector__header--dragging' : '',
|
|
504
|
-
reorderTarget === group.name && draggingGroupKind === 'flat' && reorderPosition === 'before' ? 'mint-sample-selector__header--drag-over-before' : '',
|
|
505
|
-
reorderTarget === group.name && draggingGroupKind === 'flat' && reorderPosition === 'after' ? 'mint-sample-selector__header--drag-over-after' : '',
|
|
506
|
-
]"
|
|
507
|
-
draggable="true"
|
|
508
|
-
@click="toggleGroupExpanded(group.name)"
|
|
509
|
-
@dragstart="handleGroupDragStart(group.name, 'flat', $event)"
|
|
510
|
-
@dragend="handleGroupDragEnd"
|
|
511
|
-
@dragover="handleGroupDragOver(group.name, 'flat', $event)"
|
|
512
|
-
@dragleave="handleGroupDragLeave($event)"
|
|
513
|
-
@drop="handleGroupDrop(group.name, 'flat', $event)"
|
|
514
|
-
>
|
|
515
|
-
<svg
|
|
516
|
-
:class="[
|
|
517
|
-
'mint-sample-selector__chevron',
|
|
518
|
-
isGroupExpanded(group.name) ? 'mint-sample-selector__chevron--open' : '',
|
|
519
|
-
]"
|
|
520
|
-
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
|
521
|
-
>
|
|
522
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
523
|
-
</svg>
|
|
524
|
-
|
|
525
|
-
<input
|
|
526
|
-
type="checkbox"
|
|
527
|
-
:checked="isGroupFullySelected(group.name)"
|
|
528
|
-
:indeterminate="isGroupPartiallySelected(group.name)"
|
|
529
|
-
class="mint-sample-selector__checkbox"
|
|
530
|
-
:style="{ accentColor: group.color }"
|
|
531
|
-
@click.stop
|
|
532
|
-
@change="toggleGroupSamples(group.name)"
|
|
533
|
-
/>
|
|
534
|
-
|
|
535
|
-
<button
|
|
536
|
-
type="button"
|
|
537
|
-
class="mint-sample-selector__color-dot mint-sample-selector__color-dot--large mint-sample-selector__color-dot--clickable"
|
|
538
|
-
:style="{ backgroundColor: group.color }"
|
|
539
|
-
@click.stop="openColorPicker(group.name, $event)"
|
|
540
|
-
title="Click to change color"
|
|
541
|
-
/>
|
|
542
|
-
|
|
543
|
-
<span class="mint-sample-selector__major-name">{{ group.name }}</span>
|
|
544
|
-
|
|
545
|
-
<span
|
|
546
|
-
class="mint-sample-selector__count-badge"
|
|
547
|
-
:style="{ backgroundColor: group.color + '20', color: group.color }"
|
|
548
|
-
>
|
|
549
|
-
{{ group.samples.length }}
|
|
550
|
-
</span>
|
|
551
|
-
|
|
552
|
-
<button
|
|
553
|
-
type="button"
|
|
554
|
-
class="mint-sample-selector__delete-btn mint-sample-selector__delete-btn--hidden"
|
|
555
|
-
@click.stop="deleteGroup(group.name)"
|
|
556
|
-
title="Remove group"
|
|
557
|
-
>
|
|
558
|
-
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
559
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
560
|
-
</svg>
|
|
561
|
-
</button>
|
|
562
|
-
</div>
|
|
563
|
-
|
|
564
|
-
<!-- Samples (collapsible) -->
|
|
565
|
-
<Transition name="mint-collapse">
|
|
566
|
-
<div
|
|
567
|
-
v-if="isGroupExpanded(group.name)"
|
|
568
|
-
class="mint-sample-selector__samples"
|
|
569
|
-
:style="{ borderColor: group.color + '40' }"
|
|
570
|
-
>
|
|
571
|
-
<SampleSelectorSampleRow
|
|
572
|
-
v-for="sample in group.samples"
|
|
573
|
-
:key="sample"
|
|
574
|
-
:sample="sample"
|
|
575
|
-
:selected="modelValue.includes(sample)"
|
|
576
|
-
:dragging="draggingSample === sample"
|
|
577
|
-
:accent-color="group.color"
|
|
578
|
-
checkbox-size="tiny"
|
|
579
|
-
removable
|
|
580
|
-
@dragstart="handleDragStart(sample, group.name, $event)"
|
|
581
|
-
@dragend="handleDragEnd"
|
|
582
|
-
@toggle="toggleSample(sample)"
|
|
583
|
-
@remove="removeSampleFromGroup(sample, group.name)"
|
|
584
|
-
/>
|
|
585
|
-
</div>
|
|
586
|
-
</Transition>
|
|
587
|
-
</div>
|
|
588
|
-
</template>
|
|
314
|
+
<SampleSelectorGroupNode
|
|
315
|
+
v-for="node in groupTree"
|
|
316
|
+
:key="node.key"
|
|
317
|
+
:node="node"
|
|
318
|
+
:depth="0"
|
|
319
|
+
:ctx="treeContext"
|
|
320
|
+
/>
|
|
589
321
|
|
|
590
322
|
<!-- Empty state -->
|
|
591
323
|
<div v-if="internalGroups.length === 0" class="mint-sample-selector__empty">
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* One row of the sample-selector group hierarchy, rendered recursively so a
|
|
4
|
+
* grouping of any depth (Tissue → Dose → Timepoint → samples) nests properly.
|
|
5
|
+
*
|
|
6
|
+
* Roots carry the family affordances (large dot, colour-family picker) and
|
|
7
|
+
* leaves carry the group affordances (sample drop target, sample rows); the
|
|
8
|
+
* levels in between are pure path nodes that aggregate their descendants.
|
|
9
|
+
*/
|
|
10
|
+
import { computed } from 'vue'
|
|
11
|
+
import SampleSelectorSampleRow from './SampleSelectorSampleRow.vue'
|
|
12
|
+
import type { SampleGroupNode } from '../composables/useSampleGroups'
|
|
13
|
+
import { collectLeafGroupNames, type SampleGroupTreeContext } from './SampleSelector.tree'
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
node: SampleGroupNode
|
|
17
|
+
depth: number
|
|
18
|
+
ctx: SampleGroupTreeContext
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const props = defineProps<Props>()
|
|
22
|
+
|
|
23
|
+
const isRoot = computed(() => props.depth === 0)
|
|
24
|
+
const isLeaf = computed(() => props.node.children.length === 0)
|
|
25
|
+
const expanded = computed(() => props.ctx.isExpanded(props.node.key))
|
|
26
|
+
const leafGroupNames = computed(() => collectLeafGroupNames(props.node))
|
|
27
|
+
/** Roots reorder as a family, leaves as individual groups; path nodes stay put. */
|
|
28
|
+
const dragKind = computed(() => (isRoot.value ? 'major' : isLeaf.value ? 'sub' : null))
|
|
29
|
+
/** Reorder drags address a family by its label but a group by its full name. */
|
|
30
|
+
const dragName = computed(() =>
|
|
31
|
+
isRoot.value ? props.node.label : (props.node.group?.name ?? props.node.label),
|
|
32
|
+
)
|
|
33
|
+
const isDragging = computed(
|
|
34
|
+
() => dragKind.value !== null
|
|
35
|
+
&& props.ctx.draggingGroup() === dragName.value
|
|
36
|
+
&& props.ctx.draggingGroupKind() === dragKind.value,
|
|
37
|
+
)
|
|
38
|
+
const reorderEdge = computed(() => {
|
|
39
|
+
if (dragKind.value === null) return null
|
|
40
|
+
if (props.ctx.reorderTarget() !== dragName.value) return null
|
|
41
|
+
if (props.ctx.draggingGroupKind() !== dragKind.value) return null
|
|
42
|
+
return props.ctx.reorderPosition()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
function onHeaderDragStart(event: DragEvent) {
|
|
46
|
+
if (dragKind.value === null) return
|
|
47
|
+
props.ctx.onGroupDragStart(dragName.value, dragKind.value, event)
|
|
48
|
+
}
|
|
49
|
+
function onHeaderDragOver(event: DragEvent) {
|
|
50
|
+
if (dragKind.value === null) return
|
|
51
|
+
props.ctx.onGroupDragOver(dragName.value, dragKind.value, event)
|
|
52
|
+
}
|
|
53
|
+
function onHeaderDrop(event: DragEvent) {
|
|
54
|
+
if (dragKind.value === null) return
|
|
55
|
+
props.ctx.onGroupDrop(dragName.value, dragKind.value, event)
|
|
56
|
+
}
|
|
57
|
+
</script>
|
|
58
|
+
|
|
59
|
+
<template>
|
|
60
|
+
<div
|
|
61
|
+
:class="[
|
|
62
|
+
isRoot ? 'mint-sample-selector__major-group' : 'mint-sample-selector__sub-group',
|
|
63
|
+
isLeaf && ctx.dragOverGroup() === node.group?.name
|
|
64
|
+
? 'mint-sample-selector__sub-group--drag-over'
|
|
65
|
+
: '',
|
|
66
|
+
]"
|
|
67
|
+
v-on="isLeaf && node.group
|
|
68
|
+
? {
|
|
69
|
+
dragover: (e: DragEvent) => ctx.onSampleDragOver(node.group!.name, e),
|
|
70
|
+
dragleave: ctx.onSampleDragLeave,
|
|
71
|
+
drop: (e: DragEvent) => ctx.onSampleDrop(node.group!.name, e),
|
|
72
|
+
}
|
|
73
|
+
: {}"
|
|
74
|
+
>
|
|
75
|
+
<!-- Header -->
|
|
76
|
+
<div
|
|
77
|
+
:class="[
|
|
78
|
+
isRoot ? 'mint-sample-selector__major-header' : 'mint-sample-selector__sub-header',
|
|
79
|
+
isDragging ? 'mint-sample-selector__header--dragging' : '',
|
|
80
|
+
reorderEdge === 'before' ? 'mint-sample-selector__header--drag-over-before' : '',
|
|
81
|
+
reorderEdge === 'after' ? 'mint-sample-selector__header--drag-over-after' : '',
|
|
82
|
+
]"
|
|
83
|
+
:draggable="dragKind !== null"
|
|
84
|
+
@click="ctx.toggleExpanded(node.key)"
|
|
85
|
+
@dragstart="onHeaderDragStart"
|
|
86
|
+
@dragend="ctx.onGroupDragEnd"
|
|
87
|
+
@dragover="onHeaderDragOver"
|
|
88
|
+
@dragleave="ctx.onGroupDragLeave($event)"
|
|
89
|
+
@drop="onHeaderDrop"
|
|
90
|
+
>
|
|
91
|
+
<svg
|
|
92
|
+
:class="[
|
|
93
|
+
'mint-sample-selector__chevron',
|
|
94
|
+
isRoot ? '' : 'mint-sample-selector__chevron--small',
|
|
95
|
+
expanded ? 'mint-sample-selector__chevron--open' : '',
|
|
96
|
+
]"
|
|
97
|
+
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
|
98
|
+
>
|
|
99
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
|
|
100
|
+
</svg>
|
|
101
|
+
|
|
102
|
+
<input
|
|
103
|
+
type="checkbox"
|
|
104
|
+
:checked="ctx.isFullySelected(node.allSamples)"
|
|
105
|
+
:indeterminate="ctx.isPartiallySelected(node.allSamples)"
|
|
106
|
+
:class="[
|
|
107
|
+
'mint-sample-selector__checkbox',
|
|
108
|
+
isRoot ? '' : 'mint-sample-selector__checkbox--small',
|
|
109
|
+
]"
|
|
110
|
+
:style="{ accentColor: node.color }"
|
|
111
|
+
:aria-label="`Select ${node.label}`"
|
|
112
|
+
@click.stop
|
|
113
|
+
@change="ctx.toggleSamples(node.allSamples)"
|
|
114
|
+
/>
|
|
115
|
+
|
|
116
|
+
<button
|
|
117
|
+
v-if="isRoot"
|
|
118
|
+
type="button"
|
|
119
|
+
class="mint-sample-selector__color-dot mint-sample-selector__color-dot--large mint-sample-selector__color-dot--clickable"
|
|
120
|
+
:style="{ backgroundColor: node.color }"
|
|
121
|
+
:title="`Change ${node.label} color family`"
|
|
122
|
+
:aria-label="`Change ${node.label} color family`"
|
|
123
|
+
@click.stop="ctx.openColorPicker(leafGroupNames, $event)"
|
|
124
|
+
/>
|
|
125
|
+
<div
|
|
126
|
+
v-else
|
|
127
|
+
class="mint-sample-selector__color-dot"
|
|
128
|
+
:style="{ backgroundColor: node.color }"
|
|
129
|
+
aria-hidden="true"
|
|
130
|
+
/>
|
|
131
|
+
|
|
132
|
+
<span
|
|
133
|
+
:class="isRoot ? 'mint-sample-selector__major-name' : 'mint-sample-selector__sub-name'"
|
|
134
|
+
:title="node.group?.name ?? node.label"
|
|
135
|
+
>{{ node.label }}</span>
|
|
136
|
+
|
|
137
|
+
<span
|
|
138
|
+
:class="[
|
|
139
|
+
'mint-sample-selector__count-badge',
|
|
140
|
+
isRoot ? '' : 'mint-sample-selector__count-badge--small',
|
|
141
|
+
]"
|
|
142
|
+
:style="{ backgroundColor: node.displayBg, color: node.color }"
|
|
143
|
+
>
|
|
144
|
+
{{ node.allSamples.length }}
|
|
145
|
+
</span>
|
|
146
|
+
|
|
147
|
+
<button
|
|
148
|
+
type="button"
|
|
149
|
+
class="mint-sample-selector__delete-btn mint-sample-selector__delete-btn--hidden"
|
|
150
|
+
:title="`Remove ${node.label}`"
|
|
151
|
+
:aria-label="`Remove ${node.label}`"
|
|
152
|
+
@click.stop="ctx.removeGroups(leafGroupNames)"
|
|
153
|
+
>
|
|
154
|
+
<svg fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
155
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
|
|
156
|
+
</svg>
|
|
157
|
+
</button>
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<!-- Children: nested nodes, or the sample rows on a leaf -->
|
|
161
|
+
<Transition name="mint-collapse">
|
|
162
|
+
<div
|
|
163
|
+
v-if="expanded && !isLeaf"
|
|
164
|
+
class="mint-sample-selector__sub-groups"
|
|
165
|
+
:style="{ borderColor: node.displayBorder }"
|
|
166
|
+
>
|
|
167
|
+
<SampleSelectorGroupNode
|
|
168
|
+
v-for="child in node.children"
|
|
169
|
+
:key="child.key"
|
|
170
|
+
:node="child"
|
|
171
|
+
:depth="depth + 1"
|
|
172
|
+
:ctx="ctx"
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
<div
|
|
176
|
+
v-else-if="expanded && node.group"
|
|
177
|
+
class="mint-sample-selector__samples"
|
|
178
|
+
:style="{ borderColor: node.displayBorder }"
|
|
179
|
+
>
|
|
180
|
+
<SampleSelectorSampleRow
|
|
181
|
+
v-for="sample in node.group.samples"
|
|
182
|
+
:key="sample"
|
|
183
|
+
:sample="sample"
|
|
184
|
+
:selected="ctx.isSampleSelected(sample)"
|
|
185
|
+
:dragging="ctx.draggingSample() === sample"
|
|
186
|
+
:accent-color="node.color"
|
|
187
|
+
checkbox-size="tiny"
|
|
188
|
+
removable
|
|
189
|
+
@dragstart="(e: DragEvent) => ctx.onSampleDragStart(sample, node.group!.name, e)"
|
|
190
|
+
@dragend="ctx.onSampleDragEnd"
|
|
191
|
+
@toggle="ctx.toggleSample(sample)"
|
|
192
|
+
@remove="ctx.removeSampleFromGroup(sample, node.group!.name)"
|
|
193
|
+
/>
|
|
194
|
+
</div>
|
|
195
|
+
</Transition>
|
|
196
|
+
</div>
|
|
197
|
+
</template>
|
|
@@ -58,6 +58,11 @@ export interface SmartGroupFieldRecipeProps {
|
|
|
58
58
|
// events instead of running its own fixture grouping logic.
|
|
59
59
|
/** Controls the field toggles; when set, clicking a toggle emits `toggle`. */
|
|
60
60
|
enabled?: boolean[]
|
|
61
|
+
/**
|
|
62
|
+
* Per-card 1-based hierarchy layer (1 = the sample selector's major group),
|
|
63
|
+
* `null` for cards not grouped by. When set, reorder clicks emit `move`.
|
|
64
|
+
*/
|
|
65
|
+
layers?: (number | null)[]
|
|
61
66
|
/** Pre-computed resulting groups; when set, the rail renders these directly. */
|
|
62
67
|
groups?: FieldRecipeGroup[]
|
|
63
68
|
/** QC summary chip to show in the rail; `null` hides it (e.g. when mixed). */
|
|
@@ -42,15 +42,19 @@ export interface FieldRecipeSummary {
|
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Build the ordered group list from whichever fields are switched on.
|
|
45
|
-
* Returns an empty list when no field is active.
|
|
45
|
+
* Returns an empty list when no field is active. `layerOrder` (field indices
|
|
46
|
+
* in layer order) overrides the default field-order joining of the group name.
|
|
46
47
|
*/
|
|
47
48
|
export function computeFieldGroups(
|
|
48
49
|
samples: SmartGroupSampleRecord[],
|
|
49
50
|
fields: SmartGroupField[],
|
|
50
51
|
on: boolean[],
|
|
51
52
|
palette: string[],
|
|
53
|
+
layerOrder?: readonly number[],
|
|
52
54
|
): FieldRecipeGroup[] {
|
|
53
|
-
const keys =
|
|
55
|
+
const keys = layerOrder
|
|
56
|
+
? layerOrder.filter(i => on[i]).map(i => fields[i].key)
|
|
57
|
+
: fields.filter((_, i) => on[i]).map(field => field.key)
|
|
54
58
|
if (keys.length === 0) return []
|
|
55
59
|
|
|
56
60
|
const order: string[] = []
|