@morscherlab/mint-sdk 1.0.0-beta.7 → 1.0.0-rc.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/README.md +9 -1
- package/dist/__tests__/components/LcmsSequenceTable.test.d.ts +1 -0
- package/dist/__tests__/components/ProgressBar.test.d.ts +1 -0
- package/dist/__tests__/components/RackEditor.test.d.ts +1 -0
- package/dist/__tests__/components/SequenceProgressBar.test.d.ts +1 -0
- package/dist/__tests__/composables/useExperimentSamples.test.d.ts +1 -0
- package/dist/__tests__/composables/useProtocolTemplates.test.d.ts +1 -0
- package/dist/__tests__/stores/settings.test.d.ts +1 -0
- package/dist/__tests__/utils/instrument.test.d.ts +1 -0
- package/dist/__tests__/utils/lcms.test.d.ts +1 -0
- package/dist/__tests__/utils/permissions.test.d.ts +1 -0
- package/dist/__tests__/utils/rack.test.d.ts +1 -0
- package/dist/{auth-QQj2kkze.js → auth-B7g4J4ZF.js} +148 -24
- package/dist/auth-B7g4J4ZF.js.map +1 -0
- package/dist/components/AutoGroupModal.vue.d.ts +1 -1
- package/dist/components/BaseCheckbox.vue.d.ts +1 -1
- package/dist/components/BaseToggle.vue.d.ts +2 -2
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
- package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +1 -1
- package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +1 -1
- package/dist/components/DoseDesignWorkspaceView.vue.d.ts +1 -1
- package/dist/components/FormulaInput.vue.d.ts +1 -1
- package/dist/components/InstrumentAlertLog.vue.d.ts +22 -0
- package/dist/components/InstrumentStateBadge.vue.d.ts +11 -0
- package/dist/components/InstrumentStatusCard.vue.d.ts +13 -0
- package/dist/components/LcmsSequenceTable.vue.d.ts +26 -0
- package/dist/components/ProgressBar.vue.d.ts +1 -0
- package/dist/components/RackEditor.vue.d.ts +41 -3
- package/dist/components/ReagentList.vue.d.ts +1 -1
- package/dist/components/SampleSelector.vue.d.ts +5 -2
- package/dist/components/SegmentedControl.vue.d.ts +2 -0
- package/dist/components/SequenceInput.vue.d.ts +1 -1
- package/dist/components/SequenceProgressBar.vue.d.ts +15 -0
- package/dist/components/SettingsModal.vue.d.ts +8 -1
- package/dist/components/TagsInput.vue.d.ts +1 -1
- package/dist/components/WellPlate.vue.d.ts +42 -3
- package/dist/components/index.d.ts +5 -0
- package/dist/components/index.js +3 -3
- package/dist/{components-DihbSJjU.js → components-BhK-dW99.js} +2135 -1075
- package/dist/components-BhK-dW99.js.map +1 -0
- package/dist/composables/experimentDesignData.d.ts +17 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/index.js +4 -4
- package/dist/composables/useControlSchema.d.ts +11 -0
- package/dist/composables/useExperimentData.d.ts +11 -3
- package/dist/composables/useExperimentSamples.d.ts +42 -0
- package/dist/composables/usePlatformContext.d.ts +54 -0
- package/dist/{composables-BcgZ6diz.js → composables-Bg7CFuNz.js} +5 -3
- package/dist/composables-Bg7CFuNz.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +168 -6
- package/dist/index.js.map +1 -0
- package/dist/install.js +2 -2
- package/dist/instrument.d.ts +7 -0
- package/dist/lcms.d.ts +27 -0
- package/dist/permissions.d.ts +46 -0
- package/dist/stores/auth.d.ts +74 -2
- package/dist/stores/index.js +1 -1
- package/dist/styles.css +3186 -1070
- package/dist/templates/builders.d.ts +7 -3
- package/dist/templates/index.d.ts +2 -2
- package/dist/templates/index.js +2 -2
- package/dist/templates/presets.d.ts +12 -0
- package/dist/templates/types.d.ts +16 -1
- package/dist/{templates-Cyt0Suwf.js → templates-BorLR_7p.js} +324 -10
- package/dist/templates-BorLR_7p.js.map +1 -0
- package/dist/types/auth.d.ts +2 -0
- package/dist/types/components.d.ts +32 -3
- package/dist/types/form-builder.d.ts +2 -1
- package/dist/types/index.d.ts +4 -1
- package/dist/types/instrument.d.ts +56 -0
- package/dist/types/platform.d.ts +3 -0
- package/dist/{useExperimentData-CM6Y0u5L.js → useProtocolTemplates-n6AJqSqv.js} +627 -380
- package/dist/useProtocolTemplates-n6AJqSqv.js.map +1 -0
- package/dist/utils/rack.d.ts +47 -0
- package/package.json +1 -1
- package/src/__tests__/components/AppTopBar.test.ts +15 -0
- package/src/__tests__/components/BaseTabs.test.ts +15 -0
- package/src/__tests__/components/GroupAssigner.test.ts +18 -0
- package/src/__tests__/components/LcmsSequenceTable.test.ts +57 -0
- package/src/__tests__/components/ProgressBar.test.ts +18 -0
- package/src/__tests__/components/RackEditor.test.ts +125 -0
- package/src/__tests__/components/SampleSelector.test.ts +25 -0
- package/src/__tests__/components/SegmentedControl.test.ts +45 -0
- package/src/__tests__/components/SequenceProgressBar.test.ts +39 -0
- package/src/__tests__/components/SettingsModal.test.ts +83 -2
- package/src/__tests__/composables/useApi.test.ts +45 -0
- package/src/__tests__/composables/useAuth.test.ts +20 -0
- package/src/__tests__/composables/useControlSchema.test.ts +4 -0
- package/src/__tests__/composables/useExperimentData.test.ts +23 -0
- package/src/__tests__/composables/useExperimentSamples.test.ts +91 -0
- package/src/__tests__/composables/useProtocolTemplates.test.ts +64 -0
- package/src/__tests__/stores/settings.test.ts +78 -0
- package/src/__tests__/templates/templates.test.ts +86 -0
- package/src/__tests__/utils/instrument.test.ts +47 -0
- package/src/__tests__/utils/lcms.test.ts +73 -0
- package/src/__tests__/utils/permissions.test.ts +50 -0
- package/src/__tests__/utils/rack.test.ts +120 -0
- package/src/components/AppAvatarMenu.vue +6 -3
- package/src/components/AppTopBar.vue +16 -10
- package/src/components/AuditTrail.vue +1 -1
- package/src/components/BaseTabs.vue +22 -1
- package/src/components/Calendar.vue +6 -2
- package/src/components/ConcentrationInput.vue +3 -2
- package/src/components/GroupAssigner.vue +8 -3
- package/src/components/InstrumentAlertLog.vue +191 -0
- package/src/components/InstrumentStateBadge.vue +50 -0
- package/src/components/InstrumentStatusCard.vue +188 -0
- package/src/components/LcmsSequenceTable.vue +191 -0
- package/src/components/NumberInput.vue +5 -3
- package/src/components/ProgressBar.vue +3 -0
- package/src/components/RackEditor.vue +73 -2
- package/src/components/SampleHierarchyTree.vue +3 -2
- package/src/components/SampleSelector.vue +28 -9
- package/src/components/SegmentedControl.story.vue +17 -0
- package/src/components/SegmentedControl.vue +14 -3
- package/src/components/SequenceProgressBar.vue +71 -0
- package/src/components/SettingsModal.vue +49 -2
- package/src/components/UnitInput.vue +6 -2
- package/src/components/WellPlate.vue +145 -24
- package/src/components/index.ts +5 -0
- package/src/components/internal/WellEditPopupInternal.vue +1 -0
- package/src/composables/experimentDesignData.ts +182 -0
- package/src/composables/index.ts +14 -0
- package/src/composables/useApi.ts +113 -16
- package/src/composables/useAuth.ts +4 -0
- package/src/composables/useAutoGroup.ts +18 -9
- package/src/composables/useControlSchema.ts +21 -0
- package/src/composables/useExperimentData.ts +57 -16
- package/src/composables/useExperimentSamples.ts +142 -0
- package/src/composables/useProtocolTemplates.ts +13 -1
- package/src/composables/useRackEditor.ts +3 -2
- package/src/index.ts +27 -0
- package/src/instrument.ts +90 -0
- package/src/lcms.ts +108 -0
- package/src/permissions.ts +143 -0
- package/src/stores/auth.ts +79 -26
- package/src/stores/settings.ts +10 -0
- package/src/styles/components/instrument-monitor.css +478 -0
- package/src/styles/components/lcms-sequence-table.css +189 -0
- package/src/styles/components/sequence-progress-bar.css +63 -0
- package/src/styles/components/settings-modal.css +9 -0
- package/src/styles/components/tabs.css +9 -0
- package/src/styles/components/well-edit-popup.css +7 -1
- package/src/styles/components/well-plate.css +5 -0
- package/src/styles/index.css +3 -0
- package/src/templates/builders.ts +201 -0
- package/src/templates/controlSchemas.ts +68 -0
- package/src/templates/index.ts +2 -0
- package/src/templates/presets.ts +23 -0
- package/src/templates/types.ts +17 -0
- package/src/types/auth.ts +3 -0
- package/src/types/components.ts +45 -3
- package/src/types/form-builder.ts +2 -1
- package/src/types/index.ts +35 -0
- package/src/types/instrument.ts +61 -0
- package/src/types/platform.ts +4 -0
- package/src/utils/rack.ts +209 -0
- package/dist/auth-QQj2kkze.js.map +0 -1
- package/dist/components-DihbSJjU.js.map +0 -1
- package/dist/composables-BcgZ6diz.js.map +0 -1
- package/dist/templates-Cyt0Suwf.js.map +0 -1
- package/dist/useExperimentData-CM6Y0u5L.js.map +0 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** Compact card for live instrument monitor status, sample, method, sequence progress, and ETA. */
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import {
|
|
5
|
+
estimateSequenceFinishDate,
|
|
6
|
+
formatSequenceRemaining,
|
|
7
|
+
estimateSequenceRemainingSeconds,
|
|
8
|
+
} from '../instrument'
|
|
9
|
+
import type { InstrumentState, InstrumentStatus, SequenceProgress } from '../types'
|
|
10
|
+
import InstrumentStateBadge from './InstrumentStateBadge.vue'
|
|
11
|
+
import SequenceProgressBar from './SequenceProgressBar.vue'
|
|
12
|
+
|
|
13
|
+
interface Props {
|
|
14
|
+
status: InstrumentStatus
|
|
15
|
+
name?: string
|
|
16
|
+
showPlaceholders?: boolean
|
|
17
|
+
locale?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
21
|
+
name: undefined,
|
|
22
|
+
showPlaceholders: true,
|
|
23
|
+
locale: undefined,
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const displayName = computed(() =>
|
|
27
|
+
props.name
|
|
28
|
+
?? props.status.instrument_name
|
|
29
|
+
?? props.status.instrument_id
|
|
30
|
+
)
|
|
31
|
+
const isRunning = computed(() => props.status.state === 'running')
|
|
32
|
+
const sampleLabel = computed(() => {
|
|
33
|
+
const sample = props.status.current_sample
|
|
34
|
+
return sample ? (sample.sample_name ?? sample.sample_id ?? sample.file_name) : null
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const etaDisplay = computed(() => {
|
|
38
|
+
const progress = props.status.sequence_progress
|
|
39
|
+
if (!progress) return null
|
|
40
|
+
const finish = estimateSequenceFinishDate(progress)
|
|
41
|
+
if (!finish) return null
|
|
42
|
+
|
|
43
|
+
const now = new Date()
|
|
44
|
+
const sameDay =
|
|
45
|
+
finish.getFullYear() === now.getFullYear()
|
|
46
|
+
&& finish.getMonth() === now.getMonth()
|
|
47
|
+
&& finish.getDate() === now.getDate()
|
|
48
|
+
|
|
49
|
+
const time = finish.toLocaleTimeString(props.locale, { hour: 'numeric', minute: '2-digit' })
|
|
50
|
+
if (sameDay) return time
|
|
51
|
+
|
|
52
|
+
const sameYear = finish.getFullYear() === now.getFullYear()
|
|
53
|
+
const date = finish.toLocaleDateString(props.locale, {
|
|
54
|
+
month: 'short',
|
|
55
|
+
day: 'numeric',
|
|
56
|
+
...(sameYear ? {} : { year: 'numeric' }),
|
|
57
|
+
})
|
|
58
|
+
return `${date}, ${time}`
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
const remainingLabel = computed(() => {
|
|
62
|
+
const progress = props.status.sequence_progress
|
|
63
|
+
if (!progress) return null
|
|
64
|
+
return formatSequenceRemaining(progress)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
const lastSeenAgo = computed(() => {
|
|
68
|
+
const value = props.status.last_seen ?? props.status.timestamp
|
|
69
|
+
if (!value) return null
|
|
70
|
+
return formatRelativeAge(value)
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const stateClass = computed(() => `mint-instrument-card--${props.status.state}`)
|
|
74
|
+
|
|
75
|
+
function stateTone(state: InstrumentState | string): string {
|
|
76
|
+
if (state === 'running') return 'success'
|
|
77
|
+
if (state === 'standby') return 'warning'
|
|
78
|
+
if (state === 'error') return 'error'
|
|
79
|
+
if (state === 'connected') return 'info'
|
|
80
|
+
return 'muted'
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function formatRelativeAge(value: string | Date): string | null {
|
|
84
|
+
const date = value instanceof Date ? value : new Date(value)
|
|
85
|
+
if (Number.isNaN(date.getTime())) return null
|
|
86
|
+
const minutes = Math.floor((Date.now() - date.getTime()) / 60_000)
|
|
87
|
+
if (minutes < 1) return 'just now'
|
|
88
|
+
if (minutes < 60) return `${minutes}m ago`
|
|
89
|
+
const hours = Math.floor(minutes / 60)
|
|
90
|
+
if (hours < 24) return `${hours}h ago`
|
|
91
|
+
const days = Math.floor(hours / 24)
|
|
92
|
+
return days === 1 ? '1 day ago' : `${days} days ago`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function hasRemaining(progress: SequenceProgress | null | undefined): boolean {
|
|
96
|
+
const remaining = estimateSequenceRemainingSeconds(progress)
|
|
97
|
+
return remaining !== null && remaining > 0
|
|
98
|
+
}
|
|
99
|
+
</script>
|
|
100
|
+
|
|
101
|
+
<template>
|
|
102
|
+
<div class="mint-instrument-card" :class="stateClass">
|
|
103
|
+
<div class="mint-instrument-card__header">
|
|
104
|
+
<span
|
|
105
|
+
class="mint-instrument-card__dot"
|
|
106
|
+
:class="`mint-instrument-card__dot--${stateTone(status.state)}`"
|
|
107
|
+
/>
|
|
108
|
+
<strong class="mint-instrument-card__name">{{ displayName }}</strong>
|
|
109
|
+
<InstrumentStateBadge :state="status.state" />
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<template v-if="isRunning">
|
|
113
|
+
<div v-if="status.current_sample" class="mint-instrument-card__sample">
|
|
114
|
+
<span class="mint-instrument-card__sample-label">Currently Running</span>
|
|
115
|
+
<div class="mint-instrument-card__sample-row">
|
|
116
|
+
<span class="mint-instrument-card__sample-dot" />
|
|
117
|
+
<span class="mint-instrument-card__sample-name">{{ sampleLabel }}</span>
|
|
118
|
+
<span
|
|
119
|
+
v-if="status.current_sample.vial_position"
|
|
120
|
+
class="mint-instrument-card__sample-vial"
|
|
121
|
+
>
|
|
122
|
+
{{ status.current_sample.vial_position }}
|
|
123
|
+
</span>
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
|
|
127
|
+
<span
|
|
128
|
+
v-if="status.active_method"
|
|
129
|
+
class="mint-instrument-card__method"
|
|
130
|
+
:title="status.active_method"
|
|
131
|
+
>
|
|
132
|
+
{{ status.active_method }}
|
|
133
|
+
</span>
|
|
134
|
+
|
|
135
|
+
<SequenceProgressBar
|
|
136
|
+
v-if="status.sequence_progress"
|
|
137
|
+
:progress="status.sequence_progress"
|
|
138
|
+
compact
|
|
139
|
+
/>
|
|
140
|
+
|
|
141
|
+
<div
|
|
142
|
+
v-if="etaDisplay || hasRemaining(status.sequence_progress)"
|
|
143
|
+
class="mint-instrument-card__eta"
|
|
144
|
+
>
|
|
145
|
+
<span class="mint-instrument-card__eta-label">ETA</span>
|
|
146
|
+
<span v-if="etaDisplay" class="mint-instrument-card__eta-time">{{ etaDisplay }}</span>
|
|
147
|
+
<span v-if="remainingLabel" class="mint-instrument-card__eta-remaining">
|
|
148
|
+
{{ remainingLabel }}
|
|
149
|
+
</span>
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<span
|
|
153
|
+
v-if="status.sequence_progress?.sequence_name"
|
|
154
|
+
class="mint-instrument-card__sequence"
|
|
155
|
+
:title="status.sequence_progress.sequence_name"
|
|
156
|
+
>
|
|
157
|
+
{{ status.sequence_progress.sequence_name }}
|
|
158
|
+
</span>
|
|
159
|
+
</template>
|
|
160
|
+
|
|
161
|
+
<template v-else-if="status.state === 'error'">
|
|
162
|
+
<div class="mint-instrument-card__error">
|
|
163
|
+
<div class="mint-instrument-card__error-message">
|
|
164
|
+
{{ status.active_method ?? 'Instrument error' }}
|
|
165
|
+
</div>
|
|
166
|
+
<div v-if="lastSeenAgo" class="mint-instrument-card__error-detail">
|
|
167
|
+
Last seen {{ lastSeenAgo }}
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</template>
|
|
171
|
+
|
|
172
|
+
<template v-else-if="showPlaceholders">
|
|
173
|
+
<div class="mint-instrument-card__placeholder">
|
|
174
|
+
<span class="mint-instrument-card__muted">No sample running</span>
|
|
175
|
+
<span class="mint-instrument-card__muted">No method</span>
|
|
176
|
+
<SequenceProgressBar
|
|
177
|
+
:progress="{ current_sample: 0, total_samples: 0 }"
|
|
178
|
+
compact
|
|
179
|
+
/>
|
|
180
|
+
<span class="mint-instrument-card__muted">No sequence</span>
|
|
181
|
+
</div>
|
|
182
|
+
</template>
|
|
183
|
+
</div>
|
|
184
|
+
</template>
|
|
185
|
+
|
|
186
|
+
<style>
|
|
187
|
+
@import '../styles/components/instrument-monitor.css';
|
|
188
|
+
</style>
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** Table for Xcalibur-compatible LCMS sequence rows with optional reorder/remove/duplicate controls. */
|
|
3
|
+
import { computed, ref } from 'vue'
|
|
4
|
+
import {
|
|
5
|
+
basenameFromWindowsPath,
|
|
6
|
+
DEFAULT_LCMS_SEQUENCE_COLUMNS,
|
|
7
|
+
type LcmsSequenceItem,
|
|
8
|
+
type LcmsSequenceTableColumn,
|
|
9
|
+
} from '../lcms'
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
items?: LcmsSequenceItem[]
|
|
13
|
+
columns?: LcmsSequenceTableColumn[]
|
|
14
|
+
editable?: boolean
|
|
15
|
+
maxRows?: number
|
|
16
|
+
showMoreLabel?: boolean
|
|
17
|
+
emptyMessage?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
21
|
+
items: () => [],
|
|
22
|
+
columns: () => DEFAULT_LCMS_SEQUENCE_COLUMNS,
|
|
23
|
+
editable: false,
|
|
24
|
+
maxRows: undefined,
|
|
25
|
+
showMoreLabel: true,
|
|
26
|
+
emptyMessage: 'No sequence items to display',
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const emit = defineEmits<{
|
|
30
|
+
reorder: [fromIndex: number, toIndex: number]
|
|
31
|
+
remove: [index: number]
|
|
32
|
+
duplicate: [index: number]
|
|
33
|
+
}>()
|
|
34
|
+
|
|
35
|
+
const dragIndex = ref<number | null>(null)
|
|
36
|
+
const dropTargetIndex = ref<number | null>(null)
|
|
37
|
+
|
|
38
|
+
const displayItems = computed(() => {
|
|
39
|
+
if (!props.maxRows) return props.items
|
|
40
|
+
return props.items.slice(0, props.maxRows)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
const hasMore = computed(() =>
|
|
44
|
+
props.maxRows != null && props.items.length > props.maxRows
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
function sampleTypeClass(type: string): string {
|
|
48
|
+
const normalized = type.toLowerCase()
|
|
49
|
+
if (normalized === 'blank') return 'mint-lcms-sequence-table__type--blank'
|
|
50
|
+
if (normalized === 'qc' || normalized === 'iqc' || normalized === 'eqc') {
|
|
51
|
+
return 'mint-lcms-sequence-table__type--qc'
|
|
52
|
+
}
|
|
53
|
+
return 'mint-lcms-sequence-table__type--sample'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function cellValue(item: LcmsSequenceItem, column: LcmsSequenceTableColumn, index: number): string | number {
|
|
57
|
+
if (column.key === 'index') return index + 1
|
|
58
|
+
if (column.key === 'actions') return ''
|
|
59
|
+
if (column.key === 'instrument_method') return basenameFromWindowsPath(item.instrument_method)
|
|
60
|
+
return item[column.key] ?? ''
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function onDragStart(event: DragEvent, index: number) {
|
|
64
|
+
if (!props.editable) return
|
|
65
|
+
dragIndex.value = index
|
|
66
|
+
if (event.dataTransfer) {
|
|
67
|
+
event.dataTransfer.effectAllowed = 'move'
|
|
68
|
+
event.dataTransfer.setData('text/plain', String(index))
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function onDragOver(event: DragEvent, index: number) {
|
|
73
|
+
if (!props.editable) return
|
|
74
|
+
event.preventDefault()
|
|
75
|
+
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'
|
|
76
|
+
if (dragIndex.value !== null && dragIndex.value !== index) {
|
|
77
|
+
dropTargetIndex.value = index
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function clearDragState() {
|
|
82
|
+
dragIndex.value = null
|
|
83
|
+
dropTargetIndex.value = null
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function onDrop(event: DragEvent, toIndex: number) {
|
|
87
|
+
if (!props.editable) return
|
|
88
|
+
event.preventDefault()
|
|
89
|
+
if (dragIndex.value !== null && dragIndex.value !== toIndex) {
|
|
90
|
+
emit('reorder', dragIndex.value, toIndex)
|
|
91
|
+
}
|
|
92
|
+
clearDragState()
|
|
93
|
+
}
|
|
94
|
+
</script>
|
|
95
|
+
|
|
96
|
+
<template>
|
|
97
|
+
<div class="mint-lcms-sequence-table">
|
|
98
|
+
<div class="mint-lcms-sequence-table__scroller">
|
|
99
|
+
<table class="mint-lcms-sequence-table__table">
|
|
100
|
+
<thead>
|
|
101
|
+
<tr>
|
|
102
|
+
<th v-if="editable" class="mint-lcms-sequence-table__drag-header" />
|
|
103
|
+
<th
|
|
104
|
+
v-for="column in columns"
|
|
105
|
+
:key="column.key"
|
|
106
|
+
:class="[
|
|
107
|
+
'mint-lcms-sequence-table__th',
|
|
108
|
+
{ 'mint-lcms-sequence-table__th--right': column.align === 'right' },
|
|
109
|
+
]"
|
|
110
|
+
>
|
|
111
|
+
{{ column.label }}
|
|
112
|
+
</th>
|
|
113
|
+
<th v-if="editable" class="mint-lcms-sequence-table__actions-header" />
|
|
114
|
+
</tr>
|
|
115
|
+
</thead>
|
|
116
|
+
<tbody>
|
|
117
|
+
<tr
|
|
118
|
+
v-for="(item, index) in displayItems"
|
|
119
|
+
:key="`${item.file_name}-${item.position}-${index}`"
|
|
120
|
+
:draggable="editable"
|
|
121
|
+
class="mint-lcms-sequence-table__row"
|
|
122
|
+
:class="{
|
|
123
|
+
'mint-lcms-sequence-table__row--dragging': dragIndex === index,
|
|
124
|
+
'mint-lcms-sequence-table__row--drop-above': dropTargetIndex === index && dragIndex !== null && dragIndex > index,
|
|
125
|
+
'mint-lcms-sequence-table__row--drop-below': dropTargetIndex === index && dragIndex !== null && dragIndex < index,
|
|
126
|
+
}"
|
|
127
|
+
@dragstart="onDragStart($event, index)"
|
|
128
|
+
@dragenter.prevent
|
|
129
|
+
@dragover="onDragOver($event, index)"
|
|
130
|
+
@dragleave="dropTargetIndex = null"
|
|
131
|
+
@drop="onDrop($event, index)"
|
|
132
|
+
@dragend="clearDragState"
|
|
133
|
+
>
|
|
134
|
+
<td v-if="editable" class="mint-lcms-sequence-table__drag-cell">
|
|
135
|
+
<span class="mint-lcms-sequence-table__drag-handle" aria-hidden="true">::</span>
|
|
136
|
+
</td>
|
|
137
|
+
<td
|
|
138
|
+
v-for="column in columns"
|
|
139
|
+
:key="column.key"
|
|
140
|
+
:class="[
|
|
141
|
+
'mint-lcms-sequence-table__td',
|
|
142
|
+
`mint-lcms-sequence-table__td--${column.key}`,
|
|
143
|
+
{ 'mint-lcms-sequence-table__td--right': column.align === 'right' },
|
|
144
|
+
]"
|
|
145
|
+
:title="column.key === 'instrument_method' ? item.instrument_method : undefined"
|
|
146
|
+
>
|
|
147
|
+
<span
|
|
148
|
+
v-if="column.key === 'sample_type'"
|
|
149
|
+
class="mint-lcms-sequence-table__type"
|
|
150
|
+
:class="sampleTypeClass(item.sample_type)"
|
|
151
|
+
>
|
|
152
|
+
{{ item.sample_type }}
|
|
153
|
+
</span>
|
|
154
|
+
<template v-else>{{ cellValue(item, column, index) }}</template>
|
|
155
|
+
</td>
|
|
156
|
+
<td v-if="editable" class="mint-lcms-sequence-table__actions">
|
|
157
|
+
<button
|
|
158
|
+
type="button"
|
|
159
|
+
class="mint-lcms-sequence-table__action"
|
|
160
|
+
title="Duplicate"
|
|
161
|
+
@click="emit('duplicate', index)"
|
|
162
|
+
>
|
|
163
|
+
+
|
|
164
|
+
</button>
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
class="mint-lcms-sequence-table__action mint-lcms-sequence-table__action--danger"
|
|
168
|
+
title="Delete"
|
|
169
|
+
@click="emit('remove', index)"
|
|
170
|
+
>
|
|
171
|
+
x
|
|
172
|
+
</button>
|
|
173
|
+
</td>
|
|
174
|
+
</tr>
|
|
175
|
+
</tbody>
|
|
176
|
+
</table>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div v-if="items.length === 0" class="mint-lcms-sequence-table__empty">
|
|
180
|
+
{{ emptyMessage }}
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div v-if="hasMore && showMoreLabel" class="mint-lcms-sequence-table__more">
|
|
184
|
+
Showing first {{ maxRows }} of {{ items.length }} items
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</template>
|
|
188
|
+
|
|
189
|
+
<style>
|
|
190
|
+
@import '../styles/components/lcms-sequence-table.css';
|
|
191
|
+
</style>
|
|
@@ -28,10 +28,12 @@ const emit = defineEmits<{
|
|
|
28
28
|
const isSliderMode = computed(() => props.min !== undefined && props.max !== undefined)
|
|
29
29
|
|
|
30
30
|
const sliderPercent = computed(() => {
|
|
31
|
-
|
|
32
|
-
const
|
|
31
|
+
const min = props.min
|
|
32
|
+
const max = props.max
|
|
33
|
+
if (min === undefined || max === undefined || props.modelValue === undefined) return 0
|
|
34
|
+
const range = max - min
|
|
33
35
|
if (range === 0) return 0
|
|
34
|
-
return ((props.modelValue -
|
|
36
|
+
return ((props.modelValue - min) / range) * 100
|
|
35
37
|
})
|
|
36
38
|
|
|
37
39
|
const canDecrement = computed(() => {
|
|
@@ -12,6 +12,7 @@ interface Props {
|
|
|
12
12
|
indeterminate?: boolean
|
|
13
13
|
steps?: string[]
|
|
14
14
|
currentStep?: number
|
|
15
|
+
ariaLabel?: string
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -58,6 +59,7 @@ const segmentCount = computed(() => props.steps.length || 0)
|
|
|
58
59
|
:aria-valuenow="indeterminate ? undefined : clampedValue"
|
|
59
60
|
:aria-valuemin="indeterminate ? undefined : 0"
|
|
60
61
|
:aria-valuemax="indeterminate ? undefined : 100"
|
|
62
|
+
:aria-label="ariaLabel ?? label"
|
|
61
63
|
>
|
|
62
64
|
<div
|
|
63
65
|
:class="[
|
|
@@ -77,6 +79,7 @@ const segmentCount = computed(() => props.steps.length || 0)
|
|
|
77
79
|
:aria-valuenow="currentStep"
|
|
78
80
|
:aria-valuemin="0"
|
|
79
81
|
:aria-valuemax="segmentCount"
|
|
82
|
+
:aria-label="ariaLabel ?? label"
|
|
80
83
|
>
|
|
81
84
|
<span
|
|
82
85
|
v-for="(_step, i) in steps"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/** Multi-rack editor for managing collections of well-plate racks with add/remove/reorder controls and per-well editing. */
|
|
3
3
|
import { ref, computed, watch } from 'vue'
|
|
4
|
-
import type { Rack, SlotPosition, WellPlateFormat, WellPlateSize, WellEditData, Well } from '../types'
|
|
4
|
+
import type { Rack, SlotPosition, WellPlateFormat, WellPlateSize, WellEditData, Well, WellEditField, WellSampleDropData, RackSampleDropMapper } from '../types'
|
|
5
5
|
import { useRackEditor } from '../composables/useRackEditor'
|
|
6
6
|
import WellPlate from './WellPlate.vue'
|
|
7
7
|
|
|
@@ -16,6 +16,8 @@ interface Props {
|
|
|
16
16
|
wellPlateSize?: WellPlateSize
|
|
17
17
|
showLegend?: boolean
|
|
18
18
|
showBadges?: boolean
|
|
19
|
+
allowSampleDrop?: boolean
|
|
20
|
+
sampleDropMapper?: RackSampleDropMapper
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -29,6 +31,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
29
31
|
wellPlateSize: 'md',
|
|
30
32
|
showLegend: true,
|
|
31
33
|
showBadges: true,
|
|
34
|
+
allowSampleDrop: false,
|
|
35
|
+
sampleDropMapper: undefined,
|
|
32
36
|
})
|
|
33
37
|
|
|
34
38
|
const emit = defineEmits<{
|
|
@@ -38,6 +42,24 @@ const emit = defineEmits<{
|
|
|
38
42
|
'rack-remove': [rackId: string]
|
|
39
43
|
'rack-reorder': [rackIds: string[]]
|
|
40
44
|
'well-edit': [rackId: string, wellId: string, data: WellEditData]
|
|
45
|
+
'sample-drop': [rackId: string, wellId: string, data: WellSampleDropData, event: DragEvent]
|
|
46
|
+
}>()
|
|
47
|
+
|
|
48
|
+
interface RackWellEditorSlotProps {
|
|
49
|
+
rack?: Rack
|
|
50
|
+
rackId: string
|
|
51
|
+
wellId: string
|
|
52
|
+
wellData?: Partial<Well>
|
|
53
|
+
editFields: WellEditField[]
|
|
54
|
+
defaultInjectionVolume: number
|
|
55
|
+
position: { x: number; y: number }
|
|
56
|
+
save: (data: WellEditData) => void
|
|
57
|
+
clear: () => void
|
|
58
|
+
close: () => void
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
defineSlots<{
|
|
62
|
+
'well-editor'?: (props: RackWellEditorSlotProps) => unknown
|
|
41
63
|
}>()
|
|
42
64
|
|
|
43
65
|
const editor = useRackEditor(props.modelValue, {
|
|
@@ -214,6 +236,37 @@ function handleWellClear(wellId: string) {
|
|
|
214
236
|
editor.clearWell(editor.activeRack.value.id, wellId)
|
|
215
237
|
}
|
|
216
238
|
|
|
239
|
+
function handleSampleDrop(wellId: string, data: WellSampleDropData, event: DragEvent) {
|
|
240
|
+
const rack = editor.activeRack.value
|
|
241
|
+
if (!rack) return
|
|
242
|
+
|
|
243
|
+
emit('sample-drop', rack.id, wellId, data, event)
|
|
244
|
+
|
|
245
|
+
const editData = props.sampleDropMapper?.(data, {
|
|
246
|
+
rack,
|
|
247
|
+
rackId: rack.id,
|
|
248
|
+
wellId,
|
|
249
|
+
event,
|
|
250
|
+
}) ?? defaultSampleDropEditData(wellId, data, rack)
|
|
251
|
+
|
|
252
|
+
if (!editData) return
|
|
253
|
+
handleWellEdit(wellId, { ...editData, wellId })
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function defaultSampleDropEditData(wellId: string, data: WellSampleDropData, rack: Rack): WellEditData | null {
|
|
257
|
+
const label = data.label ?? data.sampleName ?? data.id ?? ''
|
|
258
|
+
if (!label.trim()) return null
|
|
259
|
+
|
|
260
|
+
return {
|
|
261
|
+
wellId,
|
|
262
|
+
label: label.trim(),
|
|
263
|
+
sampleType: data.sampleType ?? 'sample',
|
|
264
|
+
injectionVolume: data.injectionVolume ?? rack.injectionVolume,
|
|
265
|
+
injectionCount: data.injectionCount ?? 1,
|
|
266
|
+
customMethod: data.customMethod ?? '',
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
217
270
|
// Computed well count per rack
|
|
218
271
|
function getWellCount(rack: Rack): number {
|
|
219
272
|
return Object.keys(rack.wells).length
|
|
@@ -359,9 +412,27 @@ const activeRackWells = computed(() => editor.activeRack.value?.wells ?? {})
|
|
|
359
412
|
:readonly="readonly"
|
|
360
413
|
:size="wellPlateSize"
|
|
361
414
|
:show-sample-type-indicator="true"
|
|
415
|
+
:allow-sample-drop="allowSampleDrop && !readonly"
|
|
362
416
|
@well-edit="handleWellEdit"
|
|
363
417
|
@well-clear="handleWellClear"
|
|
364
|
-
|
|
418
|
+
@sample-drop="handleSampleDrop"
|
|
419
|
+
>
|
|
420
|
+
<template v-if="$slots['well-editor']" #well-editor="slotProps">
|
|
421
|
+
<slot
|
|
422
|
+
name="well-editor"
|
|
423
|
+
:rack="editor.activeRack.value"
|
|
424
|
+
:rack-id="editor.activeRack.value?.id ?? ''"
|
|
425
|
+
:well-id="slotProps.wellId"
|
|
426
|
+
:well-data="slotProps.wellData"
|
|
427
|
+
:edit-fields="slotProps.editFields"
|
|
428
|
+
:default-injection-volume="slotProps.defaultInjectionVolume"
|
|
429
|
+
:position="slotProps.position"
|
|
430
|
+
:save="slotProps.save"
|
|
431
|
+
:clear="slotProps.clear"
|
|
432
|
+
:close="slotProps.close"
|
|
433
|
+
/>
|
|
434
|
+
</template>
|
|
435
|
+
</WellPlate>
|
|
365
436
|
</div>
|
|
366
437
|
</div>
|
|
367
438
|
</template>
|
|
@@ -149,6 +149,7 @@ function canShowChildren(node: TreeNode, depth: number): boolean {
|
|
|
149
149
|
|
|
150
150
|
// Render tree node recursively
|
|
151
151
|
function renderNode(node: TreeNode, depth: number): VNode {
|
|
152
|
+
const children = node.children
|
|
152
153
|
const expanded = isExpanded(node.id)
|
|
153
154
|
const canExpand = hasChildren(node)
|
|
154
155
|
const showChildNodes = canShowChildren(node, depth)
|
|
@@ -197,11 +198,11 @@ function renderNode(node: TreeNode, depth: number): VNode {
|
|
|
197
198
|
)
|
|
198
199
|
|
|
199
200
|
const childNodes =
|
|
200
|
-
showChildNodes &&
|
|
201
|
+
showChildNodes && children
|
|
201
202
|
? h(
|
|
202
203
|
Transition,
|
|
203
204
|
{ enterActiveClass: 'mint-sample-tree__children--entering', leaveActiveClass: 'mint-sample-tree__children--leaving' },
|
|
204
|
-
() => h('div', { class: 'mint-sample-tree__children' },
|
|
205
|
+
() => h('div', { class: 'mint-sample-tree__children' }, children.map((child) => renderNode(child, depth + 1)))
|
|
205
206
|
)
|
|
206
207
|
: null
|
|
207
208
|
|
|
@@ -11,23 +11,27 @@ import { useTextSearch } from '../composables/useTextSearch'
|
|
|
11
11
|
import { useListSelection } from '../composables/useListSelection'
|
|
12
12
|
import { useSampleGroups, type SampleMajorGroup } from '../composables/useSampleGroups'
|
|
13
13
|
import { useExpansionSet } from '../composables/useExpansionSet'
|
|
14
|
+
import { useExperimentSamples } from '../composables/useExperimentSamples'
|
|
14
15
|
|
|
15
16
|
interface Props {
|
|
16
|
-
samples
|
|
17
|
+
samples?: string[]
|
|
17
18
|
modelValue: string[]
|
|
18
19
|
groups?: SampleGroup[]
|
|
19
20
|
enableGrouping?: boolean
|
|
20
21
|
enableSmartGroup?: boolean
|
|
21
22
|
experimentId?: number
|
|
22
23
|
designData?: Record<string, unknown>
|
|
24
|
+
autoloadExperimentData?: boolean
|
|
23
25
|
}
|
|
24
26
|
|
|
25
27
|
const props = withDefaults(defineProps<Props>(), {
|
|
28
|
+
samples: () => [],
|
|
26
29
|
groups: () => [],
|
|
27
30
|
enableGrouping: true,
|
|
28
31
|
enableSmartGroup: true,
|
|
29
32
|
experimentId: undefined,
|
|
30
33
|
designData: undefined,
|
|
34
|
+
autoloadExperimentData: true,
|
|
31
35
|
})
|
|
32
36
|
|
|
33
37
|
const emit = defineEmits<{
|
|
@@ -66,8 +70,23 @@ const internalGroups = computed({
|
|
|
66
70
|
set: (value) => emit('update:groups', value),
|
|
67
71
|
})
|
|
68
72
|
|
|
73
|
+
const providedSamples = computed(() => props.samples)
|
|
74
|
+
const shouldLoadExperimentSamples = computed(() =>
|
|
75
|
+
props.autoloadExperimentData && providedSamples.value.length === 0,
|
|
76
|
+
)
|
|
77
|
+
const experimentSamples = useExperimentSamples({
|
|
78
|
+
experimentId: () => props.experimentId,
|
|
79
|
+
designData: () => props.designData,
|
|
80
|
+
enabled: shouldLoadExperimentSamples,
|
|
81
|
+
})
|
|
82
|
+
const resolvedSamples = computed(() =>
|
|
83
|
+
providedSamples.value.length > 0 ? providedSamples.value : experimentSamples.samples.value,
|
|
84
|
+
)
|
|
85
|
+
const resolvedExperimentId = computed(() => props.experimentId ?? experimentSamples.experimentId.value)
|
|
86
|
+
const resolvedDesignData = computed(() => props.designData ?? experimentSamples.designData.value ?? undefined)
|
|
87
|
+
|
|
69
88
|
const sampleGroups = useSampleGroups({
|
|
70
|
-
samples: () =>
|
|
89
|
+
samples: () => resolvedSamples.value,
|
|
71
90
|
groups: internalGroups,
|
|
72
91
|
})
|
|
73
92
|
const hierarchicalGroups = sampleGroups.hierarchicalGroups
|
|
@@ -77,7 +96,7 @@ const groupingEnabled = computed(() => internalGroups.value.length > 0)
|
|
|
77
96
|
const ungroupedSamples = sampleGroups.ungroupedSamples
|
|
78
97
|
|
|
79
98
|
const sampleSearch = useTextSearch({
|
|
80
|
-
items: () =>
|
|
99
|
+
items: () => resolvedSamples.value,
|
|
81
100
|
query: searchQuery,
|
|
82
101
|
getText: sample => sample,
|
|
83
102
|
})
|
|
@@ -85,7 +104,7 @@ const filteredSamples = sampleSearch.filteredItems
|
|
|
85
104
|
|
|
86
105
|
const sampleSelection = useListSelection({
|
|
87
106
|
selected: () => props.modelValue,
|
|
88
|
-
items: () =>
|
|
107
|
+
items: () => resolvedSamples.value,
|
|
89
108
|
})
|
|
90
109
|
const isAllSelected = sampleSelection.isAllSelected
|
|
91
110
|
|
|
@@ -433,7 +452,7 @@ function addNewGroup() {
|
|
|
433
452
|
class="mint-sample-selector__checkbox"
|
|
434
453
|
/>
|
|
435
454
|
<span class="mint-sample-selector__select-all-label">Select All</span>
|
|
436
|
-
<span class="mint-sample-selector__select-all-count">{{
|
|
455
|
+
<span class="mint-sample-selector__select-all-count">{{ resolvedSamples.length }} samples</span>
|
|
437
456
|
</label>
|
|
438
457
|
|
|
439
458
|
<!-- Action Buttons Row -->
|
|
@@ -444,7 +463,7 @@ function addNewGroup() {
|
|
|
444
463
|
v-if="enableSmartGroup"
|
|
445
464
|
:variant="groupingEnabled ? 'primary' : 'secondary'"
|
|
446
465
|
size="sm"
|
|
447
|
-
:disabled="
|
|
466
|
+
:disabled="resolvedSamples.length === 0"
|
|
448
467
|
class="mint-sample-selector__action-btn"
|
|
449
468
|
@click="showSmartGroupModal = true"
|
|
450
469
|
>
|
|
@@ -940,9 +959,9 @@ function addNewGroup() {
|
|
|
940
959
|
<!-- Smart Grouping Modal -->
|
|
941
960
|
<AutoGroupModal
|
|
942
961
|
v-model="showSmartGroupModal"
|
|
943
|
-
:samples="
|
|
944
|
-
:experiment-id="
|
|
945
|
-
:design-data="
|
|
962
|
+
:samples="resolvedSamples"
|
|
963
|
+
:experiment-id="resolvedExperimentId"
|
|
964
|
+
:design-data="resolvedDesignData"
|
|
946
965
|
@apply="handleSmartGroupApply"
|
|
947
966
|
/>
|
|
948
967
|
</div>
|
|
@@ -33,7 +33,13 @@ const withDisabledOptions: SegmentedOption[] = [
|
|
|
33
33
|
{ value: 'deleted', label: 'Deleted', disabled: true },
|
|
34
34
|
]
|
|
35
35
|
|
|
36
|
+
const twoTabOptions: SegmentedOption[] = [
|
|
37
|
+
{ value: 'table', label: 'Table' },
|
|
38
|
+
{ value: 'chart', label: 'Chart' },
|
|
39
|
+
]
|
|
40
|
+
|
|
36
41
|
const cardSelectionP3 = ref('list')
|
|
42
|
+
const twoTabSelection = ref('table')
|
|
37
43
|
</script>
|
|
38
44
|
|
|
39
45
|
<template>
|
|
@@ -100,6 +106,17 @@ const cardSelectionP3 = ref('list')
|
|
|
100
106
|
</div>
|
|
101
107
|
</Variant>
|
|
102
108
|
|
|
109
|
+
<Variant title="Disabled By Value">
|
|
110
|
+
<div style="padding: 2rem; max-width: 500px; margin: 0 auto;">
|
|
111
|
+
<SegmentedControl
|
|
112
|
+
v-model="twoTabSelection"
|
|
113
|
+
:options="twoTabOptions"
|
|
114
|
+
:disabled-values="['chart']"
|
|
115
|
+
variant="simple"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
</Variant>
|
|
119
|
+
|
|
103
120
|
<Variant title="Not Full Width">
|
|
104
121
|
<div style="padding: 2rem;">
|
|
105
122
|
<SegmentedControl :model-value="'day'" :options="basicOptions" :full-width="false" />
|