@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
|
@@ -11,6 +11,7 @@ interface Props {
|
|
|
11
11
|
size?: SegmentedControlSize
|
|
12
12
|
fullWidth?: boolean
|
|
13
13
|
disabled?: boolean
|
|
14
|
+
disabledValues?: Array<string | number>
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -18,6 +19,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
18
19
|
size: 'md',
|
|
19
20
|
fullWidth: false,
|
|
20
21
|
disabled: false,
|
|
22
|
+
disabledValues: () => [],
|
|
21
23
|
})
|
|
22
24
|
|
|
23
25
|
const emit = defineEmits<{
|
|
@@ -25,9 +27,18 @@ const emit = defineEmits<{
|
|
|
25
27
|
}>()
|
|
26
28
|
|
|
27
29
|
const normalizedOptions = computed<SegmentedOption[]>(() => props.options.map(normalizeOptionInput))
|
|
30
|
+
const disabledValueSet = computed(() => new Set(props.disabledValues))
|
|
31
|
+
|
|
32
|
+
function isOptionDisabled(option: SegmentedOption) {
|
|
33
|
+
return option.disabled === true || disabledValueSet.value.has(option.value)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function isSelectionDisabled(option: SegmentedOption) {
|
|
37
|
+
return props.disabled || isOptionDisabled(option)
|
|
38
|
+
}
|
|
28
39
|
|
|
29
40
|
function handleSelect(option: SegmentedOption) {
|
|
30
|
-
if (
|
|
41
|
+
if (isSelectionDisabled(option)) return
|
|
31
42
|
emit('update:modelValue', option.value)
|
|
32
43
|
}
|
|
33
44
|
|
|
@@ -56,13 +67,13 @@ function handleKeydown(event: KeyboardEvent, option: SegmentedOption) {
|
|
|
56
67
|
type="button"
|
|
57
68
|
role="radio"
|
|
58
69
|
:aria-checked="modelValue === option.value"
|
|
59
|
-
:disabled="
|
|
70
|
+
:disabled="isSelectionDisabled(option)"
|
|
60
71
|
:class="[
|
|
61
72
|
'mint-segmented-control__option',
|
|
62
73
|
`mint-segmented-control__option--${variant}`,
|
|
63
74
|
`mint-segmented-control__option--${size}`,
|
|
64
75
|
modelValue === option.value ? 'mint-segmented-control__option--active' : '',
|
|
65
|
-
option
|
|
76
|
+
isOptionDisabled(option) ? 'mint-segmented-control__option--disabled' : '',
|
|
66
77
|
]"
|
|
67
78
|
@click="handleSelect(option)"
|
|
68
79
|
@keydown="handleKeydown($event, option)"
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** Displays LC/MS or GC/MS acquisition sequence progress with ETA and remaining-time labels. */
|
|
3
|
+
import { computed } from 'vue'
|
|
4
|
+
import {
|
|
5
|
+
formatSequenceEta,
|
|
6
|
+
formatSequenceRemaining,
|
|
7
|
+
sequenceProgressPercent,
|
|
8
|
+
} from '../instrument'
|
|
9
|
+
import type { SequenceProgress } from '../types'
|
|
10
|
+
import ProgressBar from './ProgressBar.vue'
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
progress: SequenceProgress
|
|
14
|
+
compact?: boolean
|
|
15
|
+
label?: string
|
|
16
|
+
showEta?: boolean
|
|
17
|
+
showRemaining?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
21
|
+
compact: false,
|
|
22
|
+
label: 'samples',
|
|
23
|
+
showEta: true,
|
|
24
|
+
showRemaining: true,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
const percent = computed(() => sequenceProgressPercent(props.progress))
|
|
28
|
+
const remainingLabel = computed(() => formatSequenceRemaining(props.progress))
|
|
29
|
+
const etaLabel = computed(() => formatSequenceEta(props.progress))
|
|
30
|
+
const hasFooter = computed(() => {
|
|
31
|
+
if (props.compact) return false
|
|
32
|
+
return (props.showRemaining && remainingLabel.value) || (props.showEta && etaLabel.value)
|
|
33
|
+
})
|
|
34
|
+
</script>
|
|
35
|
+
|
|
36
|
+
<template>
|
|
37
|
+
<div class="mint-sequence-progress">
|
|
38
|
+
<div class="mint-sequence-progress__header">
|
|
39
|
+
<span class="mint-sequence-progress__label">
|
|
40
|
+
{{ progress.current_sample }} / {{ progress.total_samples }} {{ label }}
|
|
41
|
+
</span>
|
|
42
|
+
<span class="mint-sequence-progress__percent">{{ percent }}%</span>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<ProgressBar
|
|
46
|
+
:value="percent"
|
|
47
|
+
color="info"
|
|
48
|
+
size="sm"
|
|
49
|
+
aria-label="Sequence progress"
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
<div v-if="hasFooter" class="mint-sequence-progress__footer">
|
|
53
|
+
<span
|
|
54
|
+
v-if="showRemaining && remainingLabel"
|
|
55
|
+
class="mint-sequence-progress__remaining"
|
|
56
|
+
>
|
|
57
|
+
{{ remainingLabel }}
|
|
58
|
+
</span>
|
|
59
|
+
<span
|
|
60
|
+
v-if="showEta && etaLabel"
|
|
61
|
+
class="mint-sequence-progress__eta"
|
|
62
|
+
>
|
|
63
|
+
ETA {{ etaLabel }}
|
|
64
|
+
</span>
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
</template>
|
|
68
|
+
|
|
69
|
+
<style>
|
|
70
|
+
@import '../styles/components/sequence-progress-bar.css';
|
|
71
|
+
</style>
|
|
@@ -32,6 +32,9 @@ import {
|
|
|
32
32
|
type ControlWorkspaceOptions,
|
|
33
33
|
} from '../composables/useControlSchema'
|
|
34
34
|
import { useSettingsStore, colorPalettes } from '../stores/settings'
|
|
35
|
+
import { useAuthStore } from '../stores/auth'
|
|
36
|
+
import { usePlatformContext } from '../composables/usePlatformContext'
|
|
37
|
+
import { canAccessByPolicy } from '../permissions'
|
|
35
38
|
import {
|
|
36
39
|
formSchemaFieldNames,
|
|
37
40
|
pickExistingRecordKeys,
|
|
@@ -46,10 +49,12 @@ import type {
|
|
|
46
49
|
SettingsTabInput,
|
|
47
50
|
SettingsModalLayout,
|
|
48
51
|
SettingsModalSchema,
|
|
52
|
+
SettingsUserType,
|
|
49
53
|
FormSchema,
|
|
50
54
|
FormSectionSchema,
|
|
51
55
|
FormEnhancements,
|
|
52
56
|
} from '../types'
|
|
57
|
+
import type { AccessControlled, PermissionUser } from '../permissions'
|
|
53
58
|
import { normalizeItemInput } from '../utils/items'
|
|
54
59
|
|
|
55
60
|
// Map our settings groups onto the form-builder's flat-section shape.
|
|
@@ -86,6 +91,8 @@ interface Props {
|
|
|
86
91
|
values?: Record<string, unknown>
|
|
87
92
|
/** Optional dynamic enhancements (validators, dynamic options, callbacks). */
|
|
88
93
|
enhancements?: FormEnhancements<Record<string, unknown>>
|
|
94
|
+
/** Optional user type override for permission-filtered settings content. Defaults to SDK auth/platform context. */
|
|
95
|
+
userType?: SettingsUserType
|
|
89
96
|
}
|
|
90
97
|
|
|
91
98
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -104,6 +111,8 @@ const emit = defineEmits<{
|
|
|
104
111
|
}>()
|
|
105
112
|
|
|
106
113
|
const settings = useSettingsStore()
|
|
114
|
+
const auth = useAuthStore()
|
|
115
|
+
const { user: platformUser } = usePlatformContext()
|
|
107
116
|
|
|
108
117
|
const APPEARANCE_TAB_ID = 'appearance'
|
|
109
118
|
|
|
@@ -130,13 +139,34 @@ const resolvedControls = computed<ControlSchema | undefined>(() =>
|
|
|
130
139
|
const resolvedControlOptions = computed<ControlWorkspaceOptions>(() =>
|
|
131
140
|
mergeControlWorkspaceOptions(resolvedModel.value?.controlOptions ?? {}, props.controlOptions)
|
|
132
141
|
)
|
|
133
|
-
const
|
|
142
|
+
const sourceSettingsSchema = computed<SettingsModalSchema | undefined>(() =>
|
|
134
143
|
props.schema ?? (
|
|
135
144
|
resolvedControls.value
|
|
136
145
|
? controlsToSettingsSchema(resolvedControls.value, resolvedControlOptions.value)
|
|
137
146
|
: undefined
|
|
138
147
|
),
|
|
139
148
|
)
|
|
149
|
+
const currentAccessUser = computed<PermissionUser | null>(() => {
|
|
150
|
+
if (props.userType) {
|
|
151
|
+
return { role: props.userType === 'admin' ? 'admin' : 'user' }
|
|
152
|
+
}
|
|
153
|
+
return auth.userInfo ?? platformUser.value ?? null
|
|
154
|
+
})
|
|
155
|
+
const isAccessUserAuthenticated = computed(() =>
|
|
156
|
+
props.userType !== undefined || auth.isAuthenticated || !!platformUser.value,
|
|
157
|
+
)
|
|
158
|
+
const settingsSchema = computed<SettingsModalSchema | undefined>(() => {
|
|
159
|
+
if (!sourceSettingsSchema.value) return undefined
|
|
160
|
+
const groups = sourceSettingsSchema.value.groups.flatMap((group) => {
|
|
161
|
+
if (!canShowForCurrentUser(group)) return []
|
|
162
|
+
|
|
163
|
+
const fields = group.fields.filter(canShowForCurrentUser)
|
|
164
|
+
if (fields.length === 0) return []
|
|
165
|
+
|
|
166
|
+
return [{ ...group, fields }]
|
|
167
|
+
})
|
|
168
|
+
return { groups }
|
|
169
|
+
})
|
|
140
170
|
const isSchemaDriven = computed(() => !!settingsSchema.value)
|
|
141
171
|
const resolvedValues = computed<Record<string, unknown>>(() => ({
|
|
142
172
|
...(resolvedControlOptions.value.initialValues ?? {}),
|
|
@@ -201,7 +231,9 @@ const visibleSchemaGroups = computed(() =>
|
|
|
201
231
|
: [],
|
|
202
232
|
)
|
|
203
233
|
|
|
204
|
-
const manualTabs = computed<SettingsTab[]>(() =>
|
|
234
|
+
const manualTabs = computed<SettingsTab[]>(() =>
|
|
235
|
+
props.tabs.map(normalizeItemInput).filter(canShowForCurrentUser)
|
|
236
|
+
)
|
|
205
237
|
|
|
206
238
|
const allTabs = computed<SettingsTab[]>(() => {
|
|
207
239
|
const base: SettingsTab[] = settingsSchema.value
|
|
@@ -256,6 +288,14 @@ function builderFieldNames(): string[] {
|
|
|
256
288
|
function isControlModelBinding(model: ControlModel | ControlModelBinding): model is ControlModelBinding {
|
|
257
289
|
return 'controls' in model && 'controlOptions' in model
|
|
258
290
|
}
|
|
291
|
+
|
|
292
|
+
function canShowForCurrentUser(item: AccessControlled): boolean {
|
|
293
|
+
return canAccessByPolicy(
|
|
294
|
+
currentAccessUser.value,
|
|
295
|
+
item,
|
|
296
|
+
isAccessUserAuthenticated.value,
|
|
297
|
+
)
|
|
298
|
+
}
|
|
259
299
|
</script>
|
|
260
300
|
|
|
261
301
|
<template>
|
|
@@ -416,6 +456,13 @@ function isControlModelBinding(model: ControlModel | ControlModelBinding): model
|
|
|
416
456
|
</div>
|
|
417
457
|
</component>
|
|
418
458
|
</div>
|
|
459
|
+
<div v-if="$slots.footer" class="mint-settings-modal__footer">
|
|
460
|
+
<slot
|
|
461
|
+
name="footer"
|
|
462
|
+
:values="builder.form.data"
|
|
463
|
+
:close="handleClose"
|
|
464
|
+
/>
|
|
465
|
+
</div>
|
|
419
466
|
</BaseModal>
|
|
420
467
|
</template>
|
|
421
468
|
|
|
@@ -47,8 +47,12 @@ const groupedUnits = computed(() => {
|
|
|
47
47
|
const ungrouped: UnitOption[] = []
|
|
48
48
|
for (const u of props.units) {
|
|
49
49
|
if (u.group) {
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
const group = groups.get(u.group)
|
|
51
|
+
if (group) {
|
|
52
|
+
group.push(u)
|
|
53
|
+
} else {
|
|
54
|
+
groups.set(u.group, [u])
|
|
55
|
+
}
|
|
52
56
|
} else {
|
|
53
57
|
ungrouped.push(u)
|
|
54
58
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/** Interactive 96/384-well plate grid with drag-to-select, heatmap overlays, per-well editing, and sample-color mapping. */
|
|
3
3
|
import { ref, computed } from 'vue'
|
|
4
|
-
import type { WellPlateFormat, WellPlateSelectionMode, WellPlateSize, Well, HeatmapConfig, WellShape, WellEditField, WellEditData, WellLegendItem, ColumnCondition, RowCondition } from '../types'
|
|
4
|
+
import type { WellPlateFormat, WellPlateSelectionMode, WellPlateSize, Well, HeatmapConfig, WellShape, WellEditField, WellEditData, WellLegendItem, ColumnCondition, RowCondition, WellSampleDropData, WellSampleDropParser } from '../types'
|
|
5
5
|
import { useEventListener } from '../composables/useEventListener'
|
|
6
6
|
import WellEditPopupInternal from './internal/WellEditPopupInternal.vue'
|
|
7
7
|
|
|
@@ -29,6 +29,8 @@ interface Props {
|
|
|
29
29
|
legendItems?: WellLegendItem[]
|
|
30
30
|
columnConditions?: ColumnCondition[]
|
|
31
31
|
rowConditions?: RowCondition[]
|
|
32
|
+
allowSampleDrop?: boolean
|
|
33
|
+
sampleDropParser?: WellSampleDropParser
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
// Drag state for moving wells
|
|
@@ -59,6 +61,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
59
61
|
legendItems: undefined,
|
|
60
62
|
columnConditions: () => [],
|
|
61
63
|
rowConditions: () => [],
|
|
64
|
+
allowSampleDrop: false,
|
|
65
|
+
sampleDropParser: undefined,
|
|
62
66
|
})
|
|
63
67
|
|
|
64
68
|
const emit = defineEmits<{
|
|
@@ -70,6 +74,22 @@ const emit = defineEmits<{
|
|
|
70
74
|
'well-move': [sourceWellId: string, targetWellId: string]
|
|
71
75
|
'well-edit': [wellId: string, data: WellEditData]
|
|
72
76
|
'well-clear': [wellId: string]
|
|
77
|
+
'sample-drop': [wellId: string, data: WellSampleDropData, event: DragEvent]
|
|
78
|
+
}>()
|
|
79
|
+
|
|
80
|
+
interface WellEditorSlotProps {
|
|
81
|
+
wellId: string
|
|
82
|
+
wellData?: Partial<Well>
|
|
83
|
+
editFields: WellEditField[]
|
|
84
|
+
defaultInjectionVolume: number
|
|
85
|
+
position: { x: number; y: number }
|
|
86
|
+
save: (data: WellEditData) => void
|
|
87
|
+
clear: () => void
|
|
88
|
+
close: () => void
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
defineSlots<{
|
|
92
|
+
'well-editor'?: (props: WellEditorSlotProps) => unknown
|
|
73
93
|
}>()
|
|
74
94
|
|
|
75
95
|
const plateRef = ref<HTMLElement | null>(null)
|
|
@@ -164,6 +184,7 @@ const defaultLegendItems: WellLegendItem[] = [
|
|
|
164
184
|
{ type: 'sample', label: 'Sample', color: '#10b981' },
|
|
165
185
|
{ type: 'blank', label: 'Blank', color: '#f97316' },
|
|
166
186
|
{ type: 'qc', label: 'QC', color: '#8b5cf6' },
|
|
187
|
+
{ type: 'iqc', label: 'IQC', color: '#ec4899' },
|
|
167
188
|
]
|
|
168
189
|
|
|
169
190
|
const activeLegendItems = computed(() => props.legendItems ?? defaultLegendItems)
|
|
@@ -285,6 +306,7 @@ const defaultSampleTypeColors: Record<string, { bg: string; border: string }> =
|
|
|
285
306
|
control: { bg: 'rgba(59, 130, 246, 0.15)', border: 'rgba(59, 130, 246, 0.4)' },
|
|
286
307
|
blank: { bg: 'rgba(249, 115, 22, 0.15)', border: 'rgba(249, 115, 22, 0.4)' },
|
|
287
308
|
qc: { bg: 'rgba(139, 92, 246, 0.15)', border: 'rgba(139, 92, 246, 0.4)' },
|
|
309
|
+
iqc: { bg: 'rgba(236, 72, 153, 0.15)', border: 'rgba(236, 72, 153, 0.4)' },
|
|
288
310
|
}
|
|
289
311
|
|
|
290
312
|
const heatmapColors: Record<string, string[]> = {
|
|
@@ -373,6 +395,7 @@ function getSampleTypeIndicator(well: Well): string | null {
|
|
|
373
395
|
control: 'C',
|
|
374
396
|
blank: 'B',
|
|
375
397
|
qc: 'Q',
|
|
398
|
+
iqc: 'I',
|
|
376
399
|
}
|
|
377
400
|
return typeMap[well.sampleType] || well.sampleType.charAt(0).toUpperCase()
|
|
378
401
|
}
|
|
@@ -511,10 +534,12 @@ function handleDragStart(well: Well, event: DragEvent) {
|
|
|
511
534
|
}
|
|
512
535
|
|
|
513
536
|
function handleDragOver(well: Well, event: DragEvent) {
|
|
514
|
-
|
|
537
|
+
const isMovingWell = props.selectionMode === 'drag' && !!dragSourceWell.value
|
|
538
|
+
if (!isMovingWell && !props.allowSampleDrop) return
|
|
539
|
+
|
|
515
540
|
event.preventDefault()
|
|
516
541
|
if (event.dataTransfer) {
|
|
517
|
-
event.dataTransfer.dropEffect = 'move'
|
|
542
|
+
event.dataTransfer.dropEffect = isMovingWell ? 'move' : 'copy'
|
|
518
543
|
}
|
|
519
544
|
dragTargetWell.value = well.id
|
|
520
545
|
}
|
|
@@ -524,14 +549,27 @@ function handleDragLeave() {
|
|
|
524
549
|
}
|
|
525
550
|
|
|
526
551
|
function handleDrop(well: Well, event: DragEvent) {
|
|
552
|
+
const isMovingWell = props.selectionMode === 'drag' && !!dragSourceWell.value
|
|
553
|
+
if (!isMovingWell && !props.allowSampleDrop) return
|
|
554
|
+
|
|
527
555
|
event.preventDefault()
|
|
528
|
-
if (props.selectionMode !== 'drag' || !dragSourceWell.value) return
|
|
529
556
|
|
|
530
|
-
|
|
531
|
-
|
|
557
|
+
if (isMovingWell && dragSourceWell.value) {
|
|
558
|
+
const sourceId = dragSourceWell.value
|
|
559
|
+
const targetId = well.id
|
|
560
|
+
|
|
561
|
+
if (sourceId !== targetId) {
|
|
562
|
+
emit('well-move', sourceId, targetId)
|
|
563
|
+
}
|
|
532
564
|
|
|
533
|
-
|
|
534
|
-
|
|
565
|
+
dragSourceWell.value = null
|
|
566
|
+
dragTargetWell.value = null
|
|
567
|
+
return
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
const droppedSample = parseDroppedSample(event, well.id)
|
|
571
|
+
if (droppedSample) {
|
|
572
|
+
emit('sample-drop', well.id, droppedSample, event)
|
|
535
573
|
}
|
|
536
574
|
|
|
537
575
|
dragSourceWell.value = null
|
|
@@ -543,6 +581,76 @@ function handleDragEnd() {
|
|
|
543
581
|
dragTargetWell.value = null
|
|
544
582
|
}
|
|
545
583
|
|
|
584
|
+
function parseDroppedSample(event: DragEvent, wellId: string): WellSampleDropData | null {
|
|
585
|
+
if (props.sampleDropParser) {
|
|
586
|
+
return props.sampleDropParser(event, { wellId, event }) ?? null
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const transfer = event.dataTransfer
|
|
590
|
+
if (!transfer) return null
|
|
591
|
+
|
|
592
|
+
const json = transfer.getData('application/json') || transfer.getData('text/json')
|
|
593
|
+
if (json) {
|
|
594
|
+
try {
|
|
595
|
+
return normalizeDroppedSample(JSON.parse(json))
|
|
596
|
+
} catch {
|
|
597
|
+
return null
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const text = transfer.getData('text/plain')?.trim()
|
|
602
|
+
if (!text) return null
|
|
603
|
+
return {
|
|
604
|
+
sampleName: text,
|
|
605
|
+
label: text,
|
|
606
|
+
raw: text,
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function normalizeDroppedSample(raw: unknown): WellSampleDropData | null {
|
|
611
|
+
if (!raw || typeof raw !== 'object') return null
|
|
612
|
+
|
|
613
|
+
const record = raw as Record<string, unknown>
|
|
614
|
+
const sampleName =
|
|
615
|
+
getString(record.sampleName) ??
|
|
616
|
+
getString(record.sample_name) ??
|
|
617
|
+
getString(record.name) ??
|
|
618
|
+
getString(record.label)
|
|
619
|
+
const label = getString(record.label) ?? sampleName
|
|
620
|
+
const sampleType =
|
|
621
|
+
getString(record.sampleType) ??
|
|
622
|
+
getString(record.sample_type) ??
|
|
623
|
+
(record.type === 'sample' ? 'sample' : undefined)
|
|
624
|
+
|
|
625
|
+
if (!sampleName && !label && !getString(record.id)) return null
|
|
626
|
+
|
|
627
|
+
return {
|
|
628
|
+
id: getString(record.id),
|
|
629
|
+
sampleName,
|
|
630
|
+
label,
|
|
631
|
+
sampleType,
|
|
632
|
+
injectionVolume: getNumber(record.injectionVolume ?? record.injection_volume),
|
|
633
|
+
injectionCount: getNumber(record.injectionCount ?? record.injection_count),
|
|
634
|
+
customMethod: getString(record.customMethod ?? record.custom_method) ?? null,
|
|
635
|
+
metadata: getRecord(record.metadata),
|
|
636
|
+
raw,
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function getString(value: unknown): string | undefined {
|
|
641
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function getNumber(value: unknown): number | undefined {
|
|
645
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : undefined
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
function getRecord(value: unknown): Record<string, unknown> | undefined {
|
|
649
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
650
|
+
? value as Record<string, unknown>
|
|
651
|
+
: undefined
|
|
652
|
+
}
|
|
653
|
+
|
|
546
654
|
function handleKeyDown(event: KeyboardEvent) {
|
|
547
655
|
if (props.disabled || props.readonly) return
|
|
548
656
|
|
|
@@ -726,9 +834,9 @@ const tableStyle = computed(() => ({
|
|
|
726
834
|
@mouseleave="handleWellMouseLeave"
|
|
727
835
|
@contextmenu="handleContextMenu(well, $event)"
|
|
728
836
|
@dragstart="handleDragStart(well, $event)"
|
|
729
|
-
@dragover
|
|
837
|
+
@dragover="handleDragOver(well, $event)"
|
|
730
838
|
@dragleave="handleDragLeave"
|
|
731
|
-
@drop
|
|
839
|
+
@drop="handleDrop(well, $event)"
|
|
732
840
|
@dragend="handleDragEnd"
|
|
733
841
|
@keydown.enter="handleWellClick(well, $event as unknown as MouseEvent)"
|
|
734
842
|
@keydown.space.prevent="handleWellClick(well, $event as unknown as MouseEvent)"
|
|
@@ -759,10 +867,10 @@ const tableStyle = computed(() => ({
|
|
|
759
867
|
<span
|
|
760
868
|
v-if="getWellBadge(well)"
|
|
761
869
|
class="mint-well-plate__badge"
|
|
762
|
-
:style="{ backgroundColor: getWellBadge(well)
|
|
763
|
-
:title="getWellBadge(well)
|
|
870
|
+
:style="{ backgroundColor: getWellBadge(well)?.color }"
|
|
871
|
+
:title="getWellBadge(well)?.text === '+' ? 'Custom method' : `${getWellBadge(well)?.text ?? ''}x injections`"
|
|
764
872
|
>
|
|
765
|
-
{{ getWellBadge(well)
|
|
873
|
+
{{ getWellBadge(well)?.text }}
|
|
766
874
|
</span>
|
|
767
875
|
</div>
|
|
768
876
|
</td>
|
|
@@ -805,17 +913,30 @@ const tableStyle = computed(() => ({
|
|
|
805
913
|
</div>
|
|
806
914
|
|
|
807
915
|
<!-- Edit popup -->
|
|
808
|
-
<
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
916
|
+
<template v-if="editable && editingWellId">
|
|
917
|
+
<slot
|
|
918
|
+
name="well-editor"
|
|
919
|
+
:well-id="editingWellId"
|
|
920
|
+
:well-data="wells[editingWellId]"
|
|
921
|
+
:edit-fields="editFields"
|
|
922
|
+
:default-injection-volume="defaultInjectionVolume"
|
|
923
|
+
:position="editPopupPosition"
|
|
924
|
+
:save="handleEditSave"
|
|
925
|
+
:clear="handleEditClear"
|
|
926
|
+
:close="handleEditClose"
|
|
927
|
+
>
|
|
928
|
+
<WellEditPopupInternal
|
|
929
|
+
:well-id="editingWellId"
|
|
930
|
+
:well-data="wells[editingWellId]"
|
|
931
|
+
:edit-fields="editFields"
|
|
932
|
+
:default-injection-volume="defaultInjectionVolume"
|
|
933
|
+
:position="editPopupPosition"
|
|
934
|
+
@save="handleEditSave"
|
|
935
|
+
@clear="handleEditClear"
|
|
936
|
+
@close="handleEditClose"
|
|
937
|
+
/>
|
|
938
|
+
</slot>
|
|
939
|
+
</template>
|
|
819
940
|
</div>
|
|
820
941
|
</template>
|
|
821
942
|
|
package/src/components/index.ts
CHANGED
|
@@ -89,6 +89,11 @@ export { default as ProtocolStepEditor } from './ProtocolStepEditor.vue'
|
|
|
89
89
|
// Scientific display components
|
|
90
90
|
export { default as ScientificNumber } from './ScientificNumber.vue'
|
|
91
91
|
export { default as ChemicalFormula } from './ChemicalFormula.vue'
|
|
92
|
+
export { default as SequenceProgressBar } from './SequenceProgressBar.vue'
|
|
93
|
+
export { default as InstrumentAlertLog } from './InstrumentAlertLog.vue'
|
|
94
|
+
export { default as InstrumentStateBadge } from './InstrumentStateBadge.vue'
|
|
95
|
+
export { default as InstrumentStatusCard } from './InstrumentStatusCard.vue'
|
|
96
|
+
export { default as LcmsSequenceTable } from './LcmsSequenceTable.vue'
|
|
92
97
|
|
|
93
98
|
// Scientific input components
|
|
94
99
|
export { default as FormulaInput } from './FormulaInput.vue'
|