@morscherlab/mint-sdk 1.2.0-beta.7 → 1.2.0-beta.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/{ExperimentPopover-CfOdh5Rm.js → ExperimentPopover-oL0fg_A0.js} +5 -3
- package/dist/ExperimentPopover-oL0fg_A0.js.map +1 -0
- package/dist/{ExperimentSelectorModal-DvIagrxY.js → ExperimentSelectorModal-BeEOomw_.js} +4 -4
- package/dist/ExperimentSelectorModal-BeEOomw_.js.map +1 -0
- package/dist/{SettingsModal-W3TYfnP9.js → SettingsModal-Dphh3RrI.js} +4 -4
- package/dist/SettingsModal-Dphh3RrI.js.map +1 -0
- package/dist/{EmptyState-NBqf2_8z.js → Skeleton-DxtwJ5gt.js} +110 -92
- package/dist/Skeleton-DxtwJ5gt.js.map +1 -0
- package/dist/__tests__/components/ExperimentTimeline.test.d.ts +1 -0
- package/dist/__tests__/composables/useListReorder.test.d.ts +1 -0
- package/dist/__tests__/styles/cssTokens.test.d.ts +1 -0
- package/dist/components/ActionMenu.vue.d.ts +1 -1
- package/dist/components/AppPluginSwitcher.vue.d.ts +2 -0
- package/dist/components/AuditTrail.vue.d.ts +13 -0
- package/dist/components/BatchProgressList.vue.d.ts +17 -0
- package/dist/components/DataFrame.vue.d.ts +14 -2
- package/dist/components/DatePicker.vue.d.ts +1 -1
- package/dist/components/DateTimePicker.vue.d.ts +2 -0
- package/dist/components/ExperimentDataViewer.vue.d.ts +26 -1
- package/dist/components/ExperimentPopover.vue.d.ts +1 -0
- package/dist/components/ExperimentTimeline.vue.d.ts +29 -1
- package/dist/components/LcmsSequenceTable.vue.d.ts +29 -1
- package/dist/components/ProgressBar.vue.d.ts +1 -1
- package/dist/components/ProtocolStepEditor.vue.d.ts +1 -0
- package/dist/components/ReagentList.vue.d.ts +29 -1
- package/dist/components/SampleHierarchyTree.vue.d.ts +29 -1
- package/dist/components/SampleSelector.drag.d.ts +13 -0
- package/dist/components/SampleSelector.tree.d.ts +2 -0
- package/dist/components/SampleSelector.vue.d.ts +34 -1
- package/dist/components/ScheduleCalendar.vue.d.ts +20 -0
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -0
- package/dist/components/TimePicker.vue.d.ts +1 -0
- package/dist/components/WellPlate.geometry.d.ts +15 -0
- package/dist/components/WellPlate.interaction.d.ts +13 -0
- package/dist/components/WellPlate.vue.d.ts +29 -0
- package/dist/components/index.js +6 -6
- package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -0
- package/dist/components/internal/DataStateInternal.vue.d.ts +43 -0
- package/dist/{components-B80gV0P6.js → components-CgAUzF8F.js} +1930 -1688
- package/dist/components-CgAUzF8F.js.map +1 -0
- package/dist/composables/controlSchemaTypes.d.ts +34 -0
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.js +9 -9
- package/dist/composables/useDropdownState.d.ts +18 -2
- package/dist/composables/useEventStream.d.ts +9 -0
- package/dist/composables/useListReorder.d.ts +90 -0
- package/dist/composables/useMenuKeyboard.d.ts +2 -2
- package/dist/composables/usePluginConfig.d.ts +3 -1
- package/dist/composables/useRequestSyncState.d.ts +42 -2
- package/dist/composables/useScheduleDrag.d.ts +1 -0
- package/dist/{composables-C8mmD5ta.js → composables-B_pia3hO.js} +13 -69
- package/dist/composables-B_pia3hO.js.map +1 -0
- package/dist/{useApi-CDBq6z23.js → errors-_W0-LbA6.js} +102 -3
- package/dist/errors-_W0-LbA6.js.map +1 -0
- package/dist/{experiment-Y9Iz7KYi.js → experiment-Cwl9AYxw.js} +3 -8
- package/dist/experiment-Cwl9AYxw.js.map +1 -0
- package/dist/experiment-utils-DV3DrvE_.js.map +1 -1
- package/dist/index.js +14 -14
- package/dist/install.js +2 -7
- package/dist/install.js.map +1 -1
- package/dist/permissions.d.ts +42 -1
- package/dist/permissions.js +20 -10
- package/dist/permissions.js.map +1 -1
- package/dist/stores/index.d.ts +1 -1
- package/dist/stores/index.js +3 -3
- package/dist/stores/settings.d.ts +31 -1
- package/dist/styles.css +9572 -27877
- package/dist/templates/index.js +2 -4
- package/dist/{templates-CK72aKO1.js → templates-Qq113pyZ.js} +291 -85
- package/dist/templates-Qq113pyZ.js.map +1 -0
- package/dist/{useControlSchema-_OVhavKq.js → useControlSchema-aMZlb4Sc.js} +197 -1089
- package/dist/useControlSchema-aMZlb4Sc.js.map +1 -0
- package/dist/{useDropdownState-D5G4UvWd.js → useDropdownState-DitCtrTT.js} +21 -5
- package/dist/useDropdownState-DitCtrTT.js.map +1 -0
- package/dist/{useExperimentSelector-DSEckf_g.js → useExperimentSelector-I8HC8hUu.js} +3 -3
- package/dist/{useExperimentSelector-DSEckf_g.js.map → useExperimentSelector-I8HC8hUu.js.map} +1 -1
- package/dist/{useFormBuilder-Bd714C7y.js → useFormBuilder-TCxNnqeb.js} +2 -2
- package/dist/{useFormBuilder-Bd714C7y.js.map → useFormBuilder-TCxNnqeb.js.map} +1 -1
- package/dist/{useProtocolTemplates-Dq5Pzwyd.js → useProtocolTemplates-BmX_iVBR.js} +185 -157
- package/dist/useProtocolTemplates-BmX_iVBR.js.map +1 -0
- package/dist/utils/color.d.ts +13 -0
- package/dist/utils/errors.d.ts +9 -0
- package/package.json +1 -1
- package/src/__tests__/components/ActionMenu.test.ts +94 -0
- package/src/__tests__/components/DataFrame.test.ts +183 -11
- package/src/__tests__/components/DataStates.test.ts +176 -0
- package/src/__tests__/components/DatePicker.test.ts +76 -0
- package/src/__tests__/components/DateTimePicker.test.ts +36 -0
- package/src/__tests__/components/ExperimentTimeline.test.ts +63 -0
- package/src/__tests__/components/GroupAssigner.test.ts +53 -0
- package/src/__tests__/components/LcmsSequenceTable.test.ts +22 -0
- package/src/__tests__/components/RackEditor.test.ts +35 -0
- package/src/__tests__/components/ReagentList.test.ts +64 -2
- package/src/__tests__/components/RetiredStyles.test.ts +31 -0
- package/src/__tests__/components/SampleSelector.drag.test.ts +51 -1
- package/src/__tests__/components/ScheduleCalendar.test.ts +63 -0
- package/src/__tests__/components/SegmentedControl.test.ts +88 -0
- package/src/__tests__/components/TimePicker.test.ts +61 -0
- package/src/__tests__/components/WellPlate.interaction.test.ts +59 -0
- package/src/__tests__/composables/useDropdownState.test.ts +58 -1
- package/src/__tests__/composables/useListReorder.test.ts +145 -0
- package/src/__tests__/composables/useRequestSyncState.test.ts +140 -0
- package/src/__tests__/stores/settings.test.ts +71 -1
- package/src/__tests__/styles/cssTokens.test.ts +131 -0
- package/src/__tests__/utils/permissions.test.ts +68 -0
- package/src/components/ActionMenu.vue +45 -88
- package/src/components/AlertBox.vue +0 -4
- package/src/components/AppAvatarMenu.vue +2 -14
- package/src/components/AppLayout.vue +0 -4
- package/src/components/AppPluginSwitcher.story.vue +87 -0
- package/src/components/AppPluginSwitcher.vue +2 -5
- package/src/components/AppSidebar.vue +0 -4
- package/src/components/AppToastContainer.vue +6 -8
- package/src/components/AppTopBar.vue +0 -4
- package/src/components/ArtifactSaveDialog.vue +0 -4
- package/src/components/ArtifactSelectorModal.vue +0 -4
- package/src/components/AuditTrail.story.vue +18 -0
- package/src/components/AuditTrail.vue +27 -7
- package/src/components/AutoGroupModal.vue +0 -4
- package/src/components/Avatar.vue +0 -4
- package/src/components/BaseButton.vue +0 -4
- package/src/components/BaseCheckbox.vue +0 -4
- package/src/components/BaseInput.vue +0 -4
- package/src/components/BaseModal.vue +0 -4
- package/src/components/BasePill.vue +0 -4
- package/src/components/BaseRadioGroup.vue +0 -4
- package/src/components/BaseSelect.vue +0 -4
- package/src/components/BaseSlider.vue +0 -4
- package/src/components/BaseTabs.vue +0 -4
- package/src/components/BaseTextarea.vue +0 -4
- package/src/components/BaseToggle.vue +0 -4
- package/src/components/BatchProgressList.story.vue +18 -0
- package/src/components/BatchProgressList.vue +32 -4
- package/src/components/Breadcrumb.vue +0 -4
- package/src/components/Calendar.vue +0 -4
- package/src/components/ChartContainer.story.vue +1 -1
- package/src/components/ChartContainer.vue +0 -4
- package/src/components/ChemicalFormula.vue +0 -4
- package/src/components/CollapsibleCard.vue +0 -4
- package/src/components/ColorSlider.vue +0 -4
- package/src/components/ConcentrationInput.vue +0 -4
- package/src/components/ConfirmDialog.vue +0 -4
- package/src/components/DataFrame.story.vue +14 -0
- package/src/components/DataFrame.vue +97 -23
- package/src/components/DatePicker.vue +73 -27
- package/src/components/DateTimePicker.vue +45 -17
- package/src/components/Divider.vue +0 -4
- package/src/components/DoseCalculator.vue +0 -4
- package/src/components/DropdownButton.vue +2 -14
- package/src/components/EmptyState.vue +0 -4
- package/src/components/ExperimentCodeBadge.vue +0 -4
- package/src/components/ExperimentDataViewer.story.vue +3 -0
- package/src/components/ExperimentDataViewer.vue +33 -13
- package/src/components/ExperimentPopover.vue +2 -5
- package/src/components/ExperimentSelectorModal.vue +0 -4
- package/src/components/ExperimentTimeline.story.vue +18 -0
- package/src/components/ExperimentTimeline.vue +81 -75
- package/src/components/FileUploader.vue +0 -4
- package/src/components/FitPanel.vue +0 -4
- package/src/components/FormActions.story.vue +96 -0
- package/src/components/FormActions.vue +0 -4
- package/src/components/FormBuilder.vue +0 -4
- package/src/components/FormField.vue +0 -4
- package/src/components/FormulaInput.vue +0 -4
- package/src/components/GeneratedPathInput.story.vue +68 -0
- package/src/components/GeneratedPlotlyResult.story.vue +96 -0
- package/src/components/GeneratedPlotlyResult.vue +3 -3
- package/src/components/GeneratedPluginView.story.vue +145 -0
- package/src/components/GeneratedPluginView.vue +10 -10
- package/src/components/GeneratedResultRenderer.story.vue +117 -0
- package/src/components/GeneratedResultRenderer.vue +4 -4
- package/src/components/GroupAssigner.vue +67 -5
- package/src/components/IconButton.vue +0 -4
- package/src/components/InstrumentAlertLog.vue +0 -4
- package/src/components/InstrumentStateBadge.vue +0 -4
- package/src/components/InstrumentStatusCard.vue +0 -4
- package/src/components/JobsStatusTray.vue +0 -4
- package/src/components/LcmsSequenceTable.vue +81 -50
- package/src/components/LoadingSpinner.vue +0 -4
- package/src/components/MobileSupportGate.vue +0 -4
- package/src/components/MoleculeInput.vue +0 -4
- package/src/components/MultiSelect.vue +0 -4
- package/src/components/NumberInput.vue +0 -4
- package/src/components/PlateMapEditor.vue +3 -14
- package/src/components/PluginIcon.vue +0 -4
- package/src/components/ProgressBar.vue +0 -4
- package/src/components/ProtocolStepEditor.vue +2 -4
- package/src/components/RackEditor.vue +51 -70
- package/src/components/ReagentEditor.vue +0 -4
- package/src/components/ReagentList.story.vue +18 -0
- package/src/components/ReagentList.vue +127 -82
- package/src/components/ResourceCard.vue +0 -4
- package/src/components/RuntimeUpdateNotice.story.vue +49 -0
- package/src/components/RuntimeUpdateNotice.vue +4 -4
- package/src/components/SampleHierarchyTree.story.vue +18 -0
- package/src/components/SampleHierarchyTree.vue +32 -9
- package/src/components/SampleLegend.vue +2 -8
- package/src/components/SampleSelector.drag.ts +51 -0
- package/src/components/SampleSelector.story.vue +6 -0
- package/src/components/SampleSelector.tree.ts +2 -0
- package/src/components/SampleSelector.vue +49 -10
- package/src/components/SampleSelectorGroupNode.vue +19 -0
- package/src/components/ScheduleCalendar.story.vue +6 -0
- package/src/components/ScheduleCalendar.vue +82 -7
- package/src/components/ScientificNumber.vue +0 -4
- package/src/components/SectionCard.vue +0 -4
- package/src/components/SegmentedControl.vue +48 -5
- package/src/components/SequenceInput.vue +0 -4
- package/src/components/SequenceProgressBar.story.vue +110 -0
- package/src/components/SequenceProgressBar.vue +0 -4
- package/src/components/SettingsModal.vue +0 -4
- package/src/components/Skeleton.vue +0 -4
- package/src/components/SmartGroupFieldRecipe.vue +8 -6
- package/src/components/SmartGroupManual.vue +0 -4
- package/src/components/StatusIndicator.vue +0 -4
- package/src/components/StepWizard.vue +0 -4
- package/src/components/TagsInput.vue +4 -6
- package/src/components/ThemeToggle.vue +0 -4
- package/src/components/TimePicker.vue +36 -7
- package/src/components/TimeRangeInput.vue +0 -4
- package/src/components/Tooltip.story.vue +3 -3
- package/src/components/Tooltip.vue +0 -4
- package/src/components/UnitInput.vue +0 -4
- package/src/components/WellPlate.geometry.ts +60 -1
- package/src/components/WellPlate.interaction.ts +44 -0
- package/src/components/WellPlate.story.vue +8 -0
- package/src/components/WellPlate.vue +53 -7
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +2 -5
- package/src/components/internal/AppTopBarPillNavInternal.vue +0 -4
- package/src/components/internal/DataStateInternal.vue +74 -0
- package/src/components/internal/FormSectionRenderer.vue +0 -4
- package/src/components/internal/WellEditPopupInternal.vue +0 -4
- package/src/composables/controlSchemaTypes.ts +34 -0
- package/src/composables/index.ts +10 -0
- package/src/composables/useDropdownState.ts +35 -5
- package/src/composables/useEventStream.ts +14 -2
- package/src/composables/useFileImport.ts +2 -7
- package/src/composables/useGeneratedAnalysis.ts +8 -9
- package/src/composables/useListReorder.story.vue +143 -0
- package/src/composables/useListReorder.ts +242 -0
- package/src/composables/useMenuKeyboard.ts +2 -2
- package/src/composables/useOptimisticMutation.ts +1 -15
- package/src/composables/usePluginConfig.ts +3 -1
- package/src/composables/useRackEditor.ts +2 -11
- package/src/composables/useRequestSyncState.ts +98 -28
- package/src/composables/useResourceCrud.ts +1 -15
- package/src/composables/useScheduleDrag.ts +37 -0
- package/src/composables/useWellPainting.ts +5 -29
- package/src/composables/useWellPlateEditor.ts +1 -11
- package/src/permissions.ts +48 -9
- package/src/stores/experiment.ts +2 -7
- package/src/stores/index.ts +9 -1
- package/src/stores/settings.ts +93 -1
- package/src/styles/components/audit-trail.css +5 -5
- package/src/styles/components/batch-progress-list.css +7 -0
- package/src/styles/components/color-slider.css +2 -2
- package/src/styles/components/data-state.css +30 -0
- package/src/styles/components/dataframe.css +29 -35
- package/src/styles/components/experiment-data-viewer.css +4 -4
- package/src/styles/components/experiment-timeline.css +3 -30
- package/src/styles/components/form-builder.css +5 -5
- package/src/styles/components/lcms-sequence-table.css +16 -1
- package/src/styles/components/multi-select.css +1 -1
- package/src/styles/components/reagent-list.css +38 -31
- package/src/styles/components/sample-hierarchy-tree.css +5 -19
- package/src/styles/components/sample-selector.css +9 -2
- package/src/styles/components/schedule-calendar.css +8 -0
- package/src/styles/components/scientific-number.css +1 -1
- package/src/styles/components/well-plate.css +8 -0
- package/src/styles/index.css +8 -0
- package/src/styles/variables.css +29 -0
- package/src/templates/builderPresetControls.ts +1 -1
- package/src/templates/componentPlateHelpers.ts +3 -1
- package/src/templates/doseResponseAdapters.ts +2 -13
- package/src/templates/elisaAssayCollectionBuilder.ts +2 -2
- package/src/templates/plateMapBuilder.ts +2 -2
- package/src/templates/qpcrPlateBuilder.ts +1 -1
- package/src/templates/templateValidators.ts +1 -1
- package/src/templates/timeCourseBuilder.ts +2 -2
- package/src/templates/wellPlateScreenCollectionBuilder.ts +2 -2
- package/src/utils/color.ts +51 -0
- package/src/utils/errors.ts +29 -0
- package/dist/EmptyState-NBqf2_8z.js.map +0 -1
- package/dist/ExperimentPopover-CfOdh5Rm.js.map +0 -1
- package/dist/ExperimentSelectorModal-DvIagrxY.js.map +0 -1
- package/dist/SettingsModal-W3TYfnP9.js.map +0 -1
- package/dist/components-B80gV0P6.js.map +0 -1
- package/dist/composables/autoGroupConstants.d.ts +0 -1
- package/dist/composables/autoGroupGrouping.d.ts +0 -3
- package/dist/composables/useAutoGroupInputSources.d.ts +0 -32
- package/dist/composables-C8mmD5ta.js.map +0 -1
- package/dist/experiment-Y9Iz7KYi.js.map +0 -1
- package/dist/templates/builderDefaults.d.ts +0 -1
- package/dist/templates/plateGeometry.d.ts +0 -4
- package/dist/templates-CK72aKO1.js.map +0 -1
- package/dist/useApi-CDBq6z23.js.map +0 -1
- package/dist/useControlSchema-_OVhavKq.js.map +0 -1
- package/dist/useDropdownState-D5G4UvWd.js.map +0 -1
- package/dist/useProtocolTemplates-Dq5Pzwyd.js.map +0 -1
- package/src/__tests__/composables/useAutoGroupInputSources.test.ts +0 -127
- package/src/composables/autoGroupConstants.ts +0 -4
- package/src/composables/autoGroupGrouping.ts +0 -148
- package/src/composables/useAutoGroupInputSources.ts +0 -147
- package/src/templates/builderDefaults.ts +0 -11
- package/src/templates/plateGeometry.ts +0 -62
- /package/dist/__tests__/{composables/useAutoGroupInputSources.test.d.ts → components/DataStates.test.d.ts} +0 -0
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { computed, ref } from 'vue'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import { useAutoGroup } from '../../composables/useAutoGroup'
|
|
4
|
-
import { useAutoGroupInputSources } from '../../composables/useAutoGroupInputSources'
|
|
5
|
-
import type { WizardStep } from '../../types/components'
|
|
6
|
-
|
|
7
|
-
function makeSources(options: {
|
|
8
|
-
samples?: string[]
|
|
9
|
-
experimentId?: number
|
|
10
|
-
designData?: Record<string, unknown>
|
|
11
|
-
apiGet?: (url: string) => Promise<Record<string, unknown>>
|
|
12
|
-
} = {}) {
|
|
13
|
-
const autoGroup = useAutoGroup()
|
|
14
|
-
const currentStep = ref(0)
|
|
15
|
-
const dynamicSteps = ref<WizardStep[]>([
|
|
16
|
-
{ id: 'input', label: 'Input' },
|
|
17
|
-
{ id: 'fields', label: 'Fields' },
|
|
18
|
-
{ id: 'preview', label: 'Preview' },
|
|
19
|
-
])
|
|
20
|
-
const sources = useAutoGroupInputSources({
|
|
21
|
-
autoGroup,
|
|
22
|
-
samples: computed(() => options.samples ?? []),
|
|
23
|
-
experimentId: computed(() => options.experimentId),
|
|
24
|
-
designData: computed(() => options.designData),
|
|
25
|
-
currentStep,
|
|
26
|
-
dynamicSteps,
|
|
27
|
-
api: {
|
|
28
|
-
get: async <T>(url: string): Promise<T> => {
|
|
29
|
-
const value = options.apiGet
|
|
30
|
-
? await options.apiGet(url)
|
|
31
|
-
: { data: undefined }
|
|
32
|
-
return value as T
|
|
33
|
-
},
|
|
34
|
-
},
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
return { autoGroup, currentStep, dynamicSteps, sources }
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
describe('useAutoGroupInputSources', () => {
|
|
41
|
-
it('initializes pasted sample input when samples are provided', async () => {
|
|
42
|
-
const { autoGroup, currentStep, sources } = makeSources({
|
|
43
|
-
samples: ['S1_Control', 'S2_Treated'],
|
|
44
|
-
})
|
|
45
|
-
currentStep.value = 2
|
|
46
|
-
|
|
47
|
-
await sources.initializeInputSources()
|
|
48
|
-
|
|
49
|
-
expect(currentStep.value).toBe(0)
|
|
50
|
-
expect(autoGroup.rawText.value).toBe('S1_Control\nS2_Treated')
|
|
51
|
-
expect(sources.csvFileName.value).toBe('')
|
|
52
|
-
expect(sources.experimentError.value).toBeNull()
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
it('loads experiment design data and advances to the fields step', async () => {
|
|
56
|
-
const { autoGroup, currentStep, sources } = makeSources({
|
|
57
|
-
designData: {
|
|
58
|
-
samples: [
|
|
59
|
-
{ sample_name: 'S1', sample_type: 'sample', conditions: { Treatment: 'Control' } },
|
|
60
|
-
{ sample_name: 'S2', sample_type: 'sample', conditions: { Treatment: 'Drug' } },
|
|
61
|
-
],
|
|
62
|
-
},
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
await sources.initializeInputSources()
|
|
66
|
-
|
|
67
|
-
expect(autoGroup.inputMode.value).toBe('experiment')
|
|
68
|
-
expect(autoGroup.csvData.value?.rows).toHaveLength(2)
|
|
69
|
-
expect(sources.experimentFieldNames.value).toEqual(['Treatment', 'sample_type'])
|
|
70
|
-
expect(currentStep.value).toBe(1)
|
|
71
|
-
expect(sources.experimentLoading.value).toBe(false)
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('filters loaded experiment metadata to the provided sample subset', async () => {
|
|
75
|
-
const { autoGroup, sources } = makeSources({
|
|
76
|
-
samples: ['S2', 'MissingFromDesign'],
|
|
77
|
-
designData: {
|
|
78
|
-
samples: [
|
|
79
|
-
{ sample_name: 'S1', sample_type: 'sample', conditions: { Tissue: 'Kidney', Treatment: 'Control' } },
|
|
80
|
-
{ sample_name: 'S2', sample_type: 'sample', conditions: { Tissue: 'Liver', Treatment: 'Drug' } },
|
|
81
|
-
],
|
|
82
|
-
},
|
|
83
|
-
})
|
|
84
|
-
|
|
85
|
-
await sources.initializeInputSources()
|
|
86
|
-
|
|
87
|
-
expect(autoGroup.samples.value).toEqual(['S2', 'MissingFromDesign'])
|
|
88
|
-
expect(autoGroup.groups.value.map(g => g.name).sort()).toEqual([
|
|
89
|
-
'Liver / Drug',
|
|
90
|
-
'Outlier',
|
|
91
|
-
])
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it('parses uploaded CSV files and can clear them back to paste mode', async () => {
|
|
95
|
-
const { autoGroup, sources } = makeSources()
|
|
96
|
-
const file = {
|
|
97
|
-
name: 'samples.csv',
|
|
98
|
-
text: async () => 'sample_name,Treatment\nS1,Control\nS2,Drug',
|
|
99
|
-
} as File
|
|
100
|
-
|
|
101
|
-
await sources.processFile(file)
|
|
102
|
-
|
|
103
|
-
expect(autoGroup.inputMode.value).toBe('csv')
|
|
104
|
-
expect(autoGroup.csvData.value?.rows).toHaveLength(2)
|
|
105
|
-
expect(sources.csvFileName.value).toBe('samples.csv')
|
|
106
|
-
|
|
107
|
-
sources.clearCsvFile()
|
|
108
|
-
|
|
109
|
-
expect(autoGroup.inputMode.value).toBe('paste')
|
|
110
|
-
expect(autoGroup.csvData.value).toBeNull()
|
|
111
|
-
expect(sources.csvFileName.value).toBe('')
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
it('surfaces API errors when experiment metadata cannot be fetched', async () => {
|
|
115
|
-
const { sources } = makeSources({
|
|
116
|
-
experimentId: 42,
|
|
117
|
-
apiGet: async () => {
|
|
118
|
-
throw new Error('offline')
|
|
119
|
-
},
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
await sources.initializeInputSources()
|
|
123
|
-
|
|
124
|
-
expect(sources.experimentLoading.value).toBe(false)
|
|
125
|
-
expect(sources.experimentError.value).toBe('offline')
|
|
126
|
-
})
|
|
127
|
-
})
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AutoGroupResult,
|
|
3
|
-
ColumnInfo,
|
|
4
|
-
MetadataRow,
|
|
5
|
-
OutlierAction,
|
|
6
|
-
ParsedCsvData,
|
|
7
|
-
} from '../types/auto-group'
|
|
8
|
-
import type { SampleGroup } from '../types/components'
|
|
9
|
-
import { DEFAULT_COLORS } from './autoGroupConstants'
|
|
10
|
-
|
|
11
|
-
export function computeGroups(
|
|
12
|
-
allSamples: string[],
|
|
13
|
-
columns: ColumnInfo[],
|
|
14
|
-
enabledFields: Set<number>,
|
|
15
|
-
outlierActions: Map<number, OutlierAction>,
|
|
16
|
-
delimiter: string,
|
|
17
|
-
minFieldCount: number,
|
|
18
|
-
): AutoGroupResult {
|
|
19
|
-
const excludedSamples: string[] = []
|
|
20
|
-
const qcSamples: string[] = []
|
|
21
|
-
const conformingSamples: string[] = []
|
|
22
|
-
|
|
23
|
-
for (let i = 0; i < allSamples.length; i++) {
|
|
24
|
-
const action = outlierActions.get(i)
|
|
25
|
-
if (action === 'exclude') {
|
|
26
|
-
excludedSamples.push(allSamples[i])
|
|
27
|
-
} else if (action === 'qc') {
|
|
28
|
-
qcSamples.push(allSamples[i])
|
|
29
|
-
} else {
|
|
30
|
-
conformingSamples.push(allSamples[i])
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Build group map
|
|
35
|
-
const groupMap = new Map<string, string[]>()
|
|
36
|
-
const metadata: MetadataRow[] = []
|
|
37
|
-
const enabledIndices = [...enabledFields].sort((a, b) => a - b)
|
|
38
|
-
const suffixCount = minFieldCount - 1
|
|
39
|
-
|
|
40
|
-
for (const sample of conformingSamples) {
|
|
41
|
-
const parts = sample.split(delimiter)
|
|
42
|
-
const splitAt = Math.max(1, parts.length - suffixCount)
|
|
43
|
-
const row = [
|
|
44
|
-
parts.slice(0, splitAt).join(delimiter),
|
|
45
|
-
...parts.slice(splitAt),
|
|
46
|
-
]
|
|
47
|
-
|
|
48
|
-
// Build group key from enabled columns
|
|
49
|
-
const keyParts: string[] = []
|
|
50
|
-
for (const idx of enabledIndices) {
|
|
51
|
-
if (idx < row.length && idx < columns.length) {
|
|
52
|
-
keyParts.push(row[idx])
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
const groupKey = keyParts.join(' / ')
|
|
56
|
-
|
|
57
|
-
const group = groupMap.get(groupKey)
|
|
58
|
-
if (group) {
|
|
59
|
-
group.push(sample)
|
|
60
|
-
} else {
|
|
61
|
-
groupMap.set(groupKey, [sample])
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Build metadata row with ALL columns
|
|
65
|
-
const fields: Record<string, string> = {}
|
|
66
|
-
for (const col of columns) {
|
|
67
|
-
if (col.index < row.length) {
|
|
68
|
-
fields[col.name] = row[col.index]
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
metadata.push({ sampleName: sample, fields, group: groupKey })
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// Convert to SampleGroup[]
|
|
75
|
-
const groups: SampleGroup[] = []
|
|
76
|
-
let colorIdx = 0
|
|
77
|
-
for (const [name, samples] of groupMap) {
|
|
78
|
-
groups.push({
|
|
79
|
-
name,
|
|
80
|
-
color: DEFAULT_COLORS[colorIdx % DEFAULT_COLORS.length],
|
|
81
|
-
samples,
|
|
82
|
-
})
|
|
83
|
-
colorIdx++
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// QC group
|
|
87
|
-
if (qcSamples.length > 0) {
|
|
88
|
-
groups.push({
|
|
89
|
-
name: 'QC',
|
|
90
|
-
color: '#6B7280',
|
|
91
|
-
samples: qcSamples,
|
|
92
|
-
})
|
|
93
|
-
for (const sample of qcSamples) {
|
|
94
|
-
metadata.push({ sampleName: sample, fields: {}, group: 'QC' })
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return { groups, metadata, excludedSamples }
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
export function computeGroupsFromCsv(
|
|
102
|
-
csvData: ParsedCsvData,
|
|
103
|
-
columns: ColumnInfo[],
|
|
104
|
-
enabledFields: Set<number>,
|
|
105
|
-
): AutoGroupResult {
|
|
106
|
-
const groupMap = new Map<string, string[]>()
|
|
107
|
-
const metadata: MetadataRow[] = []
|
|
108
|
-
const enabledCols = columns
|
|
109
|
-
.filter(c => enabledFields.has(c.index))
|
|
110
|
-
.sort((a, b) => a.index - b.index)
|
|
111
|
-
|
|
112
|
-
for (const row of csvData.rows) {
|
|
113
|
-
const sampleName = row[csvData.sampleColumn]
|
|
114
|
-
|
|
115
|
-
// Build group key from enabled CSV column values
|
|
116
|
-
// Use originalName for CSV row lookup (survives user renames), display name for group key
|
|
117
|
-
const keyParts = enabledCols.map(col => row[col.originalName ?? col.name])
|
|
118
|
-
const groupKey = keyParts.join(' / ')
|
|
119
|
-
|
|
120
|
-
const group = groupMap.get(groupKey)
|
|
121
|
-
if (group) {
|
|
122
|
-
group.push(sampleName)
|
|
123
|
-
} else {
|
|
124
|
-
groupMap.set(groupKey, [sampleName])
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Build metadata row with ALL columns - use display name as key, original for lookup
|
|
128
|
-
const fields: Record<string, string> = {}
|
|
129
|
-
for (const col of columns) {
|
|
130
|
-
fields[col.name] = row[col.originalName ?? col.name]
|
|
131
|
-
}
|
|
132
|
-
metadata.push({ sampleName, fields, group: groupKey })
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// Convert to SampleGroup[]
|
|
136
|
-
const groups: SampleGroup[] = []
|
|
137
|
-
let colorIdx = 0
|
|
138
|
-
for (const [name, samples] of groupMap) {
|
|
139
|
-
groups.push({
|
|
140
|
-
name,
|
|
141
|
-
color: DEFAULT_COLORS[colorIdx % DEFAULT_COLORS.length],
|
|
142
|
-
samples,
|
|
143
|
-
})
|
|
144
|
-
colorIdx++
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return { groups, metadata, excludedSamples: [] }
|
|
148
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { computed, nextTick, ref, type Ref } from 'vue'
|
|
2
|
-
import { parseCSV, type useAutoGroup } from './useAutoGroup'
|
|
3
|
-
import { useApi, type UseApiReturn } from './useApi'
|
|
4
|
-
import type { WizardStep } from '../types/components'
|
|
5
|
-
|
|
6
|
-
type AutoGroupController = ReturnType<typeof useAutoGroup>
|
|
7
|
-
type ReadableRef<T> = Readonly<Ref<T>>
|
|
8
|
-
type AutoGroupApi = Pick<UseApiReturn, 'get'>
|
|
9
|
-
|
|
10
|
-
export interface UseAutoGroupInputSourcesOptions {
|
|
11
|
-
autoGroup: AutoGroupController
|
|
12
|
-
samples: ReadableRef<readonly string[]>
|
|
13
|
-
experimentId: ReadableRef<number | undefined>
|
|
14
|
-
designData: ReadableRef<Record<string, unknown> | undefined>
|
|
15
|
-
currentStep: Ref<number>
|
|
16
|
-
dynamicSteps: ReadableRef<readonly WizardStep[]>
|
|
17
|
-
api?: AutoGroupApi
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Manages AutoGroupModal input sources: pasted samples, CSV/TSV files, and experiment metadata. */
|
|
21
|
-
export function useAutoGroupInputSources(options: UseAutoGroupInputSourcesOptions) {
|
|
22
|
-
const api = options.api ?? useApi()
|
|
23
|
-
const isDragOver = ref(false)
|
|
24
|
-
const csvFileName = ref('')
|
|
25
|
-
const experimentLoading = ref(false)
|
|
26
|
-
const experimentError = ref<string | null>(null)
|
|
27
|
-
const csvError = ref<string | null>(null)
|
|
28
|
-
|
|
29
|
-
const experimentFieldNames = computed(() =>
|
|
30
|
-
options.autoGroup.csvData.value?.columns.filter(c => c !== 'sample_name') ?? [],
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
async function initializeInputSources() {
|
|
34
|
-
options.autoGroup.reset()
|
|
35
|
-
options.currentStep.value = 0
|
|
36
|
-
resetInputSourceState()
|
|
37
|
-
|
|
38
|
-
if (options.experimentId.value || options.designData.value) {
|
|
39
|
-
await fetchExperimentData()
|
|
40
|
-
} else if (options.samples.value.length > 0) {
|
|
41
|
-
options.autoGroup.rawText.value = options.samples.value.join('\n')
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function resetInputSourceState() {
|
|
46
|
-
csvFileName.value = ''
|
|
47
|
-
experimentError.value = null
|
|
48
|
-
csvError.value = null
|
|
49
|
-
isDragOver.value = false
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async function fetchExperimentData() {
|
|
53
|
-
if (!options.experimentId.value && !options.designData.value) return
|
|
54
|
-
|
|
55
|
-
options.autoGroup.inputMode.value = 'experiment'
|
|
56
|
-
experimentLoading.value = true
|
|
57
|
-
experimentError.value = null
|
|
58
|
-
|
|
59
|
-
try {
|
|
60
|
-
const pluginData = await loadExperimentPluginData()
|
|
61
|
-
|
|
62
|
-
if (!pluginData || typeof pluginData !== 'object') {
|
|
63
|
-
experimentError.value = 'No design data found for this experiment'
|
|
64
|
-
return
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const success = options.autoGroup.loadExperimentData(pluginData, { samples: options.samples.value })
|
|
68
|
-
if (!success) {
|
|
69
|
-
experimentError.value = 'No sample metadata found in this experiment'
|
|
70
|
-
return
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
await nextTick()
|
|
74
|
-
const fieldsIdx = options.dynamicSteps.value.findIndex(s => s.id === 'fields')
|
|
75
|
-
if (fieldsIdx >= 0) {
|
|
76
|
-
options.currentStep.value = fieldsIdx
|
|
77
|
-
}
|
|
78
|
-
} catch (e) {
|
|
79
|
-
experimentError.value = e instanceof Error ? e.message : 'Failed to fetch experiment data'
|
|
80
|
-
} finally {
|
|
81
|
-
experimentLoading.value = false
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
async function loadExperimentPluginData(): Promise<Record<string, unknown> | undefined> {
|
|
86
|
-
if (options.designData.value) {
|
|
87
|
-
return options.designData.value
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const result = await api.get<Record<string, unknown>>(
|
|
91
|
-
`/experiments/${options.experimentId.value}/data`,
|
|
92
|
-
)
|
|
93
|
-
return result.data as Record<string, unknown> | undefined
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function handleFileDrop(event: DragEvent) {
|
|
97
|
-
event.preventDefault()
|
|
98
|
-
isDragOver.value = false
|
|
99
|
-
const files = event.dataTransfer?.files
|
|
100
|
-
if (files && files.length > 0) {
|
|
101
|
-
void processFile(files[0])
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function handleFileInput(event: Event) {
|
|
106
|
-
const target = event.target as HTMLInputElement
|
|
107
|
-
if (target.files && target.files.length > 0) {
|
|
108
|
-
void processFile(target.files[0])
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
async function processFile(file: File) {
|
|
113
|
-
if (!file.name.endsWith('.csv') && !file.name.endsWith('.tsv')) return
|
|
114
|
-
|
|
115
|
-
csvError.value = null
|
|
116
|
-
try {
|
|
117
|
-
const text = await file.text()
|
|
118
|
-
const parsed = parseCSV(text)
|
|
119
|
-
options.autoGroup.inputMode.value = 'csv'
|
|
120
|
-
options.autoGroup.csvData.value = parsed
|
|
121
|
-
csvFileName.value = file.name
|
|
122
|
-
} catch (e) {
|
|
123
|
-
csvError.value = e instanceof Error ? e.message : 'Failed to parse file'
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function clearCsvFile() {
|
|
128
|
-
options.autoGroup.csvData.value = null
|
|
129
|
-
options.autoGroup.inputMode.value = 'paste'
|
|
130
|
-
csvFileName.value = ''
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
return {
|
|
134
|
-
isDragOver,
|
|
135
|
-
csvFileName,
|
|
136
|
-
experimentLoading,
|
|
137
|
-
experimentError,
|
|
138
|
-
csvError,
|
|
139
|
-
experimentFieldNames,
|
|
140
|
-
initializeInputSources,
|
|
141
|
-
fetchExperimentData,
|
|
142
|
-
handleFileDrop,
|
|
143
|
-
handleFileInput,
|
|
144
|
-
processFile,
|
|
145
|
-
clearCsvFile,
|
|
146
|
-
}
|
|
147
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import type { WellPlateFormat } from '../types'
|
|
2
|
-
|
|
3
|
-
const PLATE_DIMENSIONS: Record<number, readonly [number, number]> = {
|
|
4
|
-
6: [2, 3],
|
|
5
|
-
12: [3, 4],
|
|
6
|
-
24: [4, 6],
|
|
7
|
-
48: [6, 8],
|
|
8
|
-
54: [6, 9],
|
|
9
|
-
96: [8, 12],
|
|
10
|
-
384: [16, 24],
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function wellIdsForFormat(format: number): string[] {
|
|
14
|
-
const dimensions = PLATE_DIMENSIONS[format]
|
|
15
|
-
if (!dimensions) {
|
|
16
|
-
throw new Error(`Unsupported plate format '${format}'.`)
|
|
17
|
-
}
|
|
18
|
-
const [rows, cols] = dimensions
|
|
19
|
-
return Array.from({ length: rows }, (_, row) =>
|
|
20
|
-
Array.from({ length: cols }, (_col, col) => `${rowLabel(row)}${col + 1}`)
|
|
21
|
-
).flat()
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function assertWellInFormat(wellId: string, format: number): void {
|
|
25
|
-
const dimensions = PLATE_DIMENSIONS[format]
|
|
26
|
-
if (!dimensions) {
|
|
27
|
-
throw new Error(`Unsupported plate format '${format}'.`)
|
|
28
|
-
}
|
|
29
|
-
const match = /^([A-Z]+)([1-9][0-9]*)$/.exec(wellId.toUpperCase())
|
|
30
|
-
if (!match) {
|
|
31
|
-
throw new Error(`Invalid well id '${wellId}'.`)
|
|
32
|
-
}
|
|
33
|
-
const row = rowIndex(match[1])
|
|
34
|
-
const col = Number(match[2]) - 1
|
|
35
|
-
const [rows, cols] = dimensions
|
|
36
|
-
if (row < 0 || col < 0 || row >= rows || col >= cols) {
|
|
37
|
-
throw new Error(`Well '${wellId}' is outside a ${format}-well plate.`)
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function readPlateFormat(value: unknown, fallback: WellPlateFormat): WellPlateFormat {
|
|
42
|
-
const parsed = typeof value === 'number'
|
|
43
|
-
? value
|
|
44
|
-
: typeof value === 'string'
|
|
45
|
-
? Number(value)
|
|
46
|
-
: Number.NaN
|
|
47
|
-
return parsed in PLATE_DIMENSIONS ? (parsed as WellPlateFormat) : fallback
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function rowLabel(index: number): string {
|
|
51
|
-
let label = ''
|
|
52
|
-
let current = index
|
|
53
|
-
while (true) {
|
|
54
|
-
label = String.fromCharCode(65 + (current % 26)) + label
|
|
55
|
-
current = Math.floor(current / 26) - 1
|
|
56
|
-
if (current < 0) return label
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function rowIndex(label: string): number {
|
|
61
|
-
return [...label].reduce((value, char) => value * 26 + char.charCodeAt(0) - 64, 0) - 1
|
|
62
|
-
}
|
|
File without changes
|