@morscherlab/mint-sdk 1.0.0-rc.1 → 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__/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-CBG3bWEc.js → auth-B7g4J4ZF.js} +99 -5
- 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 +3 -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-5KSfsVqf.js → components-BhK-dW99.js} +2091 -1051
- 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-D4Myb30a.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 +3316 -1216
- 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-BSlxwV2c.js → templates-BorLR_7p.js} +313 -3
- 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-BbbdI5xT.js → useProtocolTemplates-n6AJqSqv.js} +534 -359
- 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/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/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__/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/AppTopBar.vue +1 -0
- package/src/components/BaseTabs.vue +22 -1
- 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/ProgressBar.vue +3 -0
- package/src/components/RackEditor.vue +73 -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 +42 -2
- package/src/components/WellPlate.vue +142 -21
- 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/useAuth.ts +4 -0
- package/src/composables/useAutoGroup.ts +5 -1
- package/src/composables/useControlSchema.ts +21 -0
- package/src/composables/useExperimentData.ts +57 -16
- package/src/composables/useExperimentSamples.ts +142 -0
- 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 +31 -3
- 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/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-CBG3bWEc.js.map +0 -1
- package/dist/components-5KSfsVqf.js.map +0 -1
- package/dist/composables-D4Myb30a.js.map +0 -1
- package/dist/templates-BSlxwV2c.js.map +0 -1
- package/dist/useExperimentData-BbbdI5xT.js.map +0 -1
|
@@ -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)"
|
|
@@ -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'
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { SelectOption } from '../types'
|
|
2
|
+
|
|
3
|
+
export interface ExperimentDesignDataResponse {
|
|
4
|
+
experiment_id?: number
|
|
5
|
+
plugin_id?: string
|
|
6
|
+
data?: Record<string, unknown>
|
|
7
|
+
schema_version?: string
|
|
8
|
+
updated_at?: string | null
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ExtractExperimentSamplesOptions {
|
|
12
|
+
includeControls?: boolean
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
16
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isPlatformDesignDataResponse(value: Record<string, unknown>): boolean {
|
|
20
|
+
return (
|
|
21
|
+
'data' in value
|
|
22
|
+
&& (
|
|
23
|
+
'experiment_id' in value
|
|
24
|
+
|| 'plugin_id' in value
|
|
25
|
+
|| 'schema_version' in value
|
|
26
|
+
|| 'updated_at' in value
|
|
27
|
+
)
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Return the plugin-defined design_data payload from common platform and plugin response shapes. */
|
|
32
|
+
export function unwrapExperimentDesignData(
|
|
33
|
+
rawData: Record<string, unknown> | null | undefined,
|
|
34
|
+
): Record<string, unknown> | null {
|
|
35
|
+
if (!rawData) return null
|
|
36
|
+
|
|
37
|
+
if (isRecord(rawData.data) && isPlatformDesignDataResponse(rawData)) {
|
|
38
|
+
return rawData.data
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (isRecord(rawData.design_data)) {
|
|
42
|
+
return rawData.design_data
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (isRecord(rawData.designData)) {
|
|
46
|
+
return rawData.designData
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return rawData
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function shouldIncludeSample(record: Record<string, unknown>, options: ExtractExperimentSamplesOptions): boolean {
|
|
53
|
+
if (options.includeControls) return true
|
|
54
|
+
const rawType = record.sample_type ?? record.sampleType ?? record.type ?? record.well_type ?? record.wellType
|
|
55
|
+
const type = typeof rawType === 'string' ? rawType.toLowerCase() : ''
|
|
56
|
+
return !['blank', 'qc'].includes(type)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function readSampleValue(record: Record<string, unknown>): string | null {
|
|
60
|
+
const candidates = [
|
|
61
|
+
record.sample_name,
|
|
62
|
+
record.sampleName,
|
|
63
|
+
record.sample_id,
|
|
64
|
+
record.sampleId,
|
|
65
|
+
record.name,
|
|
66
|
+
record.id,
|
|
67
|
+
record.label,
|
|
68
|
+
]
|
|
69
|
+
|
|
70
|
+
for (const candidate of candidates) {
|
|
71
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
72
|
+
return candidate
|
|
73
|
+
}
|
|
74
|
+
if (typeof candidate === 'number' && Number.isFinite(candidate)) {
|
|
75
|
+
return String(candidate)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return null
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function readSampleLabel(record: Record<string, unknown>, fallback: string): string {
|
|
83
|
+
const label = record.name ?? record.label ?? record.sample_name ?? record.sampleName
|
|
84
|
+
return typeof label === 'string' && label.trim() ? label : fallback
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readSampleDescription(record: Record<string, unknown>): string | undefined {
|
|
88
|
+
const parts = [
|
|
89
|
+
record.group,
|
|
90
|
+
record.batch,
|
|
91
|
+
record.batch_id,
|
|
92
|
+
record.batchId,
|
|
93
|
+
record.plate_id,
|
|
94
|
+
record.plateId,
|
|
95
|
+
record.well_id,
|
|
96
|
+
record.wellId,
|
|
97
|
+
].filter((value): value is string | number =>
|
|
98
|
+
(typeof value === 'string' && value.trim().length > 0)
|
|
99
|
+
|| typeof value === 'number',
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
return parts.length > 0 ? parts.map(String).join(' / ') : undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function addSampleRecord(
|
|
106
|
+
options: SelectOption<string>[],
|
|
107
|
+
seen: Set<string>,
|
|
108
|
+
value: unknown,
|
|
109
|
+
extractOptions: ExtractExperimentSamplesOptions,
|
|
110
|
+
): void {
|
|
111
|
+
if (typeof value === 'string') {
|
|
112
|
+
const trimmed = value.trim()
|
|
113
|
+
if (trimmed && !seen.has(trimmed)) {
|
|
114
|
+
seen.add(trimmed)
|
|
115
|
+
options.push({ value: trimmed, label: trimmed })
|
|
116
|
+
}
|
|
117
|
+
return
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (!isRecord(value) || !shouldIncludeSample(value, extractOptions)) return
|
|
121
|
+
|
|
122
|
+
const sampleValue = readSampleValue(value)
|
|
123
|
+
if (!sampleValue || seen.has(sampleValue)) return
|
|
124
|
+
|
|
125
|
+
seen.add(sampleValue)
|
|
126
|
+
options.push({
|
|
127
|
+
value: sampleValue,
|
|
128
|
+
label: readSampleLabel(value, sampleValue),
|
|
129
|
+
description: readSampleDescription(value),
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function addSampleArray(
|
|
134
|
+
options: SelectOption<string>[],
|
|
135
|
+
seen: Set<string>,
|
|
136
|
+
value: unknown,
|
|
137
|
+
extractOptions: ExtractExperimentSamplesOptions,
|
|
138
|
+
): void {
|
|
139
|
+
if (!Array.isArray(value)) return
|
|
140
|
+
for (const sample of value) {
|
|
141
|
+
addSampleRecord(options, seen, sample, extractOptions)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Extract selectable sample options from raw or wrapped experiment design_data. */
|
|
146
|
+
export function extractSampleOptionsFromDesignData(
|
|
147
|
+
rawData: Record<string, unknown> | null | undefined,
|
|
148
|
+
options: ExtractExperimentSamplesOptions = {},
|
|
149
|
+
): SelectOption<string>[] {
|
|
150
|
+
const designData = unwrapExperimentDesignData(rawData)
|
|
151
|
+
if (!designData) return []
|
|
152
|
+
|
|
153
|
+
const sampleOptions: SelectOption<string>[] = []
|
|
154
|
+
const seen = new Set<string>()
|
|
155
|
+
|
|
156
|
+
addSampleArray(sampleOptions, seen, designData.samples, options)
|
|
157
|
+
|
|
158
|
+
if (isRecord(designData.templates)) {
|
|
159
|
+
for (const template of Object.values(designData.templates)) {
|
|
160
|
+
if (!isRecord(template)) continue
|
|
161
|
+
const templateData = isRecord(template.data) ? template.data : template
|
|
162
|
+
addSampleArray(sampleOptions, seen, templateData.samples, options)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (Array.isArray(designData.plates)) {
|
|
167
|
+
for (const plate of designData.plates) {
|
|
168
|
+
if (!isRecord(plate)) continue
|
|
169
|
+
addSampleArray(sampleOptions, seen, plate.samples, options)
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return sampleOptions
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** Extract SampleSelector-compatible string values from raw or wrapped experiment design_data. */
|
|
177
|
+
export function extractSampleNamesFromDesignData(
|
|
178
|
+
rawData: Record<string, unknown> | null | undefined,
|
|
179
|
+
options: ExtractExperimentSamplesOptions = {},
|
|
180
|
+
): string[] {
|
|
181
|
+
return extractSampleOptionsFromDesignData(rawData, options).map(sample => sample.value)
|
|
182
|
+
}
|
package/src/composables/index.ts
CHANGED
|
@@ -144,6 +144,20 @@ export {
|
|
|
144
144
|
type UseExperimentDataOptions,
|
|
145
145
|
type UseExperimentDataReturn,
|
|
146
146
|
} from './useExperimentData'
|
|
147
|
+
export {
|
|
148
|
+
extractSampleNamesFromDesignData,
|
|
149
|
+
extractSampleOptionsFromDesignData,
|
|
150
|
+
unwrapExperimentDesignData,
|
|
151
|
+
type ExperimentDesignDataResponse,
|
|
152
|
+
type ExtractExperimentSamplesOptions,
|
|
153
|
+
} from './experimentDesignData'
|
|
154
|
+
export {
|
|
155
|
+
useExperimentSamples,
|
|
156
|
+
type ExperimentDesignDataSource,
|
|
157
|
+
type ExperimentIdSource,
|
|
158
|
+
type UseExperimentSamplesOptions,
|
|
159
|
+
type UseExperimentSamplesReturn,
|
|
160
|
+
} from './useExperimentSamples'
|
|
147
161
|
export {
|
|
148
162
|
getFieldRegistryEntry,
|
|
149
163
|
getTypeDefault,
|
|
@@ -3,6 +3,7 @@ import { ref, onMounted, onUnmounted, watch, getCurrentInstance, type Ref } from
|
|
|
3
3
|
import { useAuthStore } from '../stores/auth'
|
|
4
4
|
import { useSettingsStore } from '../stores/settings'
|
|
5
5
|
import type { AuthConfig, UserInfo, LoginResponse, TokenVerifyResponse, UpdateProfileRequest } from '../types'
|
|
6
|
+
import type { RoleInfo } from '../permissions'
|
|
6
7
|
|
|
7
8
|
interface UserResponse {
|
|
8
9
|
id: string
|
|
@@ -10,6 +11,7 @@ interface UserResponse {
|
|
|
10
11
|
shortname: string | null
|
|
11
12
|
email: string | null
|
|
12
13
|
role: string
|
|
14
|
+
role_obj?: RoleInfo | null
|
|
13
15
|
is_active: boolean
|
|
14
16
|
}
|
|
15
17
|
|
|
@@ -187,6 +189,7 @@ export function useAuth(): UseAuthReturn {
|
|
|
187
189
|
shortname: response.data.shortname,
|
|
188
190
|
email: response.data.email,
|
|
189
191
|
role: response.data.role,
|
|
192
|
+
roleObj: response.data.role_obj ?? null,
|
|
190
193
|
isActive: response.data.is_active,
|
|
191
194
|
}
|
|
192
195
|
|
|
@@ -379,6 +382,7 @@ export function useAuth(): UseAuthReturn {
|
|
|
379
382
|
shortname: response.data.shortname,
|
|
380
383
|
email: response.data.email,
|
|
381
384
|
role: response.data.role,
|
|
385
|
+
roleObj: response.data.role_obj ?? null,
|
|
382
386
|
isActive: response.data.is_active,
|
|
383
387
|
}
|
|
384
388
|
authStore.setUserInfo(userInfo)
|
|
@@ -9,6 +9,7 @@ import type {
|
|
|
9
9
|
ParsedCsvData,
|
|
10
10
|
} from '../types/auto-group'
|
|
11
11
|
import type { SampleGroup } from '../types/components'
|
|
12
|
+
import { unwrapExperimentDesignData } from './experimentDesignData'
|
|
12
13
|
|
|
13
14
|
export const DEFAULT_COLORS = [
|
|
14
15
|
'#3B82F6', '#10B981', '#F59E0B', '#EF4444', '#8B5CF6',
|
|
@@ -311,7 +312,10 @@ export function computeGroups(
|
|
|
311
312
|
export function extractSamplesFromDesignData(
|
|
312
313
|
rawData: Record<string, unknown>,
|
|
313
314
|
): ParsedCsvData | null {
|
|
314
|
-
const
|
|
315
|
+
const designData = unwrapExperimentDesignData(rawData)
|
|
316
|
+
if (!designData) return null
|
|
317
|
+
|
|
318
|
+
const samples = designData.samples
|
|
315
319
|
if (!Array.isArray(samples) || samples.length === 0) return null
|
|
316
320
|
|
|
317
321
|
// Single pass: filter QC/blank and collect all condition keys
|
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
SettingsModalSchema,
|
|
7
7
|
TopBarSettingsConfig,
|
|
8
8
|
} from '../types'
|
|
9
|
+
import type { AccessPolicy, AccessAudienceInput } from '../permissions'
|
|
9
10
|
import type {
|
|
10
11
|
FieldCondition,
|
|
11
12
|
FieldValidation,
|
|
@@ -34,6 +35,11 @@ export interface ControlSectionConfig {
|
|
|
34
35
|
iconBg?: string
|
|
35
36
|
columns?: 1 | 2 | 3
|
|
36
37
|
condition?: FieldCondition
|
|
38
|
+
access?: AccessPolicy
|
|
39
|
+
visibleFor?: AccessAudienceInput
|
|
40
|
+
requiresAdmin?: boolean
|
|
41
|
+
permissions?: readonly string[]
|
|
42
|
+
anyPermissions?: readonly string[]
|
|
37
43
|
defaultOpen?: boolean
|
|
38
44
|
showToggle?: boolean
|
|
39
45
|
}
|
|
@@ -70,6 +76,11 @@ export interface ControlDefinition {
|
|
|
70
76
|
pattern?: string | { value: string; message: string }
|
|
71
77
|
validation?: FieldValidation
|
|
72
78
|
condition?: FieldCondition
|
|
79
|
+
access?: AccessPolicy
|
|
80
|
+
visibleFor?: AccessAudienceInput
|
|
81
|
+
requiresAdmin?: boolean
|
|
82
|
+
permissions?: readonly string[]
|
|
83
|
+
anyPermissions?: readonly string[]
|
|
73
84
|
options?: readonly ControlOption[]
|
|
74
85
|
section?: string
|
|
75
86
|
sectionLabel?: string
|
|
@@ -760,6 +771,11 @@ export function controlsToSettingsSchema(
|
|
|
760
771
|
fields: sectionControls.map(controlToFormField),
|
|
761
772
|
columns: config.columns ?? options.columns ?? 1,
|
|
762
773
|
condition: config.condition,
|
|
774
|
+
access: config.access,
|
|
775
|
+
visibleFor: config.visibleFor,
|
|
776
|
+
requiresAdmin: config.requiresAdmin,
|
|
777
|
+
permissions: config.permissions,
|
|
778
|
+
anyPermissions: config.anyPermissions,
|
|
763
779
|
}
|
|
764
780
|
}),
|
|
765
781
|
}
|
|
@@ -1150,6 +1166,11 @@ function controlToFormField(control: NormalizedControl): FormFieldSchema {
|
|
|
1150
1166
|
readonly: definition.readonly,
|
|
1151
1167
|
validation: validationForControl(definition),
|
|
1152
1168
|
condition: definition.condition,
|
|
1169
|
+
access: definition.access,
|
|
1170
|
+
visibleFor: definition.visibleFor,
|
|
1171
|
+
requiresAdmin: definition.requiresAdmin,
|
|
1172
|
+
permissions: definition.permissions,
|
|
1173
|
+
anyPermissions: definition.anyPermissions,
|
|
1153
1174
|
colSpan: definition.colSpan,
|
|
1154
1175
|
props,
|
|
1155
1176
|
}
|