@morscherlab/mint-sdk 1.0.44 → 1.0.45
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/SmartGroupFieldRecipe.groups.test.d.ts +1 -0
- package/dist/__tests__/components/SmartGroupManual.cohorts.test.d.ts +1 -0
- package/dist/components/AutoGroupModal.adapter.d.ts +42 -0
- package/dist/components/AutoGroupModal.vue.d.ts +1 -105
- package/dist/components/SmartGroup.types.d.ts +71 -0
- package/dist/components/SmartGroupFieldRecipe.groups.d.ts +53 -0
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +37 -0
- package/dist/components/SmartGroupManual.cohorts.d.ts +52 -0
- package/dist/components/SmartGroupManual.vue.d.ts +17 -0
- package/dist/components/SmartGroupModal.vue.d.ts +27 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +2 -2
- package/dist/{components-BGVwavdd.js → components-DjfatNJv.js} +2519 -2335
- package/dist/components-DjfatNJv.js.map +1 -0
- package/dist/composables/index.js +2 -2
- package/dist/{composables-C_hPF0Gn.js → composables-CpBhNKHf.js} +7 -147
- package/dist/composables-CpBhNKHf.js.map +1 -0
- package/dist/index.js +4 -4
- package/dist/install.js +1 -1
- package/dist/styles.css +12711 -12222
- package/dist/{useProtocolTemplates-BbvlHoPD.js → useProtocolTemplates-C8-YlHj1.js} +206 -66
- package/dist/useProtocolTemplates-C8-YlHj1.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/AutoGroupModal.adapter.test.ts +164 -0
- package/src/__tests__/components/SampleSelector.test.ts +176 -16
- package/src/__tests__/components/SmartGroupFieldRecipe.groups.test.ts +96 -0
- package/src/__tests__/components/SmartGroupManual.cohorts.test.ts +97 -0
- package/src/components/AutoGroupModal.adapter.ts +147 -0
- package/src/components/AutoGroupModal.vue +176 -1321
- package/src/components/SampleSelector.vue +8 -23
- package/src/components/SmartGroup.types.ts +93 -0
- package/src/components/SmartGroupFieldRecipe.groups.ts +105 -0
- package/src/components/SmartGroupFieldRecipe.story.vue +58 -0
- package/src/components/SmartGroupFieldRecipe.vue +427 -0
- package/src/components/SmartGroupManual.cohorts.ts +112 -0
- package/src/components/SmartGroupManual.story.vue +55 -0
- package/src/components/SmartGroupManual.vue +398 -0
- package/src/components/SmartGroupModal.story.vue +61 -0
- package/src/components/SmartGroupModal.vue +61 -0
- package/src/components/index.ts +3 -0
- package/src/styles/components/sample-selector.css +1 -5
- package/src/styles/components/smart-group.css +708 -0
- package/src/styles/index.css +1 -1
- package/dist/components-BGVwavdd.js.map +0 -1
- package/dist/composables-C_hPF0Gn.js.map +0 -1
- package/dist/useProtocolTemplates-BbvlHoPD.js.map +0 -1
- package/src/__tests__/components/AutoGroupModal.preview.test.ts +0 -46
- package/src/styles/components/auto-group-modal.css +0 -1336
- /package/dist/__tests__/components/{AutoGroupModal.preview.test.d.ts → AutoGroupModal.adapter.test.d.ts} +0 -0
|
@@ -110,6 +110,7 @@ const showHierarchy = sampleGroups.showHierarchy
|
|
|
110
110
|
|
|
111
111
|
const groupingEnabled = computed(() => internalGroups.value.length > 0)
|
|
112
112
|
const ungroupedSamples = sampleGroups.ungroupedSamples
|
|
113
|
+
const groupingActionMode = computed<'auto' | 'manual'>(() => props.enableSmartGroup ? 'auto' : 'manual')
|
|
113
114
|
|
|
114
115
|
const sampleSearch = useTextSearch({
|
|
115
116
|
items: () => resolvedSamples.value,
|
|
@@ -245,38 +246,22 @@ defineExpose({ handleSmartGroupApply })
|
|
|
245
246
|
|
|
246
247
|
<!-- Action Buttons Row -->
|
|
247
248
|
<div v-if="enableGrouping" class="mint-sample-selector__actions">
|
|
248
|
-
<div
|
|
249
|
-
|
|
250
|
-
'mint-sample-selector__actions-row',
|
|
251
|
-
!enableSmartGroup ? 'mint-sample-selector__actions-row--single-primary' : '',
|
|
252
|
-
]"
|
|
253
|
-
>
|
|
254
|
-
<!-- Smart Button -->
|
|
249
|
+
<div class="mint-sample-selector__actions-row">
|
|
250
|
+
<!-- Grouping Button -->
|
|
255
251
|
<BaseButton
|
|
256
|
-
|
|
257
|
-
:variant="groupingEnabled ? 'primary' : 'secondary'"
|
|
252
|
+
:variant="enableSmartGroup && groupingEnabled ? 'primary' : 'secondary'"
|
|
258
253
|
size="sm"
|
|
259
254
|
:disabled="resolvedSamples.length === 0"
|
|
260
255
|
class="mint-sample-selector__action-btn"
|
|
261
|
-
@click="openGroupingModal(
|
|
256
|
+
@click="openGroupingModal(groupingActionMode)"
|
|
262
257
|
>
|
|
263
|
-
<svg class="mint-sample-selector__action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
258
|
+
<svg v-if="enableSmartGroup" class="mint-sample-selector__action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
264
259
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
|
265
260
|
</svg>
|
|
266
|
-
<
|
|
267
|
-
</BaseButton>
|
|
268
|
-
|
|
269
|
-
<BaseButton
|
|
270
|
-
variant="secondary"
|
|
271
|
-
size="sm"
|
|
272
|
-
:disabled="resolvedSamples.length === 0"
|
|
273
|
-
class="mint-sample-selector__action-btn"
|
|
274
|
-
@click="openGroupingModal('manual')"
|
|
275
|
-
>
|
|
276
|
-
<svg class="mint-sample-selector__action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
261
|
+
<svg v-else class="mint-sample-selector__action-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
277
262
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 3 3 7.5l9 4.5 9-4.5L12 3ZM3 12l9 4.5 9-4.5M3 16.5l9 4.5 9-4.5" />
|
|
278
263
|
</svg>
|
|
279
|
-
<span class="mint-sample-selector__action-label">
|
|
264
|
+
<span class="mint-sample-selector__action-label">Grouping</span>
|
|
280
265
|
</BaseButton>
|
|
281
266
|
|
|
282
267
|
<!-- Reset Button -->
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared prop and payload types for the Smart Group — Full View components
|
|
3
|
+
* (`SmartGroupModal` shell + `SmartGroupFieldRecipe` auto mode +
|
|
4
|
+
* `SmartGroupManual` manual mode).
|
|
5
|
+
*/
|
|
6
|
+
import type {
|
|
7
|
+
FieldRecipeGroup,
|
|
8
|
+
QcRoute,
|
|
9
|
+
SmartGroupField,
|
|
10
|
+
SmartGroupSampleRecord,
|
|
11
|
+
} from './SmartGroupFieldRecipe.groups'
|
|
12
|
+
import type { ManualSample } from './SmartGroupManual.cohorts'
|
|
13
|
+
|
|
14
|
+
export type SmartGroupMode = 'auto' | 'manual'
|
|
15
|
+
|
|
16
|
+
/** A colour swatch in the manual assignment bar. */
|
|
17
|
+
export interface ManualPaletteEntry {
|
|
18
|
+
/** CSS colour value (e.g. a `var(--grp-*)` reference). */
|
|
19
|
+
c: string
|
|
20
|
+
/** Human-readable swatch key for the `aria-label`. */
|
|
21
|
+
k: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Initial cohort assignment for a sample, keyed by sample name. */
|
|
25
|
+
export interface SmartGroupSeed {
|
|
26
|
+
group?: string
|
|
27
|
+
sub?: string
|
|
28
|
+
sub2?: string
|
|
29
|
+
color?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The QC summary chip shown in the rail when QC is not mixed into groups. */
|
|
33
|
+
export interface FieldRecipeQcChip {
|
|
34
|
+
name: string
|
|
35
|
+
count: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface SmartGroupFieldRecipeProps {
|
|
39
|
+
/** Sample records behind the experiment (one row per real sample). */
|
|
40
|
+
samples?: SmartGroupSampleRecord[]
|
|
41
|
+
/** Candidate grouping fields with role + value preview. */
|
|
42
|
+
fields?: SmartGroupField[]
|
|
43
|
+
/** Experiment code shown in the header; omitted from the subtitle when empty. */
|
|
44
|
+
experimentLabel?: string
|
|
45
|
+
/** Sample count shown in the header (real + QC). */
|
|
46
|
+
headerCount?: number
|
|
47
|
+
/** Which fields start switched on; defaults to every non-replicate field. */
|
|
48
|
+
initialOn?: boolean[]
|
|
49
|
+
/** Initial QC routing. */
|
|
50
|
+
initialRoute?: QcRoute
|
|
51
|
+
/** Group colours, cycled in group order. */
|
|
52
|
+
palette?: string[]
|
|
53
|
+
/** Colour used for the QC pool. */
|
|
54
|
+
qcColor?: string
|
|
55
|
+
|
|
56
|
+
// ── Controlled mode (driven by AutoGroupModal over the useAutoGroup engine).
|
|
57
|
+
// When these are provided the component renders the supplied data and emits
|
|
58
|
+
// events instead of running its own fixture grouping logic.
|
|
59
|
+
/** Controls the field toggles; when set, clicking a toggle emits `toggle`. */
|
|
60
|
+
enabled?: boolean[]
|
|
61
|
+
/** Pre-computed resulting groups; when set, the rail renders these directly. */
|
|
62
|
+
groups?: FieldRecipeGroup[]
|
|
63
|
+
/** QC summary chip to show in the rail; `null` hides it (e.g. when mixed). */
|
|
64
|
+
qcChip?: FieldRecipeQcChip | null
|
|
65
|
+
/** Controlled QC routing; when set, changing the select emits `update:route`. */
|
|
66
|
+
route?: QcRoute
|
|
67
|
+
/** QC sample count for the footer hint; defaults to `headerCount - samples`. */
|
|
68
|
+
qcCount?: number
|
|
69
|
+
/** Whether the template-download menu items are enabled (default `true`). */
|
|
70
|
+
canDownloadTemplate?: boolean
|
|
71
|
+
/** Show a loading state in the main body (e.g. while experiment data loads). */
|
|
72
|
+
loading?: boolean
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface FieldRecipeApplyPayload {
|
|
76
|
+
groups: FieldRecipeGroup[]
|
|
77
|
+
route: QcRoute
|
|
78
|
+
sampleTotal: number
|
|
79
|
+
activeFactors: string[]
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SmartGroupManualProps {
|
|
83
|
+
/** Flat list of sample names to group. */
|
|
84
|
+
sampleNames?: string[]
|
|
85
|
+
/** Initial assignments keyed by sample name. */
|
|
86
|
+
seed?: Record<string, SmartGroupSeed>
|
|
87
|
+
/** Colour swatches offered in the assignment bar. */
|
|
88
|
+
palette?: ManualPaletteEntry[]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ManualDonePayload {
|
|
92
|
+
samples: ManualSample[]
|
|
93
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure grouping logic for SmartGroupFieldRecipe (the "auto" / Field Recipe mode).
|
|
3
|
+
*
|
|
4
|
+
* Mirrors the design prototype's live grouping engine: groups are formed by
|
|
5
|
+
* joining the values of whichever fields are switched on, in field order.
|
|
6
|
+
* Kept framework-free so it is unit-testable in isolation from the component.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** A field/factor the user can group by. */
|
|
10
|
+
export interface SmartGroupField {
|
|
11
|
+
/** Column key as it appears in each sample record. */
|
|
12
|
+
key: string
|
|
13
|
+
/** Display role, e.g. `Factor` or `Replicate`. */
|
|
14
|
+
role: string
|
|
15
|
+
/** Comma-separated preview of the field's distinct values. */
|
|
16
|
+
vals: string
|
|
17
|
+
/** Whether this field is a replicate dimension (grouping by it shatters groups). */
|
|
18
|
+
rep: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A single sample as a flat key → value record. */
|
|
22
|
+
export type SmartGroupSampleRecord = Record<string, string>
|
|
23
|
+
|
|
24
|
+
/** A resolved group in the live preview rail. */
|
|
25
|
+
export interface FieldRecipeGroup {
|
|
26
|
+
name: string
|
|
27
|
+
count: number
|
|
28
|
+
color: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** How QC samples are routed relative to the biological groups. */
|
|
32
|
+
export type QcRoute = 'Overlay only' | 'Exclude' | 'Mix into groups'
|
|
33
|
+
|
|
34
|
+
/** Kind of inline summary callout to render. */
|
|
35
|
+
export type FieldRecipeSummaryKind = 'mute' | 'warn' | 'info'
|
|
36
|
+
|
|
37
|
+
export interface FieldRecipeSummary {
|
|
38
|
+
kind: FieldRecipeSummaryKind
|
|
39
|
+
/** Number of base (non-QC) groups the current toggles produce. */
|
|
40
|
+
groupCount: number
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build the ordered group list from whichever fields are switched on.
|
|
45
|
+
* Returns an empty list when no field is active.
|
|
46
|
+
*/
|
|
47
|
+
export function computeFieldGroups(
|
|
48
|
+
samples: SmartGroupSampleRecord[],
|
|
49
|
+
fields: SmartGroupField[],
|
|
50
|
+
on: boolean[],
|
|
51
|
+
palette: string[],
|
|
52
|
+
): FieldRecipeGroup[] {
|
|
53
|
+
const keys = fields.filter((_, i) => on[i]).map(field => field.key)
|
|
54
|
+
if (keys.length === 0) return []
|
|
55
|
+
|
|
56
|
+
const order: string[] = []
|
|
57
|
+
const counts = new Map<string, number>()
|
|
58
|
+
for (const sample of samples) {
|
|
59
|
+
const name = keys.map(key => sample[key]).join('_')
|
|
60
|
+
if (!counts.has(name)) {
|
|
61
|
+
counts.set(name, 0)
|
|
62
|
+
order.push(name)
|
|
63
|
+
}
|
|
64
|
+
counts.set(name, (counts.get(name) ?? 0) + 1)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return order.map((name, index) => ({
|
|
68
|
+
name,
|
|
69
|
+
count: counts.get(name) ?? 0,
|
|
70
|
+
color: palette[index % palette.length],
|
|
71
|
+
}))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Append the QC pool group when QC is mixed into the biological groups.
|
|
76
|
+
* `Overlay only` and `Exclude` leave the group list untouched.
|
|
77
|
+
*/
|
|
78
|
+
export function appendQcGroup(
|
|
79
|
+
groups: FieldRecipeGroup[],
|
|
80
|
+
route: QcRoute,
|
|
81
|
+
qcColor: string,
|
|
82
|
+
qcCount = 3,
|
|
83
|
+
): FieldRecipeGroup[] {
|
|
84
|
+
if (route === 'Mix into groups') {
|
|
85
|
+
return [...groups, { name: 'QC_Pool', count: qcCount, color: qcColor }]
|
|
86
|
+
}
|
|
87
|
+
return groups
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Total samples the grouping applies to: QC is dropped only when excluded. */
|
|
91
|
+
export function sampleTotalFor(route: QcRoute, realCount = 9, qcCount = 3): number {
|
|
92
|
+
return route === 'Exclude' ? realCount : realCount + qcCount
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Decide which inline summary callout the Field Recipe should show. */
|
|
96
|
+
export function fieldRecipeSummary(input: {
|
|
97
|
+
activeFactors: string[]
|
|
98
|
+
repOn: boolean
|
|
99
|
+
baseGroups: FieldRecipeGroup[]
|
|
100
|
+
}): FieldRecipeSummary {
|
|
101
|
+
const groupCount = input.baseGroups.length
|
|
102
|
+
if (input.activeFactors.length === 0) return { kind: 'mute', groupCount }
|
|
103
|
+
if (input.repOn) return { kind: 'warn', groupCount }
|
|
104
|
+
return { kind: 'info', groupCount }
|
|
105
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import SmartGroupFieldRecipe from './SmartGroupFieldRecipe.vue'
|
|
3
|
+
import type { FieldRecipeApplyPayload } from './SmartGroup.types'
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<template>
|
|
7
|
+
<Story title="Lab/SmartGroupFieldRecipe">
|
|
8
|
+
<Variant title="Playground" :init-state="() => ({ log: '' })">
|
|
9
|
+
<template #default="{ state }">
|
|
10
|
+
<div class="sg-story">
|
|
11
|
+
<div class="sg-story__stage">
|
|
12
|
+
<SmartGroupFieldRecipe
|
|
13
|
+
@apply="(r: FieldRecipeApplyPayload) => (state.log = `${r.groups.length} groups · ${r.route} · ${r.sampleTotal} samples`)"
|
|
14
|
+
@manual="state.log = 'switch to manual grouping'"
|
|
15
|
+
@cancel="state.log = 'cancelled'"
|
|
16
|
+
@close="state.log = 'closed'"
|
|
17
|
+
@csv-file="(f: File) => (state.log = `csv selected: ${f.name}`)"
|
|
18
|
+
@download-template="state.log = 'download blank template'"
|
|
19
|
+
@download-prefilled="state.log = 'download pre-filled template'"
|
|
20
|
+
/>
|
|
21
|
+
</div>
|
|
22
|
+
<p v-if="state.log" class="sg-story__log">{{ state.log }}</p>
|
|
23
|
+
</div>
|
|
24
|
+
</template>
|
|
25
|
+
</Variant>
|
|
26
|
+
|
|
27
|
+
<Variant title="Replicate singleton trap">
|
|
28
|
+
<template #default>
|
|
29
|
+
<div class="sg-story">
|
|
30
|
+
<div class="sg-story__stage">
|
|
31
|
+
<SmartGroupFieldRecipe :initial-on="[true, true, true]" />
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
</Variant>
|
|
36
|
+
</Story>
|
|
37
|
+
</template>
|
|
38
|
+
|
|
39
|
+
<style scoped>
|
|
40
|
+
.sg-story {
|
|
41
|
+
padding: 2rem;
|
|
42
|
+
display: flex;
|
|
43
|
+
flex-direction: column;
|
|
44
|
+
gap: 1rem;
|
|
45
|
+
align-items: flex-start;
|
|
46
|
+
}
|
|
47
|
+
.sg-story__stage {
|
|
48
|
+
width: 980px;
|
|
49
|
+
height: 640px;
|
|
50
|
+
max-width: 100%;
|
|
51
|
+
}
|
|
52
|
+
.sg-story__log {
|
|
53
|
+
margin: 0;
|
|
54
|
+
font-family: 'Fira Code', monospace;
|
|
55
|
+
font-size: 0.8125rem;
|
|
56
|
+
color: var(--text-secondary, #64748b);
|
|
57
|
+
}
|
|
58
|
+
</style>
|