@morscherlab/mint-sdk 1.2.10 → 1.3.0-beta.10
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 +45 -137
- package/dist/ResourcePickerListInternal-CFAw4vTZ.js +605 -0
- package/dist/ResourcePickerListInternal-CFAw4vTZ.js.map +1 -0
- package/dist/__stories__/experiment-helpers.d.ts +12 -1
- package/dist/__tests__/components/ScientificNumber.test.d.ts +1 -0
- package/dist/__tests__/components/SequenceInput.test.d.ts +1 -0
- package/dist/__tests__/components/Skeleton.test.d.ts +1 -0
- package/dist/__tests__/components/SmartGroupFieldRecipe.test.d.ts +1 -0
- package/dist/__tests__/components/SmartGroupModal.test.d.ts +1 -0
- package/dist/__tests__/components/StepWizard.test.d.ts +1 -0
- package/dist/__tests__/components/TimeRangeInput.test.d.ts +1 -0
- package/dist/__tests__/components/UnitInput.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.keyboard.test.d.ts +1 -0
- package/dist/__tests__/components/WellPlate.sampleTypeIndicator.test.d.ts +1 -0
- package/dist/__tests__/composables/useChemicalFormula.test.d.ts +1 -0
- package/dist/__tests__/composables/useRackEditor.test.d.ts +1 -0
- package/dist/__tests__/composables/useSequenceUtils.test.d.ts +1 -0
- package/dist/__tests__/styles/baseControlsRules.test.d.ts +1 -0
- package/dist/__tests__/styles/controlRefreshRules.test.d.ts +1 -0
- package/dist/__tests__/styles/darkModeRules.test.d.ts +1 -0
- package/dist/__tests__/styles/menusShellRefresh.test.d.ts +1 -0
- package/dist/__tests__/templates/templatesDeprecation.test.d.ts +1 -0
- package/dist/__tests__/utils/plotlyTemplate.test.d.ts +1 -0
- package/dist/__tests__/utils/sampleTypeMarker.test.d.ts +1 -0
- package/dist/{analysisArtifactTypes-DQ0MjwHZ.js → analysisArtifactTypes-Cy3D0Ipg.js} +1 -1
- package/dist/{analysisArtifactTypes-DQ0MjwHZ.js.map → analysisArtifactTypes-Cy3D0Ipg.js.map} +1 -1
- package/dist/components/ActionMenu.vue.d.ts +17 -5
- package/dist/components/AdductText.vue.d.ts +3 -2
- package/dist/components/AlertBox.vue.d.ts +11 -14
- package/dist/components/AppAvatarMenu.vue.d.ts +18 -28
- package/dist/components/AppContainer.vue.d.ts +7 -11
- package/dist/components/AppLayout.vue.d.ts +11 -13
- package/dist/components/AppSidebar.vue.d.ts +30 -28
- package/dist/components/AppToastContainer.vue.d.ts +2 -1
- package/dist/components/AppTopBar.vue.d.ts +45 -44
- package/dist/components/ArtifactSaveDialog.vue.d.ts +3 -2
- package/dist/components/ArtifactSelectorModal.vue.d.ts +5 -6
- package/dist/components/AuditTrail.vue.d.ts +17 -18
- package/dist/components/AutoGroupModal.vue.d.ts +4 -3
- package/dist/components/Avatar.vue.d.ts +2 -1
- package/dist/components/BaseButton.vue.d.ts +11 -13
- package/dist/components/BaseCheckbox.vue.d.ts +7 -1
- package/dist/components/BaseInput.vue.d.ts +15 -2
- package/dist/components/BaseModal.vue.d.ts +20 -23
- package/dist/components/BasePill.vue.d.ts +10 -13
- package/dist/components/BaseRadioGroup.vue.d.ts +2 -1
- package/dist/components/BaseSelect.vue.d.ts +3 -2
- package/dist/components/BaseSlider.vue.d.ts +18 -1
- package/dist/components/BaseTabs.vue.d.ts +2 -1
- package/dist/components/BaseTextarea.vue.d.ts +2 -1
- package/dist/components/BaseToggle.vue.d.ts +2 -1
- package/dist/components/BatchProgressList.vue.d.ts +22 -26
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +7 -14
- package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +6 -13
- package/dist/components/BioTemplatePresetWorkspaceView.vue.d.ts +7 -14
- package/dist/components/BioTemplateRenderer.vue.d.ts +2 -1
- package/dist/components/Breadcrumb.vue.d.ts +13 -16
- package/dist/components/Calendar.vue.d.ts +21 -23
- package/dist/components/ChartContainer.vue.d.ts +19 -25
- package/dist/components/ChemicalFormula.vue.d.ts +7 -11
- package/dist/components/CollapsibleCard.vue.d.ts +7 -11
- package/dist/components/ComponentBindingRenderer.vue.d.ts +2 -1
- package/dist/components/ConcentrationInput.vue.d.ts +2 -1
- package/dist/components/ConfirmDialog.vue.d.ts +10 -13
- package/dist/components/ControlWorkspaceView.vue.d.ts +9 -18
- package/dist/components/DataFrame.vue.d.ts +27 -24
- package/dist/components/DatePicker.vue.d.ts +3 -6
- package/dist/components/DateTimePicker.vue.d.ts +3 -6
- package/dist/components/Divider.vue.d.ts +3 -2
- package/dist/components/DoseCalculator.vue.d.ts +2 -1
- package/dist/components/DoseCalculatorVolumeField.vue.d.ts +2 -1
- package/dist/components/DoseDesignWorkspaceView.vue.d.ts +9 -18
- package/dist/components/EmptyState.vue.d.ts +9 -12
- package/dist/components/ExperimentCodeBadge.vue.d.ts +2 -1
- package/dist/components/ExperimentDataViewer.vue.d.ts +13 -15
- package/dist/components/ExperimentPopover.vue.d.ts +3 -5
- package/dist/components/ExperimentSelectorModal.vue.d.ts +3 -4
- package/dist/components/ExperimentTimeline.vue.d.ts +13 -15
- package/dist/components/FilePicker.vue.d.ts +10 -5
- package/dist/components/FileUploader.vue.d.ts +8 -16
- package/dist/components/FitPanel.vue.d.ts +26 -16
- package/dist/components/FormActions.vue.d.ts +2 -1
- package/dist/components/FormBuilder.vue.d.ts +54 -250
- package/dist/components/FormField.vue.d.ts +14 -16
- package/dist/components/FormulaInput.vue.d.ts +2 -1
- package/dist/components/GeneratedPathInput.vue.d.ts +6 -5
- package/dist/components/GeneratedPlotlyResult.vue.d.ts +2 -1
- package/dist/components/GeneratedPluginView.vue.d.ts +2 -1
- package/dist/components/GeneratedResultRenderer.vue.d.ts +2 -1
- package/dist/components/GroupAssigner.vue.d.ts +2 -1
- package/dist/components/IconButton.vue.d.ts +7 -11
- package/dist/components/InstrumentStateBadge.vue.d.ts +4 -3
- package/dist/components/JobsStatusTray.vue.d.ts +13 -29
- package/dist/components/LayoutResizeHandle.vue.d.ts +2 -1
- package/dist/components/LoadingSpinner.vue.d.ts +2 -1
- package/dist/components/MobileSupportGate.vue.d.ts +5 -13
- package/dist/components/MoleculeInput.vue.d.ts +3 -4
- package/dist/components/MultiSelect.vue.d.ts +2 -1
- package/dist/components/NumberInput.vue.d.ts +2 -1
- package/dist/components/PlateEditor.vue.d.ts +147 -0
- package/dist/components/PlotlyChart.vue.d.ts +21 -31
- package/dist/components/PluginIcon.vue.d.ts +2 -1
- package/dist/components/PluginWorkspaceView.props.d.ts +2 -6
- package/dist/components/PluginWorkspaceView.vue.d.ts +23 -51
- package/dist/components/ProgressBar.vue.d.ts +4 -9
- package/dist/components/ProtocolStepEditor.vue.d.ts +7 -5
- package/dist/components/ProtocolStepParameterField.vue.d.ts +2 -1
- package/dist/components/ReagentEditor.vue.d.ts +2 -1
- package/dist/components/ReagentList.presentation.d.ts +2 -1
- package/dist/components/ReagentList.vue.d.ts +15 -17
- package/dist/components/ResourceCard.vue.d.ts +18 -17
- package/dist/components/RuntimeUpdateNotice.vue.d.ts +3 -2
- package/dist/components/SampleHierarchyTree.vue.d.ts +21 -15
- package/dist/components/SampleLegend.vue.d.ts +2 -1
- package/dist/components/SampleSelector.vue.d.ts +15 -21
- package/dist/components/SampleSelectorGroupNode.vue.d.ts +2 -1
- package/dist/components/SampleSelectorSampleRow.vue.d.ts +2 -5
- package/dist/components/ScheduleCalendar.vue.d.ts +34 -36
- package/dist/components/ScientificNumber.vue.d.ts +3 -2
- package/dist/components/SearchableSelect.vue.d.ts +14 -32
- package/dist/components/SecretInput.vue.d.ts +2 -1
- package/dist/components/SectionCard.vue.d.ts +13 -14
- package/dist/components/SegmentedControl.vue.d.ts +2 -1
- package/dist/components/SequenceInput.vue.d.ts +10 -21
- package/dist/components/SequenceProgressBar.vue.d.ts +2 -1
- package/dist/components/SettingsModal.schema.d.ts +1 -1
- package/dist/components/SettingsModal.vue.d.ts +28 -25
- package/dist/components/Skeleton.vue.d.ts +2 -1
- package/dist/components/SmartGroup.types.d.ts +2 -2
- package/dist/components/SmartGroupFieldRecipe.groups.d.ts +2 -0
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +4 -7
- package/dist/components/SmartGroupManual.vue.d.ts +3 -2
- package/dist/components/SmartGroupModal.vue.d.ts +6 -4
- package/dist/components/StatusIndicator.vue.d.ts +2 -1
- package/dist/components/StepWizard.vue.d.ts +26 -27
- package/dist/components/TagsInput.vue.d.ts +3 -5
- package/dist/components/ThemeToggle.vue.d.ts +2 -1
- package/dist/components/TimePicker.vue.d.ts +6 -6
- package/dist/components/TimeRangeInput.vue.d.ts +9 -2
- package/dist/components/Tooltip.vue.d.ts +8 -18
- package/dist/components/UnitInput.vue.d.ts +2 -1
- package/dist/components/WellPlate.colors.d.ts +2 -4
- package/dist/components/WellPlate.rendering.d.ts +7 -1
- package/dist/components/WellPlate.sampleDrop.d.ts +2 -0
- package/dist/components/WellPlate.vue.d.ts +22 -39
- package/dist/components/index.d.ts +34 -31
- package/dist/components/index.js +5 -8
- package/dist/components/internal/ActionItemInternal.vue.d.ts +11 -13
- package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +14 -23
- package/dist/components/internal/AppTopBarPillNavInternal.vue.d.ts +2 -1
- package/dist/components/internal/CalendarGridPanelInternal.vue.d.ts +2 -1
- package/dist/components/internal/DataStateInternal.vue.d.ts +7 -11
- package/dist/components/internal/{FilePickerTreeInternal.vue.d.ts → FilePickerListInternal.vue.d.ts} +9 -6
- package/dist/components/internal/FilePickerPreviewInternal.vue.d.ts +4 -3
- package/dist/components/internal/FormFieldRendererInternal.vue.d.ts +16 -18
- package/dist/components/internal/FormSectionRenderer.vue.d.ts +36 -32
- package/dist/components/internal/PlateEditorBindingInternal.vue.d.ts +8 -0
- package/dist/components/internal/ResourcePickerListInternal.vue.d.ts +65 -0
- package/dist/components/internal/SettingsPaneHeadInternal.vue.d.ts +15 -0
- package/dist/components/internal/WellEditPopupInternal.vue.d.ts +12 -4
- package/dist/components/internal/WorkspaceControlsInternal.vue.d.ts +6 -13
- package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +19 -42
- package/dist/components/internal/WorkspaceSidebarInternal.vue.d.ts +9 -19
- package/dist/components/internal/filePickerPresentation.d.ts +8 -0
- package/dist/components/internal/generatedJobsApi.d.ts +9 -0
- package/dist/{components-Bbg_gtzF.js → components-GDDvof8D.js} +7088 -5955
- package/dist/components-GDDvof8D.js.map +1 -0
- package/dist/composables/controlSchemaTypes.d.ts +4 -43
- package/dist/composables/createFilePickerAdapter.d.ts +5 -1
- package/dist/composables/filePicker/usePickerNavigation.d.ts +7 -2
- package/dist/composables/index.d.ts +2 -13
- package/dist/composables/index.js +7 -11
- package/dist/composables/pluginWorkspaceModel.d.ts +1 -3
- package/dist/composables/useAutoGroup.d.ts +10 -0
- package/dist/composables/useBioTemplateComponents.d.ts +15 -3
- package/dist/composables/useBioTemplateControls.d.ts +5 -1
- package/dist/composables/useBioTemplatePackWorkspace.d.ts +2 -0
- package/dist/composables/useBioTemplatePresetWorkspace.d.ts +5 -1
- package/dist/composables/useBioTemplateWorkspace.d.ts +5 -1
- package/dist/composables/useChemicalFormula.d.ts +2 -1
- package/dist/composables/useDropdownState.d.ts +5 -0
- package/dist/composables/useExperimentSave.d.ts +5 -1
- package/dist/composables/useFileBrowser.d.ts +3 -2
- package/dist/composables/useFilePicker.d.ts +25 -13
- package/dist/composables/useFocusTrap.d.ts +2 -1
- package/dist/composables/useGroupAssignment.d.ts +5 -1
- package/dist/composables/useJobsStatusTray.d.ts +5 -5
- package/dist/composables/useMenuKeyboard.d.ts +3 -3
- package/dist/composables/usePlatformContext.d.ts +3 -0
- package/dist/composables/usePluginClient.d.ts +1 -1
- package/dist/composables/useRackEditor.d.ts +36 -1
- package/dist/composables/useSequenceUtils.d.ts +8 -0
- package/dist/composables/useTemplateCollection.d.ts +5 -1
- package/dist/composables/workspaceRecipes.d.ts +1 -0
- package/dist/composables-D6uqp0UF.js +591 -0
- package/dist/composables-D6uqp0UF.js.map +1 -0
- package/dist/{errors-BLuqMee5.js → experiment-DgllIc3I.js} +89 -333
- package/dist/experiment-DgllIc3I.js.map +1 -0
- package/dist/{experiment-utils-CV2Wza3z.js → experiment-utils-DTANyVKV.js} +68 -37
- package/dist/experiment-utils-DTANyVKV.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +122 -17
- package/dist/index.js.map +1 -1
- package/dist/install.js +1 -2
- package/dist/install.js.map +1 -1
- package/dist/instrument.d.ts +6 -0
- package/dist/lcms.d.ts +11 -0
- package/dist/permissions.d.ts +2 -47
- package/dist/permissions.js +2 -26
- package/dist/permissions.js.map +1 -1
- package/dist/settings-BLr8lzw6.js +341 -0
- package/dist/settings-BLr8lzw6.js.map +1 -0
- package/dist/stores/auth.d.ts +2 -150
- package/dist/stores/experiment.d.ts +2 -106
- package/dist/stores/index.js +2 -2
- package/dist/stores/settings.d.ts +2 -44
- package/dist/styles.css +8608 -6950
- package/dist/tailwind.preset.js.map +1 -1
- package/dist/templates/adapters.d.ts +3 -0
- package/dist/templates/assayMatrixAdapters.d.ts +8 -1
- package/dist/templates/assayMatrixBuilder.d.ts +1 -0
- package/dist/templates/builderPresetControls.d.ts +1 -0
- package/dist/templates/calibrationCurveAdapters.d.ts +3 -0
- package/dist/templates/calibrationCurveBuilder.d.ts +1 -0
- package/dist/templates/catalog.d.ts +6 -2
- package/dist/templates/componentBindings.d.ts +31 -5
- package/dist/templates/componentPlateHelpers.d.ts +7 -1
- package/dist/templates/controlSchemas.d.ts +9 -0
- package/dist/templates/defaultBioTemplateBuilder.d.ts +1 -0
- package/dist/templates/doseResponseAdapters.d.ts +2 -0
- package/dist/templates/doseResponseBuilder.d.ts +1 -0
- package/dist/templates/elisaAssayCollectionBuilder.d.ts +1 -0
- package/dist/templates/flowCytometryAssayCollectionBuilder.d.ts +1 -0
- package/dist/templates/flowCytometryPanelBuilder.d.ts +1 -0
- package/dist/templates/flowPanelAdapters.d.ts +3 -0
- package/dist/templates/index.d.ts +1 -1
- package/dist/templates/index.js +2 -3
- package/dist/templates/instrumentRunAdapters.d.ts +13 -2
- package/dist/templates/instrumentRunBuilder.d.ts +1 -0
- package/dist/templates/lcmsBatchCollectionBuilder.d.ts +1 -0
- package/dist/templates/packs.d.ts +4 -0
- package/dist/templates/plateMapAdapters.d.ts +2 -0
- package/dist/templates/plateMapBuilder.d.ts +1 -0
- package/dist/templates/presets.d.ts +6 -2
- package/dist/templates/protocolAdapters.d.ts +4 -0
- package/dist/templates/protocolStepsBuilder.d.ts +1 -0
- package/dist/templates/qpcrAdapters.d.ts +4 -0
- package/dist/templates/qpcrExpressionCollectionBuilder.d.ts +1 -0
- package/dist/templates/qpcrPlateBuilder.d.ts +1 -0
- package/dist/templates/reagentAdapters.d.ts +4 -0
- package/dist/templates/reagentListBuilder.d.ts +1 -0
- package/dist/templates/samplePrepAdapters.d.ts +3 -0
- package/dist/templates/samplePrepBuilder.d.ts +1 -0
- package/dist/templates/sampleSheetAdapters.d.ts +4 -0
- package/dist/templates/sampleSheetBuilder.d.ts +1 -0
- package/dist/templates/targetedMetabolomicsCollectionBuilder.d.ts +1 -0
- package/dist/templates/templateEnvelopes.d.ts +8 -0
- package/dist/templates/templatePackCollectionBuilder.d.ts +1 -0
- package/dist/templates/templatePresetCollectionBuilder.d.ts +2 -0
- package/dist/templates/templateValidators.d.ts +12 -0
- package/dist/templates/timeCourseAdapters.d.ts +4 -0
- package/dist/templates/timeCourseBuilder.d.ts +1 -0
- package/dist/templates/wellPlateScreenCollectionBuilder.d.ts +1 -0
- package/dist/templates/westernBlotAssayCollectionBuilder.d.ts +1 -0
- package/dist/templates-BU1pet28.js +12304 -0
- package/dist/templates-BU1pet28.js.map +1 -0
- package/dist/types/auth.d.ts +0 -27
- package/dist/types/componentLabTypes.d.ts +14 -7
- package/dist/types/componentWorkflowTypes.d.ts +26 -0
- package/dist/types/components.d.ts +40 -17
- package/dist/types/fileBrowserTypes.d.ts +13 -0
- package/dist/types/filePicker.d.ts +19 -1
- package/dist/types/generated-ui.d.ts +13 -0
- package/dist/types/index.d.ts +4 -4
- package/dist/types/index.js +1 -1
- package/dist/types/instrument.d.ts +43 -15
- package/dist/types/platform.d.ts +2 -0
- package/dist/utils/color.d.ts +6 -1
- package/dist/utils/formModelSync.d.ts +1 -0
- package/dist/utils/plotlyTemplate.d.ts +3 -0
- package/dist/utils/rack.d.ts +16 -0
- package/dist/utils/sampleTypeMarker.d.ts +23 -0
- package/dist/{validation-ChMyixAf.js → validation-Dmp9Q5W0.js} +3032 -838
- package/dist/validation-Dmp9Q5W0.js.map +1 -0
- package/package.json +17 -23
- package/src/__stories__/experiment-helpers.ts +8 -4
- package/src/__tests__/components/ActionMenu.test.ts +66 -0
- package/src/__tests__/components/AlertBox.test.ts +21 -0
- package/src/__tests__/components/AppAvatarMenu.test.ts +25 -0
- package/src/__tests__/components/AppTopBar.test.ts +79 -0
- package/src/__tests__/components/AppTopBarPageSelector.test.ts +20 -0
- package/src/__tests__/components/AppTopBarPillNav.test.ts +74 -0
- package/src/__tests__/components/ArtifactSelectorModal.test.ts +20 -6
- package/src/__tests__/components/AuditTrail.test.ts +38 -0
- package/src/__tests__/components/BaseCheckbox.test.ts +43 -0
- package/src/__tests__/components/BaseInput.test.ts +43 -0
- package/src/__tests__/components/BaseModal.test.ts +90 -0
- package/src/__tests__/components/BaseRadioGroup.test.ts +8 -0
- package/src/__tests__/components/BaseSlider.test.ts +116 -0
- package/src/__tests__/components/BaseToggle.test.ts +7 -0
- package/src/__tests__/components/BatchProgressList.test.ts +22 -14
- package/src/__tests__/components/BioTemplateExperimentWorkspaceView.test.ts +4 -4
- package/src/__tests__/components/BioTemplatePackWorkspaceView.test.ts +1 -1
- package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +8 -8
- package/src/__tests__/components/BioTemplateRenderer.test.ts +2 -2
- package/src/__tests__/components/Breadcrumb.test.ts +45 -0
- package/src/__tests__/components/CalendarGridPanel.test.ts +14 -0
- package/src/__tests__/components/ChartContainer.test.ts +23 -0
- package/src/__tests__/components/ChemicalFormula.test.ts +15 -0
- package/src/__tests__/components/CollapsibleCard.test.ts +12 -9
- package/src/__tests__/components/ComponentBindingRenderer.test.ts +45 -17
- package/src/__tests__/components/ConcentrationInput.test.ts +17 -2
- package/src/__tests__/components/DataFrame.test.ts +107 -4
- package/src/__tests__/components/DataStates.test.ts +0 -16
- package/src/__tests__/components/DatePicker.test.ts +10 -0
- package/src/__tests__/components/DoseCalculator.test.ts +47 -0
- package/src/__tests__/components/DoseCalculatorVolumeField.test.ts +37 -34
- package/src/__tests__/components/EmptyState.test.ts +16 -1
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +31 -3
- package/src/__tests__/components/ExperimentTimeline.test.ts +45 -0
- package/src/__tests__/components/FilePicker.state.test.ts +2 -2
- package/src/__tests__/components/FilePicker.test.ts +360 -13
- package/src/__tests__/components/FileUploader.test.ts +15 -0
- package/src/__tests__/components/FitPanel.test.ts +96 -0
- package/src/__tests__/components/FormulaInput.test.ts +97 -0
- package/src/__tests__/components/GeneratedPathInput.test.ts +168 -41
- package/src/__tests__/components/GeneratedPlotlyResult.test.ts +1 -1
- package/src/__tests__/components/GeneratedResultRenderer.test.ts +47 -2
- package/src/__tests__/components/InstrumentStateBadge.test.ts +50 -0
- package/src/__tests__/components/JobsStatusTray.test.ts +27 -0
- package/src/__tests__/components/MoleculeInput.test.ts +78 -0
- package/src/__tests__/components/MultiSelect.test.ts +6 -0
- package/src/__tests__/components/PlateEditor.test.ts +1134 -0
- package/src/__tests__/components/PlateEditorBindingInternal.test.ts +64 -0
- package/src/__tests__/components/PlotlyChart.test.ts +147 -1
- package/src/__tests__/components/ProgressBar.test.ts +18 -0
- package/src/__tests__/components/ProtocolStepEditor.test.ts +106 -18
- package/src/__tests__/components/ProtocolStepParameterField.test.ts +74 -29
- package/src/__tests__/components/ReagentList.presentation.test.ts +10 -5
- package/src/__tests__/components/ReagentList.test.ts +23 -0
- package/src/__tests__/components/ResourcePickerListInternal.test.ts +141 -0
- package/src/__tests__/components/RetiredStyles.test.ts +0 -6
- package/src/__tests__/components/SampleHierarchyTree.test.ts +102 -0
- package/src/__tests__/components/SampleLegend.test.ts +18 -0
- package/src/__tests__/components/SampleSelector.test.ts +92 -1
- package/src/__tests__/components/SampleSelectorSampleRow.test.ts +5 -2
- package/src/__tests__/components/ScheduleCalendar.test.ts +32 -0
- package/src/__tests__/components/ScientificNumber.test.ts +113 -0
- package/src/__tests__/components/SearchableSelect.test.ts +21 -0
- package/src/__tests__/components/SecretInput.test.ts +6 -0
- package/src/__tests__/components/SegmentedControl.test.ts +14 -0
- package/src/__tests__/components/SequenceInput.test.ts +81 -0
- package/src/__tests__/components/SettingsModal.schema.test.ts +5 -5
- package/src/__tests__/components/SettingsModal.test.ts +371 -23
- package/src/__tests__/components/Skeleton.test.ts +23 -0
- package/src/__tests__/components/SmartGroupFieldRecipe.test.ts +12 -0
- package/src/__tests__/components/SmartGroupModal.test.ts +69 -0
- package/src/__tests__/components/StepWizard.test.ts +75 -0
- package/src/__tests__/components/TagsInput.test.ts +16 -0
- package/src/__tests__/components/TimePicker.test.ts +66 -0
- package/src/__tests__/components/TimeRangeInput.test.ts +83 -0
- package/src/__tests__/components/UnitInput.test.ts +147 -0
- package/src/__tests__/components/WellEditPopupInternal.test.ts +434 -2
- package/src/__tests__/components/WellPlate.decoration.test.ts +18 -0
- package/src/__tests__/components/WellPlate.geometry.test.ts +13 -0
- package/src/__tests__/components/WellPlate.interaction.test.ts +213 -0
- package/src/__tests__/components/WellPlate.keyboard.test.ts +24 -0
- package/src/__tests__/components/WellPlate.rendering.test.ts +38 -19
- package/src/__tests__/components/WellPlate.sampleDrop.test.ts +19 -0
- package/src/__tests__/components/WellPlate.sampleTypeIndicator.test.ts +58 -0
- package/src/__tests__/composables/createFilePickerAdapter.test.ts +114 -1
- package/src/__tests__/composables/useBioTemplatePackWorkspace.test.ts +1 -1
- package/src/__tests__/composables/useBioTemplateWorkspace.test.ts +1 -1
- package/src/__tests__/composables/useChemicalFormula.test.ts +60 -0
- package/src/__tests__/composables/useControlSchema.test.ts +15 -5
- package/src/__tests__/composables/useDropdownState.test.ts +20 -1
- package/src/__tests__/composables/useGeneratedAnalysis.test.ts +52 -0
- package/src/__tests__/composables/usePickerNavigation.test.ts +61 -1
- package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +18 -6
- package/src/__tests__/composables/useRackEditor.test.ts +270 -0
- package/src/__tests__/composables/useSequenceUtils.test.ts +28 -0
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +44 -9
- package/src/__tests__/styles/baseControlsRules.test.ts +115 -0
- package/src/__tests__/styles/controlRefreshRules.test.ts +218 -0
- package/src/__tests__/styles/cssTokens.test.ts +7 -5
- package/src/__tests__/styles/darkModeRules.test.ts +47 -0
- package/src/__tests__/styles/menusShellRefresh.test.ts +75 -0
- package/src/__tests__/templates/templates.test.ts +65 -15
- package/src/__tests__/templates/templatesDeprecation.test.ts +78 -0
- package/src/__tests__/utils/permissions.test.ts +0 -68
- package/src/__tests__/utils/plotlyTemplate.test.ts +69 -0
- package/src/__tests__/utils/sampleTypeMarker.test.ts +54 -0
- package/src/components/ActionMenu.vue +44 -23
- package/src/components/AdductText.story.vue +18 -0
- package/src/components/AdductText.vue +7 -1
- package/src/components/AlertBox.vue +3 -7
- package/src/components/AppAvatarMenu.vue +12 -10
- package/src/components/AppSidebar.vue +1 -1
- package/src/components/AppTopBar.story.vue +47 -4
- package/src/components/AppTopBar.vue +16 -22
- package/src/components/ArtifactSelectorModal.story.vue +1 -1
- package/src/components/ArtifactSelectorModal.vue +48 -160
- package/src/components/AuditTrail.vue +26 -12
- package/src/components/AutoGroupModal.vue +1 -1
- package/src/components/BaseCheckbox.story.vue +58 -2
- package/src/components/BaseCheckbox.vue +21 -3
- package/src/components/BaseInput.story.vue +51 -0
- package/src/components/BaseInput.vue +62 -1
- package/src/components/BaseModal.story.vue +55 -0
- package/src/components/BaseModal.vue +83 -50
- package/src/components/BaseRadioGroup.vue +1 -1
- package/src/components/BaseSlider.story.vue +36 -0
- package/src/components/BaseSlider.vue +56 -8
- package/src/components/BaseToggle.vue +1 -7
- package/src/components/BatchProgressList.vue +50 -52
- package/src/components/BioTemplatePresetWorkspaceView.vue +1 -1
- package/src/components/Breadcrumb.story.vue +1 -1
- package/src/components/Breadcrumb.vue +50 -15
- package/src/components/ChartContainer.story.vue +42 -0
- package/src/components/ChartContainer.vue +22 -1
- package/src/components/ChemicalFormula.story.vue +11 -11
- package/src/components/ChemicalFormula.vue +1 -0
- package/src/components/CollapsibleCard.vue +12 -32
- package/src/components/ComponentBindingRenderer.vue +6 -8
- package/src/components/ConcentrationInput.story.vue +1 -1
- package/src/components/ConcentrationInput.vue +8 -14
- package/src/components/ControlWorkspaceView.vue +1 -1
- package/src/components/DataFrame.story.vue +45 -2
- package/src/components/DataFrame.vue +28 -13
- package/src/components/DatePicker.story.vue +11 -0
- package/src/components/DatePicker.vue +5 -5
- package/src/components/DateTimePicker.story.vue +1 -1
- package/src/components/DateTimePicker.vue +8 -5
- package/src/components/Divider.story.vue +1 -1
- package/src/components/DoseCalculator.vue +44 -51
- package/src/components/DoseCalculatorVolumeField.vue +22 -35
- package/src/components/DoseDesignWorkspaceView.vue +1 -1
- package/src/components/EmptyState.story.vue +4 -4
- package/src/components/EmptyState.vue +4 -2
- package/src/components/ExperimentPopover.story.vue +2 -2
- package/src/components/ExperimentPopover.vue +6 -4
- package/src/components/ExperimentSelectorModal.story.vue +2 -1
- package/src/components/ExperimentSelectorModal.vue +91 -261
- package/src/components/ExperimentTimeline.story.vue +29 -0
- package/src/components/ExperimentTimeline.vue +20 -0
- package/src/components/FilePicker.story.vue +65 -10
- package/src/components/FilePicker.vue +203 -121
- package/src/components/FitPanel.story.vue +72 -6
- package/src/components/FitPanel.vue +70 -18
- package/src/components/FormField.vue +2 -0
- package/src/components/FormulaInput.story.vue +6 -0
- package/src/components/FormulaInput.vue +49 -70
- package/src/components/GeneratedPathInput.story.vue +121 -8
- package/src/components/GeneratedPathInput.vue +155 -86
- package/src/components/GeneratedPlotlyResult.vue +1 -1
- package/src/components/GeneratedPluginView.vue +4 -4
- package/src/components/GeneratedResultRenderer.vue +19 -0
- package/src/components/InstrumentStateBadge.story.vue +41 -0
- package/src/components/InstrumentStateBadge.vue +24 -14
- package/src/components/JobsStatusTray.vue +30 -8
- package/src/components/MoleculeInput.vue +12 -7
- package/src/components/MultiSelect.vue +3 -3
- package/src/components/PlateEditor.story.vue +333 -0
- package/src/components/PlateEditor.vue +793 -0
- package/src/components/PlotlyChart.story.vue +37 -1
- package/src/components/PlotlyChart.vue +86 -16
- package/src/components/PluginWorkspaceView.props.ts +1 -7
- package/src/components/PluginWorkspaceView.vue +0 -4
- package/src/components/ProgressBar.story.vue +0 -30
- package/src/components/ProgressBar.vue +3 -53
- package/src/components/ProtocolStepEditor.story.vue +8 -0
- package/src/components/ProtocolStepEditor.vue +55 -145
- package/src/components/ProtocolStepParameterField.vue +55 -93
- package/src/components/ReagentList.presentation.ts +7 -6
- package/src/components/ReagentList.vue +18 -17
- package/src/components/SampleHierarchyTree.story.vue +19 -0
- package/src/components/SampleHierarchyTree.vue +47 -14
- package/src/components/SampleLegend.vue +12 -4
- package/src/components/SampleSelector.story.vue +5 -1
- package/src/components/SampleSelector.vue +25 -15
- package/src/components/SampleSelectorGroupNode.vue +10 -20
- package/src/components/SampleSelectorSampleRow.vue +7 -18
- package/src/components/ScheduleCalendar.story.vue +14 -0
- package/src/components/ScheduleCalendar.vue +13 -1
- package/src/components/ScientificNumber.story.vue +15 -2
- package/src/components/ScientificNumber.vue +22 -26
- package/src/components/SearchableSelect.vue +14 -9
- package/src/components/SecretInput.vue +1 -1
- package/src/components/SegmentedControl.story.vue +23 -0
- package/src/components/SegmentedControl.vue +6 -1
- package/src/components/SequenceInput.story.vue +27 -5
- package/src/components/SequenceInput.vue +74 -29
- package/src/components/SequenceProgressBar.vue +0 -1
- package/src/components/SettingsModal.schema.ts +4 -2
- package/src/components/SettingsModal.story.vue +83 -22
- package/src/components/SettingsModal.vue +338 -79
- package/src/components/Skeleton.vue +6 -35
- package/src/components/SmartGroup.types.ts +2 -2
- package/src/components/SmartGroupFieldRecipe.groups.ts +4 -1
- package/src/components/SmartGroupFieldRecipe.vue +14 -8
- package/src/components/SmartGroupManual.vue +14 -11
- package/src/components/StepWizard.vue +16 -19
- package/src/components/TagsInput.story.vue +16 -11
- package/src/components/TagsInput.vue +20 -3
- package/src/components/TimePicker.story.vue +38 -3
- package/src/components/TimePicker.vue +26 -11
- package/src/components/TimeRangeInput.story.vue +50 -1
- package/src/components/TimeRangeInput.vue +36 -17
- package/src/components/UnitInput.story.vue +1 -1
- package/src/components/UnitInput.vue +173 -107
- package/src/components/WellPlate.colors.ts +7 -6
- package/src/components/WellPlate.geometry.ts +3 -0
- package/src/components/WellPlate.interaction.ts +39 -5
- package/src/components/WellPlate.rendering.ts +21 -27
- package/src/components/WellPlate.sampleDrop.ts +20 -12
- package/src/components/WellPlate.story.vue +18 -7
- package/src/components/WellPlate.vue +28 -22
- package/src/components/index.ts +34 -31
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +13 -7
- package/src/components/internal/AppTopBarPillNavInternal.vue +12 -7
- package/src/components/internal/CalendarGridPanelInternal.vue +1 -1
- package/src/components/internal/FilePickerListInternal.vue +318 -0
- package/src/components/internal/FilePickerPreviewInternal.vue +54 -52
- package/src/components/internal/PlateEditorBindingInternal.vue +37 -0
- package/src/components/internal/ResourcePickerListInternal.vue +237 -0
- package/src/components/internal/SettingsPaneHeadInternal.vue +41 -0
- package/src/components/internal/WellEditPopupInternal.vue +219 -146
- package/src/components/internal/WorkspaceShellInternal.vue +0 -6
- package/src/components/internal/filePickerPresentation.ts +30 -0
- package/src/components/internal/generatedJobsApi.ts +12 -0
- package/src/composables/autoGroup/index.ts +3 -0
- package/src/composables/controlSchemaAdapters.ts +0 -4
- package/src/composables/controlSchemaFormFields.ts +0 -4
- package/src/composables/controlSchemaTypes.ts +4 -43
- package/src/composables/createFilePickerAdapter.ts +90 -22
- package/src/composables/filePicker/localFiles.ts +1 -0
- package/src/composables/filePicker/usePickerNavigation.ts +29 -2
- package/src/composables/index.ts +2 -71
- package/src/composables/pluginWorkspaceModel.ts +0 -3
- package/src/composables/useAutoGroup.ts +10 -0
- package/src/composables/useBioTemplateComponents.ts +15 -3
- package/src/composables/useBioTemplateControls.ts +5 -1
- package/src/composables/useBioTemplatePackWorkspace.ts +2 -0
- package/src/composables/useBioTemplatePresetWorkspace.ts +5 -1
- package/src/composables/useBioTemplateWorkspace.ts +5 -1
- package/src/composables/useChemicalFormula.ts +39 -13
- package/src/composables/useDropdownState.ts +6 -1
- package/src/composables/useExperimentSave.ts +5 -1
- package/src/composables/useFileBrowser.ts +3 -2
- package/src/composables/useFilePicker.ts +98 -13
- package/src/composables/useFocusTrap.ts +9 -6
- package/src/composables/useGeneratedAnalysis.ts +38 -0
- package/src/composables/useGroupAssignment.ts +5 -1
- package/src/composables/useJobsStatusTray.ts +5 -5
- package/src/composables/useMenuKeyboard.ts +5 -3
- package/src/composables/usePlatformFilePickerAdapter.ts +26 -4
- package/src/composables/usePluginClient.ts +1 -1
- package/src/composables/usePluginWorkspace.ts +0 -1
- package/src/composables/useRackEditor.ts +202 -12
- package/src/composables/useSequenceUtils.ts +20 -2
- package/src/composables/useTemplateCollection.ts +5 -1
- package/src/composables/workspaceRecipes.ts +1 -0
- package/src/index.ts +9 -3
- package/src/instrument.ts +6 -0
- package/src/lcms.ts +11 -0
- package/src/permissions.ts +2 -59
- package/src/styles/components/action-menu.css +9 -38
- package/src/styles/components/adduct-text.css +15 -3
- package/src/styles/components/alert-box.css +19 -70
- package/src/styles/components/app-avatar-menu.css +22 -57
- package/src/styles/components/app-container.css +4 -4
- package/src/styles/components/app-layout.css +18 -18
- package/src/styles/components/app-page-selector.css +50 -105
- package/src/styles/components/app-pill-nav.css +24 -42
- package/src/styles/components/app-sidebar.css +2 -2
- package/src/styles/components/app-top-bar.css +78 -55
- package/src/styles/components/artifact-selector-modal.css +1 -211
- package/src/styles/components/audit-trail.css +45 -38
- package/src/styles/components/avatar.css +1 -1
- package/src/styles/components/batch-progress-list.css +179 -95
- package/src/styles/components/breadcrumb.css +34 -15
- package/src/styles/components/calendar.css +2 -8
- package/src/styles/components/chart-container.css +81 -38
- package/src/styles/components/checkbox.css +8 -6
- package/src/styles/components/chemical-formula.css +8 -6
- package/src/styles/components/collapsible-card.css +6 -46
- package/src/styles/components/concentration-input.css +1 -10
- package/src/styles/components/dataframe.css +48 -67
- package/src/styles/components/date-picker.css +124 -62
- package/src/styles/components/datetime-picker.css +81 -47
- package/src/styles/components/divider.css +2 -4
- package/src/styles/components/dose-calculator.css +9 -135
- package/src/styles/components/empty-state.css +26 -35
- package/src/styles/components/experiment-code-badge.css +1 -1
- package/src/styles/components/experiment-popover.css +16 -83
- package/src/styles/components/experiment-selector-modal.css +9 -226
- package/src/styles/components/experiment-timeline.css +120 -50
- package/src/styles/components/file-picker.css +323 -299
- package/src/styles/components/file-uploader.css +4 -6
- package/src/styles/components/fit-panel.css +108 -22
- package/src/styles/components/form-field.css +6 -4
- package/src/styles/components/formula-input.css +121 -48
- package/src/styles/components/group-assigner.css +1 -3
- package/src/styles/components/icon-button.css +15 -37
- package/src/styles/components/input.css +91 -2
- package/src/styles/components/instrument-monitor.css +37 -447
- package/src/styles/components/jobs-status-tray.css +82 -61
- package/src/styles/components/loading-spinner.css +18 -0
- package/src/styles/components/mobile-support-gate.css +15 -17
- package/src/styles/components/modal.css +44 -33
- package/src/styles/components/molecule-input.css +31 -16
- package/src/styles/components/multi-select.css +33 -29
- package/src/styles/components/number-input.css +1 -1
- package/src/styles/components/plate-editor.css +497 -0
- package/src/styles/components/popover.css +162 -0
- package/src/styles/components/progress-bar.css +1 -70
- package/src/styles/components/protocol-step-editor.css +21 -333
- package/src/styles/components/radio-group.css +9 -18
- package/src/styles/components/reagent-editor.css +3 -7
- package/src/styles/components/reagent-list.css +63 -69
- package/src/styles/components/resource-card.css +4 -6
- package/src/styles/components/resource-picker-list.css +212 -0
- package/src/styles/components/sample-hierarchy-tree.css +83 -65
- package/src/styles/components/sample-legend.css +18 -7
- package/src/styles/components/sample-selector.css +41 -68
- package/src/styles/components/schedule-calendar.css +59 -54
- package/src/styles/components/scientific-number.css +72 -18
- package/src/styles/components/searchable-select.css +17 -26
- package/src/styles/components/secret-input.css +20 -23
- package/src/styles/components/section-card.css +3 -5
- package/src/styles/components/segmented-control.css +14 -1
- package/src/styles/components/select.css +19 -8
- package/src/styles/components/sequence-input.css +85 -37
- package/src/styles/components/sequence-progress-bar.css +18 -37
- package/src/styles/components/settings-modal.css +683 -34
- package/src/styles/components/skeleton.css +24 -23
- package/src/styles/components/slider.css +74 -8
- package/src/styles/components/smart-group.css +27 -39
- package/src/styles/components/status-indicator.css +1 -1
- package/src/styles/components/step-wizard.css +62 -99
- package/src/styles/components/tabs.css +2 -6
- package/src/styles/components/tags-input.css +64 -54
- package/src/styles/components/textarea.css +2 -2
- package/src/styles/components/time-picker.css +67 -38
- package/src/styles/components/time-range-input.css +16 -18
- package/src/styles/components/toast.css +1 -1
- package/src/styles/components/toggle.css +21 -31
- package/src/styles/components/tooltip.css +13 -15
- package/src/styles/components/unit-input.css +86 -36
- package/src/styles/components/well-edit-popup.css +111 -189
- package/src/styles/components/well-plate.css +7 -7
- package/src/styles/index.css +5 -7
- package/src/styles/variables.css +35 -7
- package/src/templates/adapters.ts +4 -0
- package/src/templates/assayMatrixAdapters.ts +8 -1
- package/src/templates/assayMatrixBuilder.ts +1 -0
- package/src/templates/builderPresetControls.ts +1 -0
- package/src/templates/calibrationCurveAdapters.ts +3 -0
- package/src/templates/calibrationCurveBuilder.ts +1 -0
- package/src/templates/catalog.ts +6 -2
- package/src/templates/componentBindingCatalog.ts +1 -1
- package/src/templates/componentBindings.ts +31 -5
- package/src/templates/componentDoseResponseProps.ts +0 -1
- package/src/templates/componentPlateHelpers.ts +22 -1
- package/src/templates/componentPlateMapProps.ts +7 -5
- package/src/templates/componentQpcrPlateProps.ts +0 -1
- package/src/templates/controlSchemas.ts +9 -0
- package/src/templates/defaultBioTemplateBuilder.ts +1 -0
- package/src/templates/doseResponseAdapters.ts +2 -0
- package/src/templates/doseResponseBuilder.ts +1 -0
- package/src/templates/elisaAssayCollectionBuilder.ts +1 -0
- package/src/templates/flowCytometryAssayCollectionBuilder.ts +1 -0
- package/src/templates/flowCytometryPanelBuilder.ts +1 -0
- package/src/templates/flowPanelAdapters.ts +3 -0
- package/src/templates/index.ts +4 -0
- package/src/templates/instrumentRunAdapters.ts +13 -2
- package/src/templates/instrumentRunBuilder.ts +1 -0
- package/src/templates/lcmsBatchCollectionBuilder.ts +1 -0
- package/src/templates/packs.ts +4 -0
- package/src/templates/plateMapAdapters.ts +2 -0
- package/src/templates/plateMapBuilder.ts +1 -0
- package/src/templates/presets.ts +6 -2
- package/src/templates/protocolAdapters.ts +4 -0
- package/src/templates/protocolStepsBuilder.ts +1 -0
- package/src/templates/qpcrAdapters.ts +4 -0
- package/src/templates/qpcrExpressionCollectionBuilder.ts +1 -0
- package/src/templates/qpcrPlateBuilder.ts +1 -0
- package/src/templates/reagentAdapters.ts +4 -0
- package/src/templates/reagentListBuilder.ts +1 -0
- package/src/templates/samplePrepAdapters.ts +3 -0
- package/src/templates/samplePrepBuilder.ts +1 -0
- package/src/templates/sampleSheetAdapters.ts +4 -0
- package/src/templates/sampleSheetBuilder.ts +1 -0
- package/src/templates/targetedMetabolomicsCollectionBuilder.ts +1 -0
- package/src/templates/templateEnvelopes.ts +8 -0
- package/src/templates/templatePackCollectionBuilder.ts +1 -0
- package/src/templates/templatePresetCollectionBuilder.ts +2 -0
- package/src/templates/templateValidators.ts +12 -0
- package/src/templates/timeCourseAdapters.ts +4 -0
- package/src/templates/timeCourseBuilder.ts +1 -0
- package/src/templates/wellPlateScreenCollectionBuilder.ts +1 -0
- package/src/templates/westernBlotAssayCollectionBuilder.ts +1 -0
- package/src/types/auth.ts +0 -37
- package/src/types/componentLabTypes.ts +16 -13
- package/src/types/componentWorkflowTypes.ts +29 -0
- package/src/types/components.ts +43 -20
- package/src/types/fileBrowserTypes.ts +15 -0
- package/src/types/filePicker.ts +19 -1
- package/src/types/generated-ui.ts +15 -0
- package/src/types/index.ts +12 -11
- package/src/types/instrument.ts +47 -15
- package/src/types/platform.ts +2 -0
- package/src/utils/color.ts +16 -1
- package/src/utils/formModelSync.ts +1 -1
- package/src/utils/plotlyTemplate.ts +39 -0
- package/src/utils/rack.ts +16 -0
- package/src/utils/sampleTypeMarker.ts +44 -0
- package/dist/ExperimentPopover-CJdNlCZb.js +0 -451
- package/dist/ExperimentPopover-CJdNlCZb.js.map +0 -1
- package/dist/ExperimentSelectorModal-DHE9k8TP.js +0 -507
- package/dist/ExperimentSelectorModal-DHE9k8TP.js.map +0 -1
- package/dist/SettingsModal-DELMkUFR.js +0 -492
- package/dist/SettingsModal-DELMkUFR.js.map +0 -1
- package/dist/Skeleton-jhF9wUkU.js +0 -485
- package/dist/Skeleton-jhF9wUkU.js.map +0 -1
- package/dist/__tests__/composables/usePluginConfig.test.d.ts +0 -15
- package/dist/chunk-BoAXSpZd.js +0 -33
- package/dist/components/AppPluginSwitcher.vue.d.ts +0 -40
- package/dist/components/ColorSlider.vue.d.ts +0 -34
- package/dist/components/DropdownButton.vue.d.ts +0 -49
- package/dist/components/FileBrowserModal.vue.d.ts +0 -99
- package/dist/components/InstrumentAlertLog.vue.d.ts +0 -22
- package/dist/components/InstrumentStatusCard.vue.d.ts +0 -13
- package/dist/components/LcmsSequenceTable.vue.d.ts +0 -54
- package/dist/components/PlateMapEditor.vue.d.ts +0 -50
- package/dist/components/RackEditor.vue.d.ts +0 -78
- package/dist/components/fileBrowserPresentation.d.ts +0 -10
- package/dist/components/internal/FileBrowserDialogInternal.vue.d.ts +0 -34
- package/dist/components/internal/FileBrowserListingInternal.vue.d.ts +0 -25
- package/dist/components/internal/FileBrowserToolbarInternal.vue.d.ts +0 -26
- package/dist/components/internal/PlateMapEditorToolbarInternal.vue.d.ts +0 -28
- package/dist/components-Bbg_gtzF.js.map +0 -1
- package/dist/composables/useAsync.d.ts +0 -132
- package/dist/composables/useAuth.d.ts +0 -47
- package/dist/composables/useCommandHistory.d.ts +0 -33
- package/dist/composables/useOptimisticMutation.d.ts +0 -32
- package/dist/composables/usePasskey.d.ts +0 -10
- package/dist/composables/usePluginConfig.d.ts +0 -27
- package/dist/composables/useResourceCrud.d.ts +0 -44
- package/dist/composables/useWellPainting.d.ts +0 -39
- package/dist/composables/useWellPlateAdapter.d.ts +0 -40
- package/dist/composables/useWellPlateEditor.d.ts +0 -33
- package/dist/composables/useWellPlateValidation.d.ts +0 -31
- package/dist/composables-BCbYb2lA.js +0 -2356
- package/dist/composables-BCbYb2lA.js.map +0 -1
- package/dist/errors-BLuqMee5.js.map +0 -1
- package/dist/experiment-CVDRAnqp.js +0 -90
- package/dist/experiment-CVDRAnqp.js.map +0 -1
- package/dist/experiment-utils-CV2Wza3z.js.map +0 -1
- package/dist/plotly.min-c2Sez60c.js +0 -180634
- package/dist/plotly.min-c2Sez60c.js.map +0 -1
- package/dist/templates-NuaNHohp.js +0 -5260
- package/dist/templates-NuaNHohp.js.map +0 -1
- package/dist/useControlSchema-vLiRPUmj.js +0 -7301
- package/dist/useControlSchema-vLiRPUmj.js.map +0 -1
- package/dist/useDropdownState-C7hRiVQf.js +0 -63
- package/dist/useDropdownState-C7hRiVQf.js.map +0 -1
- package/dist/useExperimentSelector-G_CAOFTX.js +0 -328
- package/dist/useExperimentSelector-G_CAOFTX.js.map +0 -1
- package/dist/useFormBuilder-D37ajgS8.js +0 -729
- package/dist/useFormBuilder-D37ajgS8.js.map +0 -1
- package/dist/validation-ChMyixAf.js.map +0 -1
- package/src/__tests__/components/AppPluginSwitcher.test.ts +0 -44
- package/src/__tests__/components/DropdownButton.test.ts +0 -628
- package/src/__tests__/components/FileBrowserModal.test.ts +0 -460
- package/src/__tests__/components/InstrumentAlertLog.test.ts +0 -59
- package/src/__tests__/components/LcmsSequenceTable.test.ts +0 -79
- package/src/__tests__/components/PlateMapEditorToolbarInternal.test.ts +0 -54
- package/src/__tests__/components/RackEditor.test.ts +0 -169
- package/src/__tests__/composables/useAuth.test.ts +0 -233
- package/src/__tests__/composables/useCommandHistory.test.ts +0 -43
- package/src/__tests__/composables/useOptimisticMutation.test.ts +0 -40
- package/src/__tests__/composables/usePluginConfig.test.ts +0 -497
- package/src/__tests__/composables/useResourceCrud.test.ts +0 -56
- package/src/__tests__/composables/useWellPainting.test.ts +0 -52
- package/src/__tests__/composables/useWellPlateAdapter.test.ts +0 -50
- package/src/__tests__/composables/useWellPlateValidation.test.ts +0 -42
- package/src/components/AppPluginSwitcher.story.vue +0 -87
- package/src/components/AppPluginSwitcher.vue +0 -134
- package/src/components/ColorSlider.story.vue +0 -120
- package/src/components/ColorSlider.vue +0 -160
- package/src/components/DropdownButton.story.vue +0 -102
- package/src/components/DropdownButton.vue +0 -181
- package/src/components/FileBrowserModal.story.vue +0 -203
- package/src/components/FileBrowserModal.vue +0 -384
- package/src/components/InstrumentAlertLog.vue +0 -187
- package/src/components/InstrumentStatusCard.vue +0 -184
- package/src/components/LcmsSequenceTable.vue +0 -222
- package/src/components/PlateMapEditor.story.vue +0 -92
- package/src/components/PlateMapEditor.vue +0 -423
- package/src/components/RackEditor.story.vue +0 -100
- package/src/components/RackEditor.vue +0 -429
- package/src/components/fileBrowserPresentation.ts +0 -39
- package/src/components/internal/FileBrowserDialogInternal.vue +0 -75
- package/src/components/internal/FileBrowserListingInternal.vue +0 -197
- package/src/components/internal/FileBrowserToolbarInternal.vue +0 -136
- package/src/components/internal/FilePickerTreeInternal.vue +0 -238
- package/src/components/internal/PlateMapEditorToolbarInternal.vue +0 -128
- package/src/composables/useAsync.ts +0 -327
- package/src/composables/useAuth.ts +0 -458
- package/src/composables/useCommandHistory.ts +0 -117
- package/src/composables/useOptimisticMutation.ts +0 -97
- package/src/composables/usePasskey.ts +0 -216
- package/src/composables/usePluginConfig.ts +0 -252
- package/src/composables/useResourceCrud.ts +0 -235
- package/src/composables/useWellPainting.ts +0 -167
- package/src/composables/useWellPlateAdapter.ts +0 -151
- package/src/composables/useWellPlateEditor.ts +0 -415
- package/src/composables/useWellPlateValidation.ts +0 -89
- package/src/plotly-dist.d.ts +0 -3
- package/src/styles/components/app-plugin-switcher.css +0 -196
- package/src/styles/components/color-slider.css +0 -110
- package/src/styles/components/dropdown-button.css +0 -280
- package/src/styles/components/file-browser-modal.css +0 -489
- package/src/styles/components/lcms-sequence-table.css +0 -204
- package/src/styles/components/plate-map-editor.css +0 -464
- package/src/styles/components/rack-editor.css +0 -287
- /package/dist/__tests__/components/{AppPluginSwitcher.test.d.ts → AlertBox.test.d.ts} +0 -0
- /package/dist/__tests__/components/{DropdownButton.test.d.ts → AuditTrail.test.d.ts} +0 -0
- /package/dist/__tests__/components/{FileBrowserModal.test.d.ts → BaseCheckbox.test.d.ts} +0 -0
- /package/dist/__tests__/components/{InstrumentAlertLog.test.d.ts → BaseSlider.test.d.ts} +0 -0
- /package/dist/__tests__/components/{LcmsSequenceTable.test.d.ts → DoseCalculator.test.d.ts} +0 -0
- /package/dist/__tests__/components/{PlateMapEditorToolbarInternal.test.d.ts → FileUploader.test.d.ts} +0 -0
- /package/dist/__tests__/components/{RackEditor.test.d.ts → FitPanel.test.d.ts} +0 -0
- /package/dist/__tests__/{composables/useAuth.test.d.ts → components/FormulaInput.test.d.ts} +0 -0
- /package/dist/__tests__/{composables/useCommandHistory.test.d.ts → components/InstrumentStateBadge.test.d.ts} +0 -0
- /package/dist/__tests__/{composables/useOptimisticMutation.test.d.ts → components/MoleculeInput.test.d.ts} +0 -0
- /package/dist/__tests__/{composables/useResourceCrud.test.d.ts → components/PlateEditor.test.d.ts} +0 -0
- /package/dist/__tests__/{composables/useWellPainting.test.d.ts → components/PlateEditorBindingInternal.test.d.ts} +0 -0
- /package/dist/__tests__/{composables/useWellPlateAdapter.test.d.ts → components/ResourcePickerListInternal.test.d.ts} +0 -0
- /package/dist/__tests__/{composables/useWellPlateValidation.test.d.ts → components/SampleLegend.test.d.ts} +0 -0
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { d as resolveExperimentCode } from "./experiment-utils-
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { t as useExperimentStore } from "./experiment-CVDRAnqp.js";
|
|
6
|
-
import { B as toBioTemplateComponentProps, F as getBioTemplateComponentProps$1, H as toBioTemplateComponentPropsById, Jt as getBioTemplatePresetInfo, L as toBioTemplateComponentBindings, P as getBioTemplateComponentBindings, Qt as getBioTemplatePackInfo, R as toBioTemplateComponentBindingsById, U as toBioTemplateComponentSnippets, V as toBioTemplateComponentPropsByComponent$1, W as toBioTemplateComponentUsage, bn as rowLabel, d as createBioTemplatePresetCollectionFromControls, dn as extractTemplateCollection, i as createBioTemplateControlToolkit, p as createBioTemplatePackCollection, pn as PLATE_CONFIGS, sn as createTemplateCollection, u as createBioTemplatePresetCollection, un as ensureTemplateFromCollection, wn as deriveShade, xn as SAMPLE_COLOR_SCALE, z as toBioTemplateComponentImports } from "./templates-NuaNHohp.js";
|
|
1
|
+
import { o as useSettingsStore } from "./settings-BLr8lzw6.js";
|
|
2
|
+
import { d as resolveExperimentCode, o as datePresetToISO } from "./experiment-utils-DTANyVKV.js";
|
|
3
|
+
import { $t as getBioTemplatePackInfo, An as getControlDefaults, B as toBioTemplateComponentProps, Bn as getDefaultControlView, Er as PLUGIN_SECRET_FORMAT_KEY, F as getBioTemplateComponentProps$1, Gn as getFieldRegistryEntry, H as toBioTemplateComponentPropsById, Hn as omitUndefined, Kn as getTypeDefault, L as toBioTemplateComponentBindings, P as getBioTemplateComponentBindings, R as toBioTemplateComponentBindingsById, Tn as deriveShade, U as toBioTemplateComponentSnippets, Un as recordValue, V as toBioTemplateComponentPropsByComponent$1, Vn as normalizeControlDefinition, W as toBioTemplateComponentUsage, Wn as replaceRecord$1, Yn as useConcentrationUnits, Yt as getBioTemplatePresetInfo, ar as getInjectedPlatformContext, bn as rowIndex, br as isRecord, cn as createTemplateCollection, d as createBioTemplatePresetCollectionFromControls, dn as ensureTemplateFromCollection, fn as extractTemplateCollection, fr as isActiveJobStatus, gr as normalizeJobState, i as createBioTemplateControlToolkit, ir as currentExperimentFromContext, jn as useControlSchema, mn as PLATE_CONFIGS, or as parseExperimentId, p as createBioTemplatePackCollection, sr as resolveCurrentExperimentId, u as createBioTemplatePresetCollection, vr as useEventStream, xn as rowLabel, z as toBioTemplateComponentImports, zn as controlsToViewItems } from "./templates-BU1pet28.js";
|
|
4
|
+
import { a as useApi, i as authenticationRequiredError, n as readErrorMessage, o as useAuthStore, r as AuthenticationRequiredError, t as useExperimentStore } from "./experiment-DgllIc3I.js";
|
|
7
5
|
import { computed, effectScope, getCurrentScope, inject, nextTick, onMounted, onScopeDispose, onUnmounted, provide, reactive, readonly, ref, shallowRef, toRaw, toValue, unref, watch } from "vue";
|
|
8
6
|
//#region src/composables/useRovingFocus.ts
|
|
9
7
|
/**
|
|
@@ -49,66 +47,6 @@ function useRovingFocus(options) {
|
|
|
49
47
|
return { handleKeydown };
|
|
50
48
|
}
|
|
51
49
|
//#endregion
|
|
52
|
-
//#region src/composables/useMenuKeyboard.ts
|
|
53
|
-
/**
|
|
54
|
-
* Shared keyboard contract for menu- and listbox-style disclosure panels (the
|
|
55
|
-
* WAI-ARIA menu button pattern). Used by ActionMenu, AppAvatarMenu and DropdownButton,
|
|
56
|
-
* each pairing it with `useDropdownState` for the disclosure state itself.
|
|
57
|
-
*/
|
|
58
|
-
function useMenuKeyboard(options) {
|
|
59
|
-
function focusItem(position) {
|
|
60
|
-
nextTick(() => {
|
|
61
|
-
const items = options.items();
|
|
62
|
-
(position === "first" ? items[0] : items[items.length - 1])?.focus();
|
|
63
|
-
});
|
|
64
|
-
}
|
|
65
|
-
function handleTriggerKeydown(event) {
|
|
66
|
-
if (options.isOpen()) return;
|
|
67
|
-
if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
|
|
68
|
-
event.preventDefault();
|
|
69
|
-
options.open();
|
|
70
|
-
focusItem("first");
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
if (event.key === "ArrowUp") {
|
|
74
|
-
event.preventDefault();
|
|
75
|
-
options.open();
|
|
76
|
-
focusItem("last");
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
function handlePanelKeydown(event) {
|
|
80
|
-
const items = options.items();
|
|
81
|
-
const currentIndex = items.indexOf(document.activeElement);
|
|
82
|
-
if (event.key === "Escape") {
|
|
83
|
-
event.preventDefault();
|
|
84
|
-
options.close(true);
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
if (event.key === "Tab") {
|
|
88
|
-
options.close();
|
|
89
|
-
return;
|
|
90
|
-
}
|
|
91
|
-
if (event.key === "Home") {
|
|
92
|
-
event.preventDefault();
|
|
93
|
-
items[0]?.focus();
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (event.key === "End") {
|
|
97
|
-
event.preventDefault();
|
|
98
|
-
items[items.length - 1]?.focus();
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
|
|
102
|
-
event.preventDefault();
|
|
103
|
-
const direction = event.key === "ArrowDown" ? 1 : -1;
|
|
104
|
-
items[(Math.max(currentIndex, 0) + direction + items.length) % items.length]?.focus();
|
|
105
|
-
}
|
|
106
|
-
return {
|
|
107
|
-
handleTriggerKeydown,
|
|
108
|
-
handlePanelKeydown
|
|
109
|
-
};
|
|
110
|
-
}
|
|
111
|
-
//#endregion
|
|
112
50
|
//#region src/composables/useSortedItems.ts
|
|
113
51
|
/** Shared sorting for SDK tables and lists with stable empty-value handling. */
|
|
114
52
|
function useSortedItems(options) {
|
|
@@ -264,6 +202,221 @@ function useTheme() {
|
|
|
264
202
|
};
|
|
265
203
|
}
|
|
266
204
|
//#endregion
|
|
205
|
+
//#region src/composables/usePlatformContext.ts
|
|
206
|
+
var DEFAULT_CONTEXT = {
|
|
207
|
+
isIntegrated: false,
|
|
208
|
+
theme: "system"
|
|
209
|
+
};
|
|
210
|
+
var platformContext = ref({ ...DEFAULT_CONTEXT });
|
|
211
|
+
var inferredOrigins = /* @__PURE__ */ new Set();
|
|
212
|
+
var allowedOrigins = /* @__PURE__ */ new Set();
|
|
213
|
+
var allowAnyOrigin = false;
|
|
214
|
+
var initialized = false;
|
|
215
|
+
var listenerCount = 0;
|
|
216
|
+
var nextConsumerId = 0;
|
|
217
|
+
var currentHandler = null;
|
|
218
|
+
var consumerOrigins = /* @__PURE__ */ new Map();
|
|
219
|
+
var allowAnyOriginConsumers = /* @__PURE__ */ new Set();
|
|
220
|
+
/**
|
|
221
|
+
* Derive origin from URL (protocol + host)
|
|
222
|
+
*/
|
|
223
|
+
function getOriginFromUrl(url) {
|
|
224
|
+
try {
|
|
225
|
+
return new URL(url).origin;
|
|
226
|
+
} catch {
|
|
227
|
+
return null;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function normalizeAllowedOrigins(origins) {
|
|
231
|
+
const normalized = /* @__PURE__ */ new Set();
|
|
232
|
+
if (!origins) return normalized;
|
|
233
|
+
for (const origin of origins) normalized.add(getOriginFromUrl(origin) || origin);
|
|
234
|
+
return normalized;
|
|
235
|
+
}
|
|
236
|
+
function recomputeOriginPolicy() {
|
|
237
|
+
allowedOrigins = new Set(inferredOrigins);
|
|
238
|
+
for (const origins of consumerOrigins.values()) for (const origin of origins) allowedOrigins.add(origin);
|
|
239
|
+
allowAnyOrigin = allowAnyOriginConsumers.size > 0;
|
|
240
|
+
}
|
|
241
|
+
function resetPlatformContextState() {
|
|
242
|
+
inferredOrigins = /* @__PURE__ */ new Set();
|
|
243
|
+
allowedOrigins = /* @__PURE__ */ new Set();
|
|
244
|
+
allowAnyOrigin = false;
|
|
245
|
+
initialized = false;
|
|
246
|
+
listenerCount = 0;
|
|
247
|
+
currentHandler = null;
|
|
248
|
+
consumerOrigins.clear();
|
|
249
|
+
allowAnyOriginConsumers.clear();
|
|
250
|
+
platformContext.value = { ...DEFAULT_CONTEXT };
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Check if an origin is allowed for postMessage communication
|
|
254
|
+
*/
|
|
255
|
+
function isOriginAllowed(origin) {
|
|
256
|
+
if (allowAnyOrigin) {
|
|
257
|
+
console.warn("[MINT SDK] postMessage origin validation disabled - only use in development");
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
if (origin === window.location.origin) return true;
|
|
261
|
+
return allowedOrigins.has(origin);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Platform context composable for plugin integration with MINT Platform.
|
|
265
|
+
*
|
|
266
|
+
* Provides secure communication with the parent platform via postMessage.
|
|
267
|
+
*
|
|
268
|
+
* @param options - Configuration options
|
|
269
|
+
* @param options.allowedOrigins - List of allowed origins for postMessage
|
|
270
|
+
* @param options.allowAnyOrigin - Allow any origin (UNSAFE, development only)
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* ```typescript
|
|
274
|
+
* // Basic usage - derives origin from platform injection
|
|
275
|
+
* const { isIntegrated, user, theme } = usePlatformContext()
|
|
276
|
+
*
|
|
277
|
+
* // With explicit allowed origins
|
|
278
|
+
* const { isIntegrated } = usePlatformContext({
|
|
279
|
+
* allowedOrigins: ['https://mint.example.com']
|
|
280
|
+
* })
|
|
281
|
+
*
|
|
282
|
+
* // Development mode (UNSAFE)
|
|
283
|
+
* const { isIntegrated } = usePlatformContext({
|
|
284
|
+
* allowAnyOrigin: import.meta.env.DEV
|
|
285
|
+
* })
|
|
286
|
+
* ```
|
|
287
|
+
*/
|
|
288
|
+
/** Connects a plugin to the MINT platform via postMessage, exposing user, theme, and experiment context. */
|
|
289
|
+
function usePlatformContext(options = {}) {
|
|
290
|
+
const consumerId = ++nextConsumerId;
|
|
291
|
+
const instanceOrigins = normalizeAllowedOrigins(options.allowedOrigins);
|
|
292
|
+
const instanceAllowAnyOrigin = options.allowAnyOrigin === true;
|
|
293
|
+
function detectPlatform() {
|
|
294
|
+
const detectedOrigins = /* @__PURE__ */ new Set();
|
|
295
|
+
const platformData = window.__MINT_PLATFORM__;
|
|
296
|
+
if (platformData) {
|
|
297
|
+
platformContext.value = {
|
|
298
|
+
...platformData,
|
|
299
|
+
isIntegrated: true
|
|
300
|
+
};
|
|
301
|
+
if (platformData.platformOrigin) detectedOrigins.add(platformData.platformOrigin);
|
|
302
|
+
else if (platformData.platformApiUrl) {
|
|
303
|
+
const origin = getOriginFromUrl(platformData.platformApiUrl);
|
|
304
|
+
if (origin) detectedOrigins.add(origin);
|
|
305
|
+
}
|
|
306
|
+
} else {
|
|
307
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
308
|
+
const hasPluginParam = urlParams.has("mint-plugin");
|
|
309
|
+
const platformOrigin = urlParams.get("mint-origin");
|
|
310
|
+
if (platformOrigin) {
|
|
311
|
+
const origin = getOriginFromUrl(platformOrigin);
|
|
312
|
+
if (origin) detectedOrigins.add(origin);
|
|
313
|
+
}
|
|
314
|
+
platformContext.value = {
|
|
315
|
+
isIntegrated: hasPluginParam,
|
|
316
|
+
theme: (() => {
|
|
317
|
+
try {
|
|
318
|
+
const s = localStorage.getItem("mint-settings");
|
|
319
|
+
if (s) return JSON.parse(s).theme || "system";
|
|
320
|
+
} catch {}
|
|
321
|
+
return "system";
|
|
322
|
+
})(),
|
|
323
|
+
platformOrigin: platformOrigin || void 0
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
inferredOrigins = detectedOrigins;
|
|
327
|
+
}
|
|
328
|
+
function handlePlatformMessage(event) {
|
|
329
|
+
if (event.source !== window.parent) return;
|
|
330
|
+
if (!isOriginAllowed(event.origin)) {
|
|
331
|
+
console.warn(`[MINT SDK] Rejected postMessage from untrusted origin: ${event.origin}`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
try {
|
|
335
|
+
const platformEvent = event.data;
|
|
336
|
+
if (!platformEvent.type?.startsWith("mint:")) return;
|
|
337
|
+
switch (platformEvent.type) {
|
|
338
|
+
case "mint:theme-changed":
|
|
339
|
+
platformContext.value.theme = platformEvent.payload;
|
|
340
|
+
break;
|
|
341
|
+
case "mint:user-changed": platformContext.value.user = platformEvent.payload;
|
|
342
|
+
}
|
|
343
|
+
} catch {}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* Send a message to the parent platform.
|
|
347
|
+
* Uses validated target origin for security.
|
|
348
|
+
*/
|
|
349
|
+
function sendToPlatform(event) {
|
|
350
|
+
if (!platformContext.value.isIntegrated || window.parent === window) return;
|
|
351
|
+
let targetOrigin;
|
|
352
|
+
if (platformContext.value.platformOrigin) targetOrigin = platformContext.value.platformOrigin;
|
|
353
|
+
else if (allowedOrigins.size > 0) targetOrigin = allowedOrigins.values().next().value;
|
|
354
|
+
else if (allowAnyOrigin) {
|
|
355
|
+
targetOrigin = "*";
|
|
356
|
+
console.warn("[MINT SDK] Using wildcard origin for postMessage - only use in development");
|
|
357
|
+
} else {
|
|
358
|
+
console.warn("[MINT SDK] Cannot send postMessage: no platform origin configured");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
window.parent.postMessage(event, targetOrigin);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Request navigation to a path in the platform.
|
|
365
|
+
*/
|
|
366
|
+
function navigate(path) {
|
|
367
|
+
sendToPlatform({
|
|
368
|
+
type: "mint:navigate",
|
|
369
|
+
payload: path
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Show a notification in the platform.
|
|
374
|
+
*/
|
|
375
|
+
function notify(message, type = "info") {
|
|
376
|
+
sendToPlatform({
|
|
377
|
+
type: "mint:notification",
|
|
378
|
+
payload: {
|
|
379
|
+
message,
|
|
380
|
+
type
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
onMounted(() => {
|
|
385
|
+
consumerOrigins.set(consumerId, instanceOrigins);
|
|
386
|
+
if (instanceAllowAnyOrigin) allowAnyOriginConsumers.add(consumerId);
|
|
387
|
+
if (!initialized) {
|
|
388
|
+
detectPlatform();
|
|
389
|
+
currentHandler = handlePlatformMessage;
|
|
390
|
+
window.addEventListener("message", handlePlatformMessage);
|
|
391
|
+
initialized = true;
|
|
392
|
+
}
|
|
393
|
+
listenerCount++;
|
|
394
|
+
recomputeOriginPolicy();
|
|
395
|
+
});
|
|
396
|
+
onUnmounted(() => {
|
|
397
|
+
consumerOrigins.delete(consumerId);
|
|
398
|
+
allowAnyOriginConsumers.delete(consumerId);
|
|
399
|
+
listenerCount = Math.max(0, listenerCount - 1);
|
|
400
|
+
if (listenerCount === 0) {
|
|
401
|
+
if (currentHandler) window.removeEventListener("message", currentHandler);
|
|
402
|
+
resetPlatformContextState();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
recomputeOriginPolicy();
|
|
406
|
+
});
|
|
407
|
+
return {
|
|
408
|
+
context: platformContext,
|
|
409
|
+
isIntegrated: computed(() => platformContext.value.isIntegrated),
|
|
410
|
+
plugin: computed(() => platformContext.value.plugin),
|
|
411
|
+
user: computed(() => platformContext.value.user),
|
|
412
|
+
theme: computed(() => platformContext.value.theme),
|
|
413
|
+
features: computed(() => platformContext.value.features),
|
|
414
|
+
navigate,
|
|
415
|
+
notify,
|
|
416
|
+
sendToPlatform
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
//#endregion
|
|
267
420
|
//#region src/composables/usePresenceHeartbeat.ts
|
|
268
421
|
var PRESENCE_CLIENT_ID_KEY = "mint:presence:client-id";
|
|
269
422
|
var DEFAULT_HEARTBEAT_INTERVAL_MS = 3e4;
|
|
@@ -595,137 +748,1082 @@ function useAppExperiment(options = {}) {
|
|
|
595
748
|
};
|
|
596
749
|
}
|
|
597
750
|
//#endregion
|
|
598
|
-
//#region src/composables/
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
let removeMediaListener;
|
|
612
|
-
const syncMobileState = (event) => {
|
|
613
|
-
isMobileViewport.value = Boolean(event?.matches ?? mediaQueryList?.matches);
|
|
614
|
-
};
|
|
615
|
-
const unbindMediaQuery = () => {
|
|
616
|
-
removeMediaListener?.();
|
|
617
|
-
removeMediaListener = void 0;
|
|
618
|
-
mediaQueryList = null;
|
|
751
|
+
//#region src/composables/controlWorkspaceOptions.ts
|
|
752
|
+
function mergeControlWorkspaceOptions(base, override) {
|
|
753
|
+
return {
|
|
754
|
+
...base ?? {},
|
|
755
|
+
...override ?? {},
|
|
756
|
+
initialValues: {
|
|
757
|
+
...base?.initialValues ?? {},
|
|
758
|
+
...override?.initialValues ?? {}
|
|
759
|
+
},
|
|
760
|
+
topBarSettings: {
|
|
761
|
+
...base?.topBarSettings ?? {},
|
|
762
|
+
...override?.topBarSettings ?? {}
|
|
763
|
+
}
|
|
619
764
|
};
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
765
|
+
}
|
|
766
|
+
//#endregion
|
|
767
|
+
//#region src/composables/controlComponentBindings.ts
|
|
768
|
+
/** Map control workspace values into component props for direct `v-bind` usage. */
|
|
769
|
+
function controlValuesToComponentProps(values, mapping) {
|
|
770
|
+
if (mapping === void 0) return { ...values };
|
|
771
|
+
if (Array.isArray(mapping)) {
|
|
772
|
+
const props = {};
|
|
773
|
+
for (const key of mapping) props[key] = values[key];
|
|
774
|
+
return props;
|
|
775
|
+
}
|
|
776
|
+
const props = {};
|
|
777
|
+
for (const [prop, source] of Object.entries(mapping)) props[prop] = typeof source === "function" ? source(values) : values[source];
|
|
778
|
+
return props;
|
|
779
|
+
}
|
|
780
|
+
/** Map control workspace values into named SDK component bindings for direct slot rendering. */
|
|
781
|
+
function controlValuesToComponentBindings(values, bindings) {
|
|
782
|
+
if (bindings === void 0) return [];
|
|
783
|
+
return normalizeControlComponentBindingConfigs(bindings).map((binding) => ({
|
|
784
|
+
id: binding.id,
|
|
785
|
+
component: binding.component,
|
|
786
|
+
props: controlValuesToComponentProps(values, binding.props)
|
|
787
|
+
}));
|
|
788
|
+
}
|
|
789
|
+
/** Map control workspace values into SDK component bindings keyed by binding id. */
|
|
790
|
+
function controlValuesToComponentBindingsById(values, bindings) {
|
|
791
|
+
return Object.fromEntries(controlValuesToComponentBindings(values, bindings).map((binding) => [binding.id, binding]));
|
|
792
|
+
}
|
|
793
|
+
function normalizeControlComponentBindingConfigs(bindings) {
|
|
794
|
+
if (Array.isArray(bindings)) {
|
|
795
|
+
const usedIds = /* @__PURE__ */ new Map();
|
|
796
|
+
return bindings.map((binding) => ({
|
|
797
|
+
id: uniqueComponentBindingId(binding.id ?? binding.component, usedIds),
|
|
798
|
+
component: binding.component,
|
|
799
|
+
props: binding.props
|
|
800
|
+
}));
|
|
801
|
+
}
|
|
802
|
+
return Object.entries(bindings).map(([id, binding]) => ({
|
|
803
|
+
id,
|
|
804
|
+
component: binding.component,
|
|
805
|
+
props: binding.props
|
|
806
|
+
}));
|
|
807
|
+
}
|
|
808
|
+
function uniqueComponentBindingId(id, usedIds) {
|
|
809
|
+
const count = usedIds.get(id) ?? 0;
|
|
810
|
+
usedIds.set(id, count + 1);
|
|
811
|
+
return count === 0 ? id : `${id}-${count + 1}`;
|
|
812
|
+
}
|
|
813
|
+
//#endregion
|
|
814
|
+
//#region src/composables/controlSchemaModel.ts
|
|
815
|
+
function isControlModelBinding(model) {
|
|
816
|
+
return typeof model === "object" && model !== null && "controls" in model && "controlOptions" in model;
|
|
817
|
+
}
|
|
818
|
+
function resolveControlModel(model) {
|
|
819
|
+
if (model === void 0) return void 0;
|
|
820
|
+
return isControlModelBinding(model) ? model : defineControlModel(model);
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Create a complete workspace component binding from a simple controls data model for ControlWorkspaceView, generated forms, and sidebars.
|
|
824
|
+
*
|
|
825
|
+
* @example Recommended
|
|
826
|
+
* ```ts
|
|
827
|
+
* const workspaceModel = defineControlModel({
|
|
828
|
+
* controls: { threshold: { type: 'number', default: 0.05 } },
|
|
829
|
+
* })
|
|
830
|
+
* ```
|
|
831
|
+
*/
|
|
832
|
+
function defineControlModel(model) {
|
|
833
|
+
const { controls: rootControls, sections: rootSections, views: modelViews, components, componentBindings, componentProps, componentPropsById, ...baseOptions } = model;
|
|
834
|
+
const resolvedComponentBindings = componentBindings ?? components;
|
|
835
|
+
const controls = {};
|
|
836
|
+
const views = {};
|
|
837
|
+
const sections = {};
|
|
838
|
+
const defaultViewId = model.view ?? "default";
|
|
839
|
+
const defaultSectionId = model.section ?? "controls";
|
|
840
|
+
if (rootControls) {
|
|
841
|
+
registerSectionOptions(sections, defaultSectionId, {
|
|
842
|
+
id: defaultSectionId,
|
|
843
|
+
label: model.sectionLabel,
|
|
844
|
+
title: model.sectionLabel,
|
|
845
|
+
columns: model.columns
|
|
846
|
+
});
|
|
847
|
+
appendModelControls(controls, rootControls, defaultViewId, defaultSectionId);
|
|
848
|
+
}
|
|
849
|
+
for (const [sectionId, section] of Object.entries(rootSections ?? {})) {
|
|
850
|
+
registerSectionOptions(sections, sectionId, controlModelSectionOptions(section));
|
|
851
|
+
appendModelControls(controls, section.controls, defaultViewId, sectionId, section.sidebar);
|
|
852
|
+
}
|
|
853
|
+
for (const [viewId, view] of Object.entries(modelViews ?? {})) {
|
|
854
|
+
const { controls: viewControls, sections: viewSections, section, sectionLabel, sectionDescription, ...viewConfig } = view;
|
|
855
|
+
views[viewId] = omitUndefined(viewConfig);
|
|
856
|
+
if (viewControls) {
|
|
857
|
+
const sectionId = section ?? `${viewId}-controls`;
|
|
858
|
+
const label = sectionLabel ?? view.label;
|
|
859
|
+
registerSectionOptions(sections, sectionId, {
|
|
860
|
+
id: sectionId,
|
|
861
|
+
label,
|
|
862
|
+
title: label,
|
|
863
|
+
description: sectionDescription,
|
|
864
|
+
columns: model.columns
|
|
865
|
+
});
|
|
866
|
+
appendModelControls(controls, viewControls, viewId, sectionId);
|
|
625
867
|
}
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
if (typeof queryList.addEventListener === "function") {
|
|
631
|
-
queryList.addEventListener("change", handleChange);
|
|
632
|
-
removeMediaListener = () => queryList.removeEventListener("change", handleChange);
|
|
633
|
-
return;
|
|
868
|
+
for (const [sectionKey, sectionConfig] of Object.entries(viewSections ?? {})) {
|
|
869
|
+
const sectionId = sectionConfig.id ?? `${viewId}-${sectionKey}`;
|
|
870
|
+
registerSectionOptions(sections, sectionId, controlModelSectionOptions(sectionConfig));
|
|
871
|
+
appendModelControls(controls, sectionConfig.controls, viewId, sectionId, sectionConfig.sidebar);
|
|
634
872
|
}
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
watch(mediaQuery, bindMediaQuery, { flush: "post" });
|
|
640
|
-
onUnmounted(unbindMediaQuery);
|
|
873
|
+
}
|
|
874
|
+
const controlOptions = { ...baseOptions };
|
|
875
|
+
if (Object.keys(views).length > 0) controlOptions.views = views;
|
|
876
|
+
if (Object.keys(sections).length > 0) controlOptions.sections = sections;
|
|
641
877
|
return {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
878
|
+
controls,
|
|
879
|
+
controlOptions,
|
|
880
|
+
...resolvedComponentBindings === void 0 ? {} : { componentBindings: resolvedComponentBindings },
|
|
881
|
+
...componentProps === void 0 ? {} : { componentProps },
|
|
882
|
+
...componentPropsById === void 0 ? {} : { componentPropsById }
|
|
645
883
|
};
|
|
646
884
|
}
|
|
885
|
+
function appendModelControls(target, source, viewId, sectionId, sidebar) {
|
|
886
|
+
for (const [name, input] of Object.entries(source)) {
|
|
887
|
+
if (Object.prototype.hasOwnProperty.call(target, name)) throw new Error(`Duplicate control "${name}" in defineControlModel(). Control names must be unique across views and sections.`);
|
|
888
|
+
const definition = normalizeControlDefinition(input);
|
|
889
|
+
target[name] = omitUndefined({
|
|
890
|
+
...definition,
|
|
891
|
+
view: definition.view ?? viewId,
|
|
892
|
+
section: definition.section ?? sectionId,
|
|
893
|
+
sidebar: definition.sidebar ?? sidebar
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
function controlModelSectionOptions(section) {
|
|
898
|
+
const { controls: _controls, sidebar: _sidebar, ...options } = section;
|
|
899
|
+
return omitUndefined(options);
|
|
900
|
+
}
|
|
901
|
+
function registerSectionOptions(target, sectionId, options) {
|
|
902
|
+
target[sectionId] = omitUndefined({
|
|
903
|
+
...target[sectionId] ?? {},
|
|
904
|
+
...options,
|
|
905
|
+
id: sectionId
|
|
906
|
+
});
|
|
907
|
+
}
|
|
647
908
|
//#endregion
|
|
648
|
-
//#region src/
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
909
|
+
//#region src/composables/useControlWorkspace.ts
|
|
910
|
+
/**
|
|
911
|
+
* Prepare shared reactive values plus AppTopBar/AppSidebar/FormBuilder bindings from one simple controls data model.
|
|
912
|
+
*
|
|
913
|
+
* @example Recommended
|
|
914
|
+
* ```ts
|
|
915
|
+
* const workspace = useControlWorkspace(workspaceModel)
|
|
916
|
+
* const { form, sidebar, topBar } = workspace.bindings
|
|
917
|
+
* ```
|
|
918
|
+
*/
|
|
919
|
+
function useControlWorkspace(controlsOrModel, options = {}) {
|
|
920
|
+
const model = isControlModelBinding(controlsOrModel) ? controlsOrModel : void 0;
|
|
921
|
+
const controls = model ? model.controls : controlsOrModel;
|
|
922
|
+
const { initialValues, topBarSettings, ...schemaOptions } = model ? mergeControlWorkspaceOptions(model.controlOptions, options) : options;
|
|
923
|
+
const schema = useControlSchema(controls, schemaOptions);
|
|
924
|
+
const activeView = ref(schema.defaultView);
|
|
925
|
+
const values = reactive({
|
|
926
|
+
...schema.initialValues,
|
|
927
|
+
...initialValues ?? {}
|
|
662
928
|
});
|
|
663
|
-
function
|
|
664
|
-
|
|
665
|
-
Object.assign(internalControlValues, values);
|
|
929
|
+
function setActiveView(viewId) {
|
|
930
|
+
syncActiveView(viewId);
|
|
666
931
|
}
|
|
667
|
-
function
|
|
668
|
-
|
|
932
|
+
function syncActiveView(viewId) {
|
|
933
|
+
if (!schema.viewIds.includes(viewId)) return;
|
|
934
|
+
if (activeView.value !== viewId) activeView.value = viewId;
|
|
935
|
+
if (sidebar.activeView !== viewId) sidebar.activeView = viewId;
|
|
936
|
+
if (pillNav.currentItemId !== viewId) pillNav.currentItemId = viewId;
|
|
669
937
|
}
|
|
670
|
-
function
|
|
671
|
-
|
|
672
|
-
replaceControlValues({
|
|
673
|
-
...generatedControlDefaults.value,
|
|
674
|
-
...externalControlValues.value === void 0 ? internalControlValues : externalControlValues.value
|
|
675
|
-
});
|
|
676
|
-
syncingControlValues = false;
|
|
938
|
+
function setValues(nextValues) {
|
|
939
|
+
Object.assign(values, nextValues);
|
|
677
940
|
}
|
|
678
|
-
function
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
...
|
|
682
|
-
...values
|
|
941
|
+
function resetValues(nextValues = {}) {
|
|
942
|
+
replaceRecord$1(values, {
|
|
943
|
+
...schema.initialValues,
|
|
944
|
+
...nextValues
|
|
683
945
|
});
|
|
684
|
-
syncingControlValues = false;
|
|
685
|
-
emitControlValues();
|
|
686
946
|
}
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
return mapping === void 0 ? {} : controlValuesToComponentProps(internalControlValues, mapping);
|
|
692
|
-
});
|
|
693
|
-
const resolvedComponentPropsById = computed(() => {
|
|
694
|
-
const mappings = options.componentPropsById() ?? resolvedModel.value?.componentPropsById;
|
|
947
|
+
function getComponentProps(mapping) {
|
|
948
|
+
return controlValuesToComponentProps(values, mapping);
|
|
949
|
+
}
|
|
950
|
+
function getComponentPropsById(mappings) {
|
|
695
951
|
if (mappings === void 0) return {};
|
|
696
|
-
return Object.fromEntries(Object.entries(mappings).map(([id, mapping]) => [id, controlValuesToComponentProps(
|
|
697
|
-
}
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
952
|
+
return Object.fromEntries(Object.entries(mappings).map(([id, mapping]) => [id, controlValuesToComponentProps(values, mapping)]));
|
|
953
|
+
}
|
|
954
|
+
function getComponentBindings(bindings) {
|
|
955
|
+
return controlValuesToComponentBindings(values, bindings);
|
|
956
|
+
}
|
|
957
|
+
function getComponentBindingsById(bindings) {
|
|
958
|
+
return controlValuesToComponentBindingsById(values, bindings);
|
|
959
|
+
}
|
|
960
|
+
const componentProps = computed(() => model?.componentProps === void 0 ? {} : getComponentProps(model.componentProps));
|
|
961
|
+
const componentPropsById = computed(() => getComponentPropsById(model?.componentPropsById));
|
|
962
|
+
const componentBindings = computed(() => getComponentBindings(model?.componentBindings));
|
|
963
|
+
const componentBindingsById = computed(() => getComponentBindingsById(model?.componentBindings));
|
|
964
|
+
const form = {
|
|
965
|
+
...schema.form,
|
|
966
|
+
modelValue: values,
|
|
967
|
+
"onUpdate:modelValue": setValues
|
|
968
|
+
};
|
|
969
|
+
const topBarSettingsConfig = {
|
|
970
|
+
...schema.topBarSettingsConfig,
|
|
971
|
+
...topBarSettings ?? {},
|
|
972
|
+
values
|
|
973
|
+
};
|
|
974
|
+
const sidebar = reactive({
|
|
975
|
+
...schema.sidebar,
|
|
976
|
+
activeView: activeView.value,
|
|
977
|
+
modelValue: values,
|
|
978
|
+
"onUpdate:modelValue": setValues,
|
|
979
|
+
values,
|
|
980
|
+
"onUpdate:values": setValues
|
|
705
981
|
});
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
emitControlValues();
|
|
711
|
-
}, {
|
|
712
|
-
deep: true,
|
|
713
|
-
flush: "sync"
|
|
982
|
+
const pillNav = reactive({
|
|
983
|
+
items: schema.viewItems,
|
|
984
|
+
currentItemId: activeView.value,
|
|
985
|
+
onSelect: (item) => setActiveView(item.id)
|
|
714
986
|
});
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
generatedPillNav,
|
|
720
|
-
generatedDefaultView,
|
|
721
|
-
internalControlValues,
|
|
722
|
-
resolvedComponentBindings,
|
|
723
|
-
resolvedComponentBindingsById,
|
|
724
|
-
resolvedComponentProps,
|
|
725
|
-
resolvedComponentPropsById,
|
|
726
|
-
handleControlValuesUpdate
|
|
987
|
+
const topBarSettingsBinding = {
|
|
988
|
+
showSettings: true,
|
|
989
|
+
settingsConfig: topBarSettingsConfig,
|
|
990
|
+
onSettingsValuesChange: setValues
|
|
727
991
|
};
|
|
728
|
-
|
|
992
|
+
const topBarProps = computed(() => ({
|
|
993
|
+
pillNav: pillNav.items,
|
|
994
|
+
currentPillId: pillNav.currentItemId,
|
|
995
|
+
onPillSelect: pillNav.onSelect,
|
|
996
|
+
...topBarSettingsBinding
|
|
997
|
+
}));
|
|
998
|
+
const bindings = {
|
|
999
|
+
form,
|
|
1000
|
+
sidebar,
|
|
1001
|
+
topBar: topBarProps,
|
|
1002
|
+
topBarSettings: topBarSettingsBinding,
|
|
1003
|
+
pillNav,
|
|
1004
|
+
componentBindings,
|
|
1005
|
+
componentBindingsById,
|
|
1006
|
+
componentProps,
|
|
1007
|
+
componentPropsById
|
|
1008
|
+
};
|
|
1009
|
+
watch(activeView, syncActiveView, { flush: "sync" });
|
|
1010
|
+
watch(() => sidebar.activeView, syncActiveView, { flush: "sync" });
|
|
1011
|
+
watch(() => pillNav.currentItemId, syncActiveView, { flush: "sync" });
|
|
1012
|
+
return {
|
|
1013
|
+
...schema,
|
|
1014
|
+
schema,
|
|
1015
|
+
values,
|
|
1016
|
+
activeView,
|
|
1017
|
+
topBarSettingsConfig,
|
|
1018
|
+
form,
|
|
1019
|
+
sidebar,
|
|
1020
|
+
topBar: topBarProps,
|
|
1021
|
+
pillNav,
|
|
1022
|
+
topBarSettings: topBarSettingsBinding,
|
|
1023
|
+
bindings,
|
|
1024
|
+
componentBindings,
|
|
1025
|
+
componentBindingsById,
|
|
1026
|
+
componentProps,
|
|
1027
|
+
componentPropsById,
|
|
1028
|
+
setActiveView,
|
|
1029
|
+
setValues,
|
|
1030
|
+
resetValues,
|
|
1031
|
+
getComponentProps,
|
|
1032
|
+
getComponentPropsById,
|
|
1033
|
+
getComponentBindings,
|
|
1034
|
+
getComponentBindingsById
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
//#endregion
|
|
1038
|
+
//#region src/composables/controlSchemaDoseDesign.ts
|
|
1039
|
+
/** Return a default WellPlate prop mapping for generated control workspaces. */
|
|
1040
|
+
function defineWellPlateControlProps(options = {}) {
|
|
1041
|
+
const selectedWells = options.selectedWells ?? sourceKey("selectedWells");
|
|
1042
|
+
return compactComponentPropsMap({
|
|
1043
|
+
modelValue: selectedWells,
|
|
1044
|
+
"onUpdate:modelValue": options.onUpdateModelValue === false ? void 0 : options.onUpdateModelValue ?? updateControlValueSource(selectedWells),
|
|
1045
|
+
format: options.format ?? sourceKey("plateFormat"),
|
|
1046
|
+
wells: options.wells ?? sourceKey("wells"),
|
|
1047
|
+
disabled: options.disabled ?? sourceKey("disabled"),
|
|
1048
|
+
readonly: options.readonly ?? sourceKey("readonly"),
|
|
1049
|
+
showLegend: options.showLegend,
|
|
1050
|
+
showSampleTypeIndicator: options.showSampleTypeIndicator
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
/** Return a default DoseCalculator prop mapping for generated control workspaces. */
|
|
1054
|
+
function defineDoseCalculatorControlProps(options = {}) {
|
|
1055
|
+
const targetWells = options.targetWells ?? sourceKey("selectedWells");
|
|
1056
|
+
const wells = options.wells ?? sourceKey("wells");
|
|
1057
|
+
const onApplyToWells = options.onApplyToWells === false ? void 0 : options.onApplyToWells ?? defaultDoseApplyHandler(wells, targetWells);
|
|
1058
|
+
return compactComponentPropsMap({
|
|
1059
|
+
mode: options.mode ?? sourceKey("doseMode"),
|
|
1060
|
+
targetWells,
|
|
1061
|
+
disabled: options.disabled ?? sourceKey("disabled"),
|
|
1062
|
+
molecularWeight: options.molecularWeight,
|
|
1063
|
+
onApplyToWells
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
/** Return named WellPlate + DoseCalculator prop mappings for one dose-design control model. */
|
|
1067
|
+
function defineWellPlateDoseControlProps(options = {}) {
|
|
1068
|
+
return {
|
|
1069
|
+
[options.plateId ?? "plate"]: defineWellPlateControlProps(options.plate),
|
|
1070
|
+
[options.doseId ?? "dose"]: defineDoseCalculatorControlProps(options.dose)
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
1073
|
+
/** Return named WellPlate + DoseCalculator component bindings for one dose-design control model. */
|
|
1074
|
+
function defineWellPlateDoseComponentBindings(options = {}) {
|
|
1075
|
+
return [{
|
|
1076
|
+
id: options.plateId ?? "plate",
|
|
1077
|
+
component: "WellPlate",
|
|
1078
|
+
props: defineWellPlateControlProps(options.plate)
|
|
1079
|
+
}, {
|
|
1080
|
+
id: options.doseId ?? "dose",
|
|
1081
|
+
component: "DoseCalculator",
|
|
1082
|
+
props: defineDoseCalculatorControlProps(options.dose)
|
|
1083
|
+
}];
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* Return a complete ControlWorkspaceView model for WellPlate + DoseCalculator dose design.
|
|
1087
|
+
*
|
|
1088
|
+
* @example Recommended
|
|
1089
|
+
* ```ts
|
|
1090
|
+
* const workspaceModel = defineDoseDesignControlModel({ selectedWells: ['A1', 'A2'] })
|
|
1091
|
+
* ```
|
|
1092
|
+
*/
|
|
1093
|
+
function defineDoseDesignControlModel(options = {}) {
|
|
1094
|
+
const viewId = options.viewId ?? "design";
|
|
1095
|
+
const sectionId = options.sectionId ?? "dose";
|
|
1096
|
+
const doseControls = {
|
|
1097
|
+
selectedWells: {
|
|
1098
|
+
type: "tags",
|
|
1099
|
+
label: "Selected wells",
|
|
1100
|
+
default: [...options.selectedWells ?? ["A1", "A2"]]
|
|
1101
|
+
},
|
|
1102
|
+
plateFormat: {
|
|
1103
|
+
label: "Plate format",
|
|
1104
|
+
default: options.plateFormat ?? 96,
|
|
1105
|
+
options: options.plateFormats ?? [96, 384]
|
|
1106
|
+
},
|
|
1107
|
+
doseMode: {
|
|
1108
|
+
label: "Dose mode",
|
|
1109
|
+
default: options.doseMode ?? "serial",
|
|
1110
|
+
options: options.doseModes ?? [
|
|
1111
|
+
"serial",
|
|
1112
|
+
"dilution",
|
|
1113
|
+
"conversion"
|
|
1114
|
+
]
|
|
1115
|
+
},
|
|
1116
|
+
disabled: {
|
|
1117
|
+
label: "Disable tools",
|
|
1118
|
+
default: options.disabled ?? false
|
|
1119
|
+
}
|
|
1120
|
+
};
|
|
1121
|
+
if (options.includeMolecularWeight) doseControls.molecularWeight = {
|
|
1122
|
+
type: "number",
|
|
1123
|
+
label: "Molecular weight",
|
|
1124
|
+
default: options.molecularWeight ?? 300,
|
|
1125
|
+
min: 0
|
|
1126
|
+
};
|
|
1127
|
+
const componentProps = {
|
|
1128
|
+
...options.componentProps,
|
|
1129
|
+
dose: {
|
|
1130
|
+
...options.componentProps?.dose ?? {},
|
|
1131
|
+
...options.includeMolecularWeight && options.componentProps?.dose?.molecularWeight === void 0 ? { molecularWeight: "molecularWeight" } : {}
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
return defineControlModel({
|
|
1135
|
+
componentBindings: defineWellPlateDoseComponentBindings(componentProps),
|
|
1136
|
+
componentPropsById: defineWellPlateDoseControlProps(componentProps),
|
|
1137
|
+
views: { [viewId]: {
|
|
1138
|
+
label: options.viewLabel ?? "Design",
|
|
1139
|
+
sections: { [sectionId]: {
|
|
1140
|
+
label: options.sectionLabel ?? "Dose design",
|
|
1141
|
+
description: options.sectionDescription ?? "Well selection and dose calculator controls",
|
|
1142
|
+
controls: doseControls
|
|
1143
|
+
} }
|
|
1144
|
+
} }
|
|
1145
|
+
});
|
|
1146
|
+
}
|
|
1147
|
+
function compactComponentPropsMap(mapping) {
|
|
1148
|
+
return Object.fromEntries(Object.entries(mapping).filter((entry) => entry[1] !== void 0));
|
|
1149
|
+
}
|
|
1150
|
+
function sourceKey(key) {
|
|
1151
|
+
return key;
|
|
1152
|
+
}
|
|
1153
|
+
function updateControlValueSource(source) {
|
|
1154
|
+
if (typeof source !== "string") return void 0;
|
|
1155
|
+
return (values) => (value) => {
|
|
1156
|
+
const writableValues = values;
|
|
1157
|
+
writableValues[source] = value;
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
function defaultDoseApplyHandler(wells, targetWells) {
|
|
1161
|
+
if (wells === targetWells) return void 0;
|
|
1162
|
+
return updateWellsFromDoseResults(wells);
|
|
1163
|
+
}
|
|
1164
|
+
function updateWellsFromDoseResults(source) {
|
|
1165
|
+
if (typeof source !== "string") return void 0;
|
|
1166
|
+
return (values) => (results) => {
|
|
1167
|
+
const writableValues = values;
|
|
1168
|
+
const currentWells = recordValue(writableValues[source]);
|
|
1169
|
+
const nextWells = {};
|
|
1170
|
+
for (const [wellId, well] of Object.entries(currentWells)) nextWells[wellId] = recordValue(well);
|
|
1171
|
+
for (const result of results) {
|
|
1172
|
+
const existing = recordValue(nextWells[result.wellId]);
|
|
1173
|
+
const metadata = recordValue(existing.metadata);
|
|
1174
|
+
nextWells[result.wellId] = {
|
|
1175
|
+
...existing,
|
|
1176
|
+
state: "filled",
|
|
1177
|
+
value: result.concentration.value,
|
|
1178
|
+
metadata: {
|
|
1179
|
+
...metadata,
|
|
1180
|
+
concentration: result.concentration,
|
|
1181
|
+
...result.volume === void 0 ? {} : { volume: result.volume }
|
|
1182
|
+
}
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
writableValues[source] = nextWells;
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
//#endregion
|
|
1189
|
+
//#region src/composables/useFormValidation.ts
|
|
1190
|
+
var validators = {
|
|
1191
|
+
required: (value, message = "This field is required") => {
|
|
1192
|
+
if (value === null || value === void 0 || value === "") return message;
|
|
1193
|
+
if (Array.isArray(value) && value.length === 0) return message;
|
|
1194
|
+
return null;
|
|
1195
|
+
},
|
|
1196
|
+
minLength: (value, min, message) => {
|
|
1197
|
+
if (typeof value !== "string") return null;
|
|
1198
|
+
if (value.length < min) return message || `Must be at least ${min} characters`;
|
|
1199
|
+
return null;
|
|
1200
|
+
},
|
|
1201
|
+
maxLength: (value, max, message) => {
|
|
1202
|
+
if (typeof value !== "string") return null;
|
|
1203
|
+
if (value.length > max) return message || `Must be at most ${max} characters`;
|
|
1204
|
+
return null;
|
|
1205
|
+
},
|
|
1206
|
+
min: (value, min, message) => {
|
|
1207
|
+
if (typeof value !== "number") return null;
|
|
1208
|
+
if (value < min) return message || `Must be at least ${min}`;
|
|
1209
|
+
return null;
|
|
1210
|
+
},
|
|
1211
|
+
max: (value, max, message) => {
|
|
1212
|
+
if (typeof value !== "number") return null;
|
|
1213
|
+
if (value > max) return message || `Must be at most ${max}`;
|
|
1214
|
+
return null;
|
|
1215
|
+
},
|
|
1216
|
+
pattern: (value, pattern, message) => {
|
|
1217
|
+
if (typeof value !== "string") return null;
|
|
1218
|
+
if (!pattern.test(value)) return message || "Invalid format";
|
|
1219
|
+
return null;
|
|
1220
|
+
},
|
|
1221
|
+
email: (value, message = "Invalid email address") => {
|
|
1222
|
+
if (typeof value !== "string" || !value) return null;
|
|
1223
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return message;
|
|
1224
|
+
return null;
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
function validateFieldValue(value, fieldRules, formData) {
|
|
1228
|
+
if (!fieldRules) return null;
|
|
1229
|
+
if (fieldRules.required) {
|
|
1230
|
+
const message = typeof fieldRules.required === "string" ? fieldRules.required : void 0;
|
|
1231
|
+
const error = validators.required(value, message);
|
|
1232
|
+
if (error) return error;
|
|
1233
|
+
}
|
|
1234
|
+
if (value === null || value === void 0 || value === "") return null;
|
|
1235
|
+
if (fieldRules.minLength !== void 0) {
|
|
1236
|
+
const config = typeof fieldRules.minLength === "number" ? {
|
|
1237
|
+
value: fieldRules.minLength,
|
|
1238
|
+
message: void 0
|
|
1239
|
+
} : fieldRules.minLength;
|
|
1240
|
+
const error = validators.minLength(value, config.value, config.message);
|
|
1241
|
+
if (error) return error;
|
|
1242
|
+
}
|
|
1243
|
+
if (fieldRules.maxLength !== void 0) {
|
|
1244
|
+
const config = typeof fieldRules.maxLength === "number" ? {
|
|
1245
|
+
value: fieldRules.maxLength,
|
|
1246
|
+
message: void 0
|
|
1247
|
+
} : fieldRules.maxLength;
|
|
1248
|
+
const error = validators.maxLength(value, config.value, config.message);
|
|
1249
|
+
if (error) return error;
|
|
1250
|
+
}
|
|
1251
|
+
if (fieldRules.min !== void 0) {
|
|
1252
|
+
const config = typeof fieldRules.min === "number" ? {
|
|
1253
|
+
value: fieldRules.min,
|
|
1254
|
+
message: void 0
|
|
1255
|
+
} : fieldRules.min;
|
|
1256
|
+
const error = validators.min(value, config.value, config.message);
|
|
1257
|
+
if (error) return error;
|
|
1258
|
+
}
|
|
1259
|
+
if (fieldRules.max !== void 0) {
|
|
1260
|
+
const config = typeof fieldRules.max === "number" ? {
|
|
1261
|
+
value: fieldRules.max,
|
|
1262
|
+
message: void 0
|
|
1263
|
+
} : fieldRules.max;
|
|
1264
|
+
const error = validators.max(value, config.value, config.message);
|
|
1265
|
+
if (error) return error;
|
|
1266
|
+
}
|
|
1267
|
+
if (fieldRules.pattern !== void 0) {
|
|
1268
|
+
const config = fieldRules.pattern instanceof RegExp ? {
|
|
1269
|
+
value: fieldRules.pattern,
|
|
1270
|
+
message: void 0
|
|
1271
|
+
} : fieldRules.pattern;
|
|
1272
|
+
const error = validators.pattern(value, config.value, config.message);
|
|
1273
|
+
if (error) return error;
|
|
1274
|
+
}
|
|
1275
|
+
if (fieldRules.email) {
|
|
1276
|
+
const message = typeof fieldRules.email === "string" ? fieldRules.email : void 0;
|
|
1277
|
+
const error = validators.email(value, message);
|
|
1278
|
+
if (error) return error;
|
|
1279
|
+
}
|
|
1280
|
+
if (fieldRules.custom) {
|
|
1281
|
+
const customRules = Array.isArray(fieldRules.custom) ? fieldRules.custom : [fieldRules.custom];
|
|
1282
|
+
for (const rule of customRules) {
|
|
1283
|
+
const error = rule(value, formData);
|
|
1284
|
+
if (error) return error;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return null;
|
|
1288
|
+
}
|
|
1289
|
+
//#endregion
|
|
1290
|
+
//#region src/composables/useForm.ts
|
|
1291
|
+
/**
|
|
1292
|
+
* Form state management composable with validation.
|
|
1293
|
+
*
|
|
1294
|
+
* @param initialValues - Initial form values
|
|
1295
|
+
* @param rules - Validation rules for each field
|
|
1296
|
+
*
|
|
1297
|
+
* @example
|
|
1298
|
+
* ```typescript
|
|
1299
|
+
* const { data, errors, isValid, handleSubmit, getFieldProps } = useForm(
|
|
1300
|
+
* { email: '', password: '' },
|
|
1301
|
+
* {
|
|
1302
|
+
* email: { required: true, email: true },
|
|
1303
|
+
* password: { required: true, minLength: 8 },
|
|
1304
|
+
* }
|
|
1305
|
+
* )
|
|
1306
|
+
*
|
|
1307
|
+
* // In template
|
|
1308
|
+
* <BaseInput v-bind="getFieldProps('email')" label="Email" />
|
|
1309
|
+
* <BaseInput v-bind="getFieldProps('password')" type="password" label="Password" />
|
|
1310
|
+
* <BaseButton @click="handleSubmit(onSubmit)" :disabled="!isValid">Submit</BaseButton>
|
|
1311
|
+
* ```
|
|
1312
|
+
*/
|
|
1313
|
+
/** Reactive form state with field-level validation, dirty tracking, and submit handling. */
|
|
1314
|
+
function useForm(initialValues, rules = {}) {
|
|
1315
|
+
const cloneableInitialValues = deepToRaw(initialValues);
|
|
1316
|
+
let _initialValues = structuredClone(cloneableInitialValues);
|
|
1317
|
+
const data = reactive(structuredClone(cloneableInitialValues));
|
|
1318
|
+
const errors = reactive(Object.keys(initialValues).reduce((acc, key) => {
|
|
1319
|
+
acc[key] = null;
|
|
1320
|
+
return acc;
|
|
1321
|
+
}, {}));
|
|
1322
|
+
const touched = reactive(Object.keys(initialValues).reduce((acc, key) => {
|
|
1323
|
+
acc[key] = false;
|
|
1324
|
+
return acc;
|
|
1325
|
+
}, {}));
|
|
1326
|
+
const dirty = reactive(Object.keys(initialValues).reduce((acc, key) => {
|
|
1327
|
+
acc[key] = false;
|
|
1328
|
+
return acc;
|
|
1329
|
+
}, {}));
|
|
1330
|
+
const isSubmitting = ref(false);
|
|
1331
|
+
watch(() => ({ ...data }), (newData) => {
|
|
1332
|
+
for (const key of Object.keys(newData)) dirty[key] = newData[key] !== _initialValues[key];
|
|
1333
|
+
}, { deep: true });
|
|
1334
|
+
function validateField(field) {
|
|
1335
|
+
const value = data[field];
|
|
1336
|
+
const fieldRules = rules[field];
|
|
1337
|
+
const error = validateFieldValue(value, fieldRules, data);
|
|
1338
|
+
errors[field] = error;
|
|
1339
|
+
return error === null;
|
|
1340
|
+
}
|
|
1341
|
+
function validate() {
|
|
1342
|
+
let isAllValid = true;
|
|
1343
|
+
for (const field of Object.keys(data)) if (!validateField(field)) isAllValid = false;
|
|
1344
|
+
return isAllValid;
|
|
1345
|
+
}
|
|
1346
|
+
const isValid = computed(() => {
|
|
1347
|
+
return Object.values(errors).every((error) => error === null);
|
|
1348
|
+
});
|
|
1349
|
+
const isDirty = computed(() => {
|
|
1350
|
+
return Object.values(dirty).some((d) => d);
|
|
1351
|
+
});
|
|
1352
|
+
function setFieldValue(field, value) {
|
|
1353
|
+
data[field] = value;
|
|
1354
|
+
if (touched[field]) validateField(field);
|
|
1355
|
+
}
|
|
1356
|
+
function setFieldError(field, error) {
|
|
1357
|
+
errors[field] = error;
|
|
1358
|
+
}
|
|
1359
|
+
function setFieldTouched(field, isTouched = true) {
|
|
1360
|
+
touched[field] = isTouched;
|
|
1361
|
+
if (isTouched) validateField(field);
|
|
1362
|
+
}
|
|
1363
|
+
function reset(values) {
|
|
1364
|
+
const resetValues = values ? {
|
|
1365
|
+
..._initialValues,
|
|
1366
|
+
...values
|
|
1367
|
+
} : _initialValues;
|
|
1368
|
+
for (const key of Object.keys(data)) {
|
|
1369
|
+
data[key] = structuredClone(deepToRaw(resetValues[key]));
|
|
1370
|
+
errors[key] = null;
|
|
1371
|
+
touched[key] = false;
|
|
1372
|
+
dirty[key] = false;
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
function replaceState(values) {
|
|
1376
|
+
const nextValues = structuredClone(deepToRaw(values));
|
|
1377
|
+
_initialValues = structuredClone(nextValues);
|
|
1378
|
+
for (const key of Object.keys(data)) {
|
|
1379
|
+
if (key in nextValues) continue;
|
|
1380
|
+
delete data[key];
|
|
1381
|
+
delete errors[key];
|
|
1382
|
+
delete touched[key];
|
|
1383
|
+
delete dirty[key];
|
|
1384
|
+
}
|
|
1385
|
+
for (const key of Object.keys(nextValues)) {
|
|
1386
|
+
data[key] = structuredClone(deepToRaw(nextValues[key]));
|
|
1387
|
+
errors[key] = null;
|
|
1388
|
+
touched[key] = false;
|
|
1389
|
+
dirty[key] = false;
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
function handleSubmit(onSubmit) {
|
|
1393
|
+
return async (e) => {
|
|
1394
|
+
e?.preventDefault();
|
|
1395
|
+
for (const field of Object.keys(data)) touched[field] = true;
|
|
1396
|
+
if (!validate()) return;
|
|
1397
|
+
isSubmitting.value = true;
|
|
1398
|
+
try {
|
|
1399
|
+
await onSubmit(data);
|
|
1400
|
+
} finally {
|
|
1401
|
+
isSubmitting.value = false;
|
|
1402
|
+
}
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
function getFieldProps(field) {
|
|
1406
|
+
const fieldStr = field;
|
|
1407
|
+
return {
|
|
1408
|
+
modelValue: data[field],
|
|
1409
|
+
"onUpdate:modelValue": (value) => setFieldValue(field, value),
|
|
1410
|
+
onBlur: () => setFieldTouched(fieldStr),
|
|
1411
|
+
error: touched[fieldStr] ? errors[fieldStr] : null
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
return {
|
|
1415
|
+
data,
|
|
1416
|
+
errors,
|
|
1417
|
+
touched,
|
|
1418
|
+
dirty,
|
|
1419
|
+
isValid,
|
|
1420
|
+
isDirty,
|
|
1421
|
+
isSubmitting,
|
|
1422
|
+
setFieldValue,
|
|
1423
|
+
setFieldError,
|
|
1424
|
+
setFieldTouched,
|
|
1425
|
+
validateField,
|
|
1426
|
+
validate,
|
|
1427
|
+
reset,
|
|
1428
|
+
replaceState,
|
|
1429
|
+
handleSubmit,
|
|
1430
|
+
getFieldProps
|
|
1431
|
+
};
|
|
1432
|
+
}
|
|
1433
|
+
function deepToRaw(value) {
|
|
1434
|
+
const raw = toRaw(value);
|
|
1435
|
+
if (Array.isArray(raw)) return raw.map((item) => deepToRaw(item));
|
|
1436
|
+
if (isPlainRecord(raw)) return Object.fromEntries(Object.entries(raw).map(([key, item]) => [key, deepToRaw(item)]));
|
|
1437
|
+
return raw;
|
|
1438
|
+
}
|
|
1439
|
+
function isPlainRecord(value) {
|
|
1440
|
+
return Object.prototype.toString.call(value) === "[object Object]";
|
|
1441
|
+
}
|
|
1442
|
+
//#endregion
|
|
1443
|
+
//#region src/composables/formBuilderSchema.ts
|
|
1444
|
+
/**
|
|
1445
|
+
* Evaluate a JSON-serializable field condition against the current form data.
|
|
1446
|
+
*
|
|
1447
|
+
* Supports logical operators (`and`, `or`, `not`) and comparison operators
|
|
1448
|
+
* (`eq`, `neq`, `gt`, `lt`, `gte`, `lte`, `in`, `notIn`, `truthy`, `falsy`,
|
|
1449
|
+
* `contains`). Returns `true` if the condition passes.
|
|
1450
|
+
*/
|
|
1451
|
+
function evaluateCondition(condition, data) {
|
|
1452
|
+
if ("and" in condition) return condition.and.every((c) => evaluateCondition(c, data));
|
|
1453
|
+
if ("or" in condition) return condition.or.some((c) => evaluateCondition(c, data));
|
|
1454
|
+
if ("not" in condition) return !evaluateCondition(condition.not, data);
|
|
1455
|
+
const value = data[condition.field];
|
|
1456
|
+
if ("eq" in condition) return value === condition.eq;
|
|
1457
|
+
if ("neq" in condition) return value !== condition.neq;
|
|
1458
|
+
if ("gt" in condition) return typeof value === "number" && value > condition.gt;
|
|
1459
|
+
if ("lt" in condition) return typeof value === "number" && value < condition.lt;
|
|
1460
|
+
if ("gte" in condition) return typeof value === "number" && value >= condition.gte;
|
|
1461
|
+
if ("lte" in condition) return typeof value === "number" && value <= condition.lte;
|
|
1462
|
+
if ("in" in condition) return condition.in.includes(value);
|
|
1463
|
+
if ("notIn" in condition) return !condition.notIn.includes(value);
|
|
1464
|
+
if ("truthy" in condition) return !!value;
|
|
1465
|
+
if ("falsy" in condition) return !value;
|
|
1466
|
+
if ("contains" in condition) {
|
|
1467
|
+
if (typeof value === "string") return value.includes(condition.contains);
|
|
1468
|
+
if (Array.isArray(value)) return value.includes(condition.contains);
|
|
1469
|
+
return false;
|
|
1470
|
+
}
|
|
1471
|
+
return true;
|
|
1472
|
+
}
|
|
1473
|
+
/** Return all sections across steps (wizard) or directly from a flat schema. */
|
|
1474
|
+
function collectSections(schema) {
|
|
1475
|
+
return schema.steps ? schema.steps.flatMap((step) => step.sections) : schema.sections;
|
|
1476
|
+
}
|
|
1477
|
+
/** Return all field schemas in schema order, flattening sections and steps. */
|
|
1478
|
+
function flattenFields(schema) {
|
|
1479
|
+
return collectSections(schema).flatMap((section) => section.fields);
|
|
1480
|
+
}
|
|
1481
|
+
/** Convert a JSON-safe `FieldValidation` descriptor to a runtime `FieldRules` object. */
|
|
1482
|
+
function convertValidation(v) {
|
|
1483
|
+
const rules = {};
|
|
1484
|
+
if (v.required !== void 0) rules.required = v.required;
|
|
1485
|
+
if (v.minLength !== void 0) rules.minLength = v.minLength;
|
|
1486
|
+
if (v.maxLength !== void 0) rules.maxLength = v.maxLength;
|
|
1487
|
+
if (v.min !== void 0) rules.min = v.min;
|
|
1488
|
+
if (v.max !== void 0) rules.max = v.max;
|
|
1489
|
+
if (v.email !== void 0) rules.email = v.email;
|
|
1490
|
+
if (v.pattern !== void 0) rules.pattern = typeof v.pattern === "string" ? new RegExp(v.pattern) : {
|
|
1491
|
+
value: new RegExp(v.pattern.value),
|
|
1492
|
+
message: v.pattern.message
|
|
1493
|
+
};
|
|
1494
|
+
return rules;
|
|
1495
|
+
}
|
|
1496
|
+
function buildInitialValues(fields, initialData) {
|
|
1497
|
+
const initialValues = {};
|
|
1498
|
+
for (const field of fields) {
|
|
1499
|
+
const key = field.name;
|
|
1500
|
+
if (initialData && hasOwnKey(initialData, key)) initialValues[key] = initialData[key];
|
|
1501
|
+
else if (field.defaultValue !== void 0) initialValues[key] = field.defaultValue;
|
|
1502
|
+
else initialValues[key] = getTypeDefault(field.type);
|
|
1503
|
+
}
|
|
1504
|
+
return initialValues;
|
|
1505
|
+
}
|
|
1506
|
+
function buildRules(fields, enhancements) {
|
|
1507
|
+
const rules = {};
|
|
1508
|
+
for (const field of fields) {
|
|
1509
|
+
const base = field.validation ? convertValidation(field.validation) : {};
|
|
1510
|
+
const enhancement = enhancements?.fields?.[field.name];
|
|
1511
|
+
const customValidators = [];
|
|
1512
|
+
if (enhancement?.validate) {
|
|
1513
|
+
const fn = enhancement.validate;
|
|
1514
|
+
customValidators.push((value, formData) => fn(value, formData));
|
|
1515
|
+
}
|
|
1516
|
+
if (customValidators.length > 0) base.custom = customValidators;
|
|
1517
|
+
if (Object.keys(base).length > 0) rules[field.name] = base;
|
|
1518
|
+
}
|
|
1519
|
+
return rules;
|
|
1520
|
+
}
|
|
1521
|
+
function replaceArray(target, values) {
|
|
1522
|
+
target.splice(0, target.length, ...values);
|
|
1523
|
+
}
|
|
1524
|
+
function replaceRecord(target, source) {
|
|
1525
|
+
for (const key of Object.keys(target)) delete target[key];
|
|
1526
|
+
Object.assign(target, source);
|
|
1527
|
+
}
|
|
1528
|
+
function hasOwnKey(source, key) {
|
|
1529
|
+
return Object.prototype.hasOwnProperty.call(source, key);
|
|
1530
|
+
}
|
|
1531
|
+
//#endregion
|
|
1532
|
+
//#region src/composables/useFormBuilder.ts
|
|
1533
|
+
/**
|
|
1534
|
+
* Drive a `FormSchema` as reactive form state.
|
|
1535
|
+
*
|
|
1536
|
+
* Builds initial values from schema defaults and `initialData`, derives
|
|
1537
|
+
* validation rules from `FieldValidation` descriptors and enhancement
|
|
1538
|
+
* validators, evaluates `FieldCondition` expressions for field/section
|
|
1539
|
+
* visibility, and wires wizard step navigation when `schema.steps` is set.
|
|
1540
|
+
*
|
|
1541
|
+
* @param schema - Declarative form or wizard schema.
|
|
1542
|
+
* @param initialData - Values that override schema defaults.
|
|
1543
|
+
* @param enhancements - TypeScript-only callbacks (dynamic options, validators,
|
|
1544
|
+
* submit handler, transform, field-change watcher).
|
|
1545
|
+
*/
|
|
1546
|
+
/** Drives a declarative FormSchema as reactive state with conditional fields, wizard steps, and validation. */
|
|
1547
|
+
function useFormBuilder(schema, initialData, enhancements) {
|
|
1548
|
+
const currentSchema = shallowRef(schema);
|
|
1549
|
+
const fields = reactive(flattenFields(schema));
|
|
1550
|
+
const sections = reactive(collectSections(schema));
|
|
1551
|
+
const rules = reactive(buildRules(fields, enhancements));
|
|
1552
|
+
const form = useForm(buildInitialValues(fields, initialData), rules);
|
|
1553
|
+
function isFieldVisible(name) {
|
|
1554
|
+
const enhancement = enhancements?.fields?.[name];
|
|
1555
|
+
if (enhancement?.visible) return enhancement.visible(form.data);
|
|
1556
|
+
const field = fields.find((f) => f.name === name);
|
|
1557
|
+
if (field?.condition) return evaluateCondition(field.condition, form.data);
|
|
1558
|
+
return true;
|
|
1559
|
+
}
|
|
1560
|
+
function isSectionVisible(id) {
|
|
1561
|
+
const section = sections.find((s) => s.id === id);
|
|
1562
|
+
if (section?.condition) return evaluateCondition(section.condition, form.data);
|
|
1563
|
+
return true;
|
|
1564
|
+
}
|
|
1565
|
+
function getResolvedFieldProps(field) {
|
|
1566
|
+
const entry = getFieldRegistryEntry(field.type);
|
|
1567
|
+
const formProps = form.getFieldProps(field.name);
|
|
1568
|
+
const merged = {
|
|
1569
|
+
...entry.defaults,
|
|
1570
|
+
...field.props ?? {}
|
|
1571
|
+
};
|
|
1572
|
+
const enhancement = enhancements?.fields?.[field.name];
|
|
1573
|
+
if (enhancement?.props) Object.assign(merged, enhancement.props(form.data));
|
|
1574
|
+
const options = getFieldOptions(field.name);
|
|
1575
|
+
if (options) merged.options = options;
|
|
1576
|
+
if (entry.vModel) {
|
|
1577
|
+
merged.modelValue = formProps.modelValue;
|
|
1578
|
+
merged["onUpdate:modelValue"] = formProps["onUpdate:modelValue"];
|
|
1579
|
+
}
|
|
1580
|
+
merged.onBlur = formProps.onBlur;
|
|
1581
|
+
if (formProps.error) merged.error = true;
|
|
1582
|
+
if (field.placeholder) merged.placeholder = field.placeholder;
|
|
1583
|
+
if (field.size) merged.size = field.size;
|
|
1584
|
+
if (field.disabled) merged.disabled = true;
|
|
1585
|
+
if (field.readonly) merged.readonly = true;
|
|
1586
|
+
if (field.type === "radio" && !merged.name) merged.name = field.name;
|
|
1587
|
+
return merged;
|
|
1588
|
+
}
|
|
1589
|
+
function getFieldOptions(name) {
|
|
1590
|
+
const enhancement = enhancements?.fields?.[name];
|
|
1591
|
+
if (enhancement?.options) return enhancement.options(form.data);
|
|
1592
|
+
const field = fields.find((f) => f.name === name);
|
|
1593
|
+
if (field?.props?.options) return field.props.options;
|
|
1594
|
+
}
|
|
1595
|
+
const currentStep = ref(0);
|
|
1596
|
+
const isCurrentStepValid = computed(() => {
|
|
1597
|
+
if (!currentSchema.value.steps) return form.isValid.value;
|
|
1598
|
+
const step = currentSchema.value.steps[currentStep.value];
|
|
1599
|
+
if (!step) return true;
|
|
1600
|
+
for (const section of step.sections) {
|
|
1601
|
+
if (!isSectionVisible(section.id)) continue;
|
|
1602
|
+
for (const field of section.fields) {
|
|
1603
|
+
if (!isFieldVisible(field.name)) continue;
|
|
1604
|
+
if (!form.validateField(field.name)) return false;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
return true;
|
|
1608
|
+
});
|
|
1609
|
+
function goNext() {
|
|
1610
|
+
if (!currentSchema.value.steps) return false;
|
|
1611
|
+
const step = currentSchema.value.steps[currentStep.value];
|
|
1612
|
+
if (!step) return false;
|
|
1613
|
+
let valid = true;
|
|
1614
|
+
for (const section of step.sections) {
|
|
1615
|
+
if (!isSectionVisible(section.id)) continue;
|
|
1616
|
+
for (const field of section.fields) {
|
|
1617
|
+
if (!isFieldVisible(field.name)) continue;
|
|
1618
|
+
form.setFieldTouched(field.name, true);
|
|
1619
|
+
if (!form.validateField(field.name)) valid = false;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
if (!valid) return false;
|
|
1623
|
+
if (currentStep.value < currentSchema.value.steps.length - 1) currentStep.value++;
|
|
1624
|
+
return true;
|
|
1625
|
+
}
|
|
1626
|
+
function goBack() {
|
|
1627
|
+
if (currentStep.value > 0) currentStep.value--;
|
|
1628
|
+
}
|
|
1629
|
+
function goToStep(index) {
|
|
1630
|
+
if (!currentSchema.value.steps) return;
|
|
1631
|
+
if (index >= 0 && index < currentSchema.value.steps.length) currentStep.value = index;
|
|
1632
|
+
}
|
|
1633
|
+
function validate() {
|
|
1634
|
+
let allValid = true;
|
|
1635
|
+
for (const field of fields) {
|
|
1636
|
+
if (!isFieldVisible(field.name)) continue;
|
|
1637
|
+
form.setFieldTouched(field.name, true);
|
|
1638
|
+
if (!form.validateField(field.name)) allValid = false;
|
|
1639
|
+
}
|
|
1640
|
+
return allValid;
|
|
1641
|
+
}
|
|
1642
|
+
async function submit() {
|
|
1643
|
+
if (!validate()) return;
|
|
1644
|
+
let submitData = {};
|
|
1645
|
+
for (const field of fields) if (isFieldVisible(field.name)) submitData[field.name] = form.data[field.name];
|
|
1646
|
+
if (enhancements?.transform) submitData = enhancements.transform(submitData);
|
|
1647
|
+
if (enhancements?.onSubmit) {
|
|
1648
|
+
form.isSubmitting.value = true;
|
|
1649
|
+
try {
|
|
1650
|
+
await enhancements.onSubmit(submitData);
|
|
1651
|
+
} finally {
|
|
1652
|
+
form.isSubmitting.value = false;
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
function reset(values) {
|
|
1657
|
+
form.reset(values);
|
|
1658
|
+
currentStep.value = 0;
|
|
1659
|
+
}
|
|
1660
|
+
function updateSchema(nextSchema, nextData) {
|
|
1661
|
+
currentSchema.value = nextSchema;
|
|
1662
|
+
const nextFields = flattenFields(nextSchema);
|
|
1663
|
+
replaceArray(fields, nextFields);
|
|
1664
|
+
replaceArray(sections, collectSections(nextSchema));
|
|
1665
|
+
replaceRecord(rules, buildRules(nextFields, enhancements));
|
|
1666
|
+
form.replaceState(buildInitialValues(nextFields, nextData));
|
|
1667
|
+
currentStep.value = 0;
|
|
1668
|
+
}
|
|
1669
|
+
if (enhancements?.onFieldChange) {
|
|
1670
|
+
const callback = enhancements.onFieldChange;
|
|
1671
|
+
watch(() => ({ ...form.data }), (newData, oldData) => {
|
|
1672
|
+
if (!oldData) return;
|
|
1673
|
+
for (const key of Object.keys(newData)) if (newData[key] !== oldData[key]) callback(key, newData[key], newData);
|
|
1674
|
+
}, { deep: true });
|
|
1675
|
+
}
|
|
1676
|
+
return {
|
|
1677
|
+
form,
|
|
1678
|
+
rules,
|
|
1679
|
+
isFieldVisible,
|
|
1680
|
+
isSectionVisible,
|
|
1681
|
+
fields,
|
|
1682
|
+
getResolvedFieldProps,
|
|
1683
|
+
getFieldOptions,
|
|
1684
|
+
currentStep,
|
|
1685
|
+
isCurrentStepValid,
|
|
1686
|
+
goNext,
|
|
1687
|
+
goBack,
|
|
1688
|
+
goToStep,
|
|
1689
|
+
validate,
|
|
1690
|
+
reset,
|
|
1691
|
+
submit,
|
|
1692
|
+
updateSchema
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
//#endregion
|
|
1696
|
+
//#region src/composables/useMobileSupportGate.ts
|
|
1697
|
+
var DEFAULT_MOBILE_VIEWPORT_QUERY = "(max-width: 767px)";
|
|
1698
|
+
function useMobileSupportGate(options = {}) {
|
|
1699
|
+
const isMobileViewport = ref(false);
|
|
1700
|
+
const mediaQuery = computed(() => {
|
|
1701
|
+
return unref(options.mediaQuery)?.trim() || "(max-width: 767px)";
|
|
1702
|
+
});
|
|
1703
|
+
const isSupported = computed(() => {
|
|
1704
|
+
if (options.supported === void 0) return true;
|
|
1705
|
+
return Boolean(unref(options.supported));
|
|
1706
|
+
});
|
|
1707
|
+
const shouldShowUnsupported = computed(() => isMobileViewport.value && !isSupported.value);
|
|
1708
|
+
let mediaQueryList = null;
|
|
1709
|
+
let removeMediaListener;
|
|
1710
|
+
const syncMobileState = (event) => {
|
|
1711
|
+
isMobileViewport.value = Boolean(event?.matches ?? mediaQueryList?.matches);
|
|
1712
|
+
};
|
|
1713
|
+
const unbindMediaQuery = () => {
|
|
1714
|
+
removeMediaListener?.();
|
|
1715
|
+
removeMediaListener = void 0;
|
|
1716
|
+
mediaQueryList = null;
|
|
1717
|
+
};
|
|
1718
|
+
const bindMediaQuery = () => {
|
|
1719
|
+
unbindMediaQuery();
|
|
1720
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
1721
|
+
isMobileViewport.value = false;
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
const queryList = window.matchMedia(mediaQuery.value);
|
|
1725
|
+
mediaQueryList = queryList;
|
|
1726
|
+
syncMobileState(queryList);
|
|
1727
|
+
const handleChange = (event) => syncMobileState(event);
|
|
1728
|
+
if (typeof queryList.addEventListener === "function") {
|
|
1729
|
+
queryList.addEventListener("change", handleChange);
|
|
1730
|
+
removeMediaListener = () => queryList.removeEventListener("change", handleChange);
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
queryList.addListener(handleChange);
|
|
1734
|
+
removeMediaListener = () => queryList.removeListener(handleChange);
|
|
1735
|
+
};
|
|
1736
|
+
onMounted(bindMediaQuery);
|
|
1737
|
+
watch(mediaQuery, bindMediaQuery, { flush: "post" });
|
|
1738
|
+
onUnmounted(unbindMediaQuery);
|
|
1739
|
+
return {
|
|
1740
|
+
isMobileViewport,
|
|
1741
|
+
isSupported,
|
|
1742
|
+
shouldShowUnsupported
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
//#endregion
|
|
1746
|
+
//#region src/components/PluginWorkspaceView.controls.ts
|
|
1747
|
+
function usePluginWorkspaceControls(options) {
|
|
1748
|
+
const resolvedModel = computed(() => resolveControlModel(options.model()));
|
|
1749
|
+
const resolvedControls = computed(() => options.controls() ?? resolvedModel.value?.controls);
|
|
1750
|
+
const resolvedControlOptions = computed(() => mergeControlWorkspaceOptions(resolvedModel.value?.controlOptions ?? {}, options.controlOptions()));
|
|
1751
|
+
const externalControlValues = computed(() => options.modelValue() ?? options.values());
|
|
1752
|
+
const internalControlValues = reactive({});
|
|
1753
|
+
let syncingControlValues = false;
|
|
1754
|
+
const generatedControlDefaults = computed(() => {
|
|
1755
|
+
if (!resolvedControls.value) return {};
|
|
1756
|
+
return {
|
|
1757
|
+
...getControlDefaults(resolvedControls.value),
|
|
1758
|
+
...resolvedControlOptions.value.initialValues ?? {}
|
|
1759
|
+
};
|
|
1760
|
+
});
|
|
1761
|
+
function replaceControlValues(values) {
|
|
1762
|
+
for (const key of Object.keys(internalControlValues)) if (!(key in values)) delete internalControlValues[key];
|
|
1763
|
+
Object.assign(internalControlValues, values);
|
|
1764
|
+
}
|
|
1765
|
+
function emitControlValues() {
|
|
1766
|
+
options.emitValues({ ...internalControlValues });
|
|
1767
|
+
}
|
|
1768
|
+
function syncResolvedControlValues() {
|
|
1769
|
+
syncingControlValues = true;
|
|
1770
|
+
replaceControlValues({
|
|
1771
|
+
...generatedControlDefaults.value,
|
|
1772
|
+
...externalControlValues.value === void 0 ? internalControlValues : externalControlValues.value
|
|
1773
|
+
});
|
|
1774
|
+
syncingControlValues = false;
|
|
1775
|
+
}
|
|
1776
|
+
function handleControlValuesUpdate(values) {
|
|
1777
|
+
syncingControlValues = true;
|
|
1778
|
+
replaceControlValues({
|
|
1779
|
+
...generatedControlDefaults.value,
|
|
1780
|
+
...values
|
|
1781
|
+
});
|
|
1782
|
+
syncingControlValues = false;
|
|
1783
|
+
emitControlValues();
|
|
1784
|
+
}
|
|
1785
|
+
const resolvedComponentBindings = computed(() => controlValuesToComponentBindings(internalControlValues, options.componentBindings() ?? resolvedModel.value?.componentBindings));
|
|
1786
|
+
const resolvedComponentBindingsById = computed(() => controlValuesToComponentBindingsById(internalControlValues, options.componentBindings() ?? resolvedModel.value?.componentBindings));
|
|
1787
|
+
const resolvedComponentProps = computed(() => {
|
|
1788
|
+
const mapping = options.componentProps() ?? resolvedModel.value?.componentProps;
|
|
1789
|
+
return mapping === void 0 ? {} : controlValuesToComponentProps(internalControlValues, mapping);
|
|
1790
|
+
});
|
|
1791
|
+
const resolvedComponentPropsById = computed(() => {
|
|
1792
|
+
const mappings = options.componentPropsById() ?? resolvedModel.value?.componentPropsById;
|
|
1793
|
+
if (mappings === void 0) return {};
|
|
1794
|
+
return Object.fromEntries(Object.entries(mappings).map(([id, mapping]) => [id, controlValuesToComponentProps(internalControlValues, mapping)]));
|
|
1795
|
+
});
|
|
1796
|
+
const generatedPillNav = computed(() => {
|
|
1797
|
+
if (!resolvedControls.value) return [];
|
|
1798
|
+
return controlsToViewItems(resolvedControls.value, resolvedControlOptions.value);
|
|
1799
|
+
});
|
|
1800
|
+
const generatedDefaultView = computed(() => {
|
|
1801
|
+
if (!resolvedControls.value) return "";
|
|
1802
|
+
return getDefaultControlView(resolvedControls.value, resolvedControlOptions.value);
|
|
1803
|
+
});
|
|
1804
|
+
syncResolvedControlValues();
|
|
1805
|
+
watch([generatedControlDefaults, externalControlValues], syncResolvedControlValues, { deep: true });
|
|
1806
|
+
watch(internalControlValues, () => {
|
|
1807
|
+
if (syncingControlValues) return;
|
|
1808
|
+
emitControlValues();
|
|
1809
|
+
}, {
|
|
1810
|
+
deep: true,
|
|
1811
|
+
flush: "sync"
|
|
1812
|
+
});
|
|
1813
|
+
return {
|
|
1814
|
+
resolvedModel,
|
|
1815
|
+
resolvedControls,
|
|
1816
|
+
resolvedControlOptions,
|
|
1817
|
+
generatedPillNav,
|
|
1818
|
+
generatedDefaultView,
|
|
1819
|
+
internalControlValues,
|
|
1820
|
+
resolvedComponentBindings,
|
|
1821
|
+
resolvedComponentBindingsById,
|
|
1822
|
+
resolvedComponentProps,
|
|
1823
|
+
resolvedComponentPropsById,
|
|
1824
|
+
handleControlValuesUpdate
|
|
1825
|
+
};
|
|
1826
|
+
}
|
|
729
1827
|
//#endregion
|
|
730
1828
|
//#region src/components/PluginWorkspaceView.navigation.ts
|
|
731
1829
|
function usePluginWorkspaceNavigation(options) {
|
|
@@ -773,7 +1871,7 @@ function firstItemId(items) {
|
|
|
773
1871
|
}
|
|
774
1872
|
//#endregion
|
|
775
1873
|
//#region src/components/PluginWorkspaceView.shell.ts
|
|
776
|
-
var topBarForwardedSlots = new Set([
|
|
1874
|
+
var topBarForwardedSlots = /* @__PURE__ */ new Set([
|
|
777
1875
|
"icon",
|
|
778
1876
|
"logo",
|
|
779
1877
|
"page-selector-icon",
|
|
@@ -785,7 +1883,7 @@ var topBarForwardedSlots = new Set([
|
|
|
785
1883
|
"account-menu-item-icon",
|
|
786
1884
|
"settings-appearance"
|
|
787
1885
|
]);
|
|
788
|
-
var sidebarForwardedSlots = new Set([
|
|
1886
|
+
var sidebarForwardedSlots = /* @__PURE__ */ new Set([
|
|
789
1887
|
"header",
|
|
790
1888
|
"collapsed",
|
|
791
1889
|
"collapsed-footer",
|
|
@@ -835,116 +1933,818 @@ function usePluginWorkspace(options) {
|
|
|
835
1933
|
emit("update:values", values);
|
|
836
1934
|
}
|
|
837
1935
|
});
|
|
838
|
-
const navigationState = usePluginWorkspaceNavigation({
|
|
839
|
-
activeView: () => props.activeView,
|
|
840
|
-
defaultActiveView: () => props.defaultActiveView,
|
|
841
|
-
currentPillId: () => props.currentPillId,
|
|
842
|
-
currentPageSelectorId: () => props.currentPageSelectorId,
|
|
843
|
-
pillNav: () => props.pillNav,
|
|
844
|
-
pageSelector: () => props.pageSelector,
|
|
845
|
-
panelIds: () => Object.keys(props.panels ?? {}),
|
|
846
|
-
generatedPillNav: controlsState.generatedPillNav,
|
|
847
|
-
generatedDefaultView: controlsState.generatedDefaultView,
|
|
848
|
-
emitActiveView: (viewId) => emit("update:activeView", viewId),
|
|
849
|
-
emitPageSelectorSelect: (page) => emit("page-selector-select", page),
|
|
850
|
-
emitPillSelect: (item) => emit("pill-select", item)
|
|
1936
|
+
const navigationState = usePluginWorkspaceNavigation({
|
|
1937
|
+
activeView: () => props.activeView,
|
|
1938
|
+
defaultActiveView: () => props.defaultActiveView,
|
|
1939
|
+
currentPillId: () => props.currentPillId,
|
|
1940
|
+
currentPageSelectorId: () => props.currentPageSelectorId,
|
|
1941
|
+
pillNav: () => props.pillNav,
|
|
1942
|
+
pageSelector: () => props.pageSelector,
|
|
1943
|
+
panelIds: () => Object.keys(props.panels ?? {}),
|
|
1944
|
+
generatedPillNav: controlsState.generatedPillNav,
|
|
1945
|
+
generatedDefaultView: controlsState.generatedDefaultView,
|
|
1946
|
+
emitActiveView: (viewId) => emit("update:activeView", viewId),
|
|
1947
|
+
emitPageSelectorSelect: (page) => emit("page-selector-select", page),
|
|
1948
|
+
emitPillSelect: (item) => emit("pill-select", item)
|
|
1949
|
+
});
|
|
1950
|
+
const topBarSlotNames = computed(() => getPluginWorkspaceTopBarSlotNames(slots));
|
|
1951
|
+
const sidebarSlotNames = computed(() => getPluginWorkspaceSidebarSlotNames(slots));
|
|
1952
|
+
const hasSidebarSurface = computed(() => hasPluginWorkspaceSidebarSurface({
|
|
1953
|
+
showSidebar: props.showSidebar ?? true,
|
|
1954
|
+
panels: props.panels ?? {},
|
|
1955
|
+
forms: props.forms ?? {},
|
|
1956
|
+
model: props.model,
|
|
1957
|
+
controls: props.controls,
|
|
1958
|
+
showSidebarWhenEmpty: props.showSidebarWhenEmpty ?? false,
|
|
1959
|
+
sidebarContentId: props.sidebarContentId,
|
|
1960
|
+
sidebarTitle: props.sidebarTitle,
|
|
1961
|
+
sidebarSubtitle: props.sidebarSubtitle,
|
|
1962
|
+
sidebarBadge: props.sidebarBadge,
|
|
1963
|
+
hasSidebarSlot: Boolean(slots.sidebar),
|
|
1964
|
+
sidebarSlotCount: sidebarSlotNames.value.length
|
|
1965
|
+
}));
|
|
1966
|
+
const controls = computed(() => ({
|
|
1967
|
+
activeView: navigationState.resolvedActiveView.value,
|
|
1968
|
+
setActiveView: navigationState.setActiveView,
|
|
1969
|
+
values: controlsState.internalControlValues,
|
|
1970
|
+
componentBindings: controlsState.resolvedComponentBindings.value,
|
|
1971
|
+
componentBindingsById: controlsState.resolvedComponentBindingsById.value,
|
|
1972
|
+
componentProps: controlsState.resolvedComponentProps.value,
|
|
1973
|
+
componentPropsById: controlsState.resolvedComponentPropsById.value
|
|
1974
|
+
}));
|
|
1975
|
+
return {
|
|
1976
|
+
shell: computed(() => ({
|
|
1977
|
+
sidebarPosition: props.sidebarPosition ?? "left",
|
|
1978
|
+
sidebarWidth: props.sidebarWidth,
|
|
1979
|
+
floating: props.floating ?? true,
|
|
1980
|
+
responsiveSidebar: props.responsiveSidebar ?? true,
|
|
1981
|
+
sidebarToggleLabel: props.sidebarToggleLabel ?? "Open workspace sidebar",
|
|
1982
|
+
sidebarCloseLabel: props.sidebarCloseLabel ?? "Close workspace sidebar",
|
|
1983
|
+
hasSidebarSurface: hasSidebarSurface.value,
|
|
1984
|
+
title: props.title ?? "Workspace",
|
|
1985
|
+
subtitle: props.subtitle,
|
|
1986
|
+
topBarVariant: props.topBarVariant ?? "card",
|
|
1987
|
+
homePath: props.homePath ?? "/",
|
|
1988
|
+
showLogo: props.showLogo ?? true,
|
|
1989
|
+
pageSelector: props.pageSelector,
|
|
1990
|
+
currentPageSelectorId: navigationState.resolvedCurrentPageSelectorId.value,
|
|
1991
|
+
pillNav: navigationState.resolvedPillNav.value,
|
|
1992
|
+
currentPillId: navigationState.resolvedCurrentPillId.value,
|
|
1993
|
+
showThemeToggle: props.showThemeToggle ?? true,
|
|
1994
|
+
showSettings: props.showSettings ?? true,
|
|
1995
|
+
settingsConfig: props.settingsConfig,
|
|
1996
|
+
showStandaloneLabel: props.showStandaloneLabel ?? false,
|
|
1997
|
+
standaloneLabel: props.standaloneLabel ?? "Standalone",
|
|
1998
|
+
accountMenu: props.accountMenu,
|
|
1999
|
+
showNotifications: props.showNotifications ?? false,
|
|
2000
|
+
hasNotificationDot: props.hasNotificationDot ?? false,
|
|
2001
|
+
showAdmin: props.showAdmin ?? false,
|
|
2002
|
+
adminPath: props.adminPath ?? "/admin",
|
|
2003
|
+
userName: props.userName,
|
|
2004
|
+
userInitial: props.userInitial,
|
|
2005
|
+
userEmail: props.userEmail,
|
|
2006
|
+
topBarSlotNames: topBarSlotNames.value,
|
|
2007
|
+
controls: controls.value
|
|
2008
|
+
})),
|
|
2009
|
+
sidebar: computed(() => ({
|
|
2010
|
+
title: props.sidebarTitle,
|
|
2011
|
+
subtitle: props.sidebarSubtitle,
|
|
2012
|
+
badge: props.sidebarBadge,
|
|
2013
|
+
panels: props.panels ?? {},
|
|
2014
|
+
activeView: navigationState.resolvedActiveView.value,
|
|
2015
|
+
floating: false,
|
|
2016
|
+
dense: props.dense ?? true,
|
|
2017
|
+
width: props.sidebarWidth,
|
|
2018
|
+
side: props.sidebarPosition ?? "left",
|
|
2019
|
+
forms: props.forms ?? {},
|
|
2020
|
+
contentId: props.sidebarContentId,
|
|
2021
|
+
showWhenEmpty: (props.showSidebarWhenEmpty ?? false) || props.sidebarContentId !== void 0,
|
|
2022
|
+
model: props.model,
|
|
2023
|
+
controlSchema: props.controls,
|
|
2024
|
+
controlOptions: props.controlOptions ?? {},
|
|
2025
|
+
values: controlsState.internalControlValues,
|
|
2026
|
+
formEnhancements: props.formEnhancements,
|
|
2027
|
+
showFormActions: props.showFormActions ?? false,
|
|
2028
|
+
formLoading: props.formLoading ?? false,
|
|
2029
|
+
formDisabled: props.formDisabled ?? false,
|
|
2030
|
+
formReadonly: props.formReadonly ?? false,
|
|
2031
|
+
formSize: props.formSize ?? "sm",
|
|
2032
|
+
collapsible: props.sidebarCollapsible ?? true,
|
|
2033
|
+
collapsed: props.sidebarCollapsed,
|
|
2034
|
+
defaultCollapsed: props.defaultSidebarCollapsed ?? false,
|
|
2035
|
+
collapsedWidth: props.sidebarCollapsedWidth ?? "3rem",
|
|
2036
|
+
collapseButtonLabel: props.sidebarCollapseButtonLabel ?? "Collapse sidebar",
|
|
2037
|
+
expandButtonLabel: props.sidebarExpandButtonLabel ?? "Expand sidebar",
|
|
2038
|
+
slotNames: sidebarSlotNames.value
|
|
2039
|
+
})),
|
|
2040
|
+
controls,
|
|
2041
|
+
hasSidebarSurface,
|
|
2042
|
+
handleControlValuesUpdate: controlsState.handleControlValuesUpdate,
|
|
2043
|
+
handlePageSelectorSelect: navigationState.handlePageSelectorSelect,
|
|
2044
|
+
handlePillSelect: navigationState.handlePillSelect
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
//#endregion
|
|
2048
|
+
//#region src/composables/useRequestSyncState.ts
|
|
2049
|
+
/** Shared loading/error/timestamp state for generated plugin request helpers. */
|
|
2050
|
+
function useRequestSyncState(defaultErrorMessage = "Request failed.") {
|
|
2051
|
+
const loading = ref(false);
|
|
2052
|
+
const error = ref(null);
|
|
2053
|
+
const lastLoadedAt = ref(null);
|
|
2054
|
+
const lastSavedAt = ref(null);
|
|
2055
|
+
const lastRunAt = ref(null);
|
|
2056
|
+
function clearError() {
|
|
2057
|
+
error.value = null;
|
|
2058
|
+
}
|
|
2059
|
+
function readErrorMessage$1(value, fallback = defaultErrorMessage) {
|
|
2060
|
+
return readErrorMessage(value, fallback);
|
|
2061
|
+
}
|
|
2062
|
+
function setError(value, fallback) {
|
|
2063
|
+
const message = readErrorMessage$1(value, fallback);
|
|
2064
|
+
error.value = message;
|
|
2065
|
+
return message;
|
|
2066
|
+
}
|
|
2067
|
+
function markLoaded(date = /* @__PURE__ */ new Date()) {
|
|
2068
|
+
lastLoadedAt.value = date;
|
|
2069
|
+
}
|
|
2070
|
+
function markSaved(date = /* @__PURE__ */ new Date()) {
|
|
2071
|
+
lastSavedAt.value = date;
|
|
2072
|
+
}
|
|
2073
|
+
function markRun(date = /* @__PURE__ */ new Date()) {
|
|
2074
|
+
lastRunAt.value = date;
|
|
2075
|
+
}
|
|
2076
|
+
let runToken = 0;
|
|
2077
|
+
let activeController = null;
|
|
2078
|
+
function cancel() {
|
|
2079
|
+
runToken += 1;
|
|
2080
|
+
activeController?.abort();
|
|
2081
|
+
activeController = null;
|
|
2082
|
+
loading.value = false;
|
|
2083
|
+
}
|
|
2084
|
+
async function run(operation, options = {}) {
|
|
2085
|
+
if (options.abortPrevious !== false) activeController?.abort();
|
|
2086
|
+
const token = runToken += 1;
|
|
2087
|
+
const controller = new AbortController();
|
|
2088
|
+
activeController = controller;
|
|
2089
|
+
const callerSignal = options.signal;
|
|
2090
|
+
if (callerSignal) {
|
|
2091
|
+
if (callerSignal.aborted) controller.abort(callerSignal.reason);
|
|
2092
|
+
else callerSignal.addEventListener("abort", () => controller.abort(callerSignal.reason), { once: true });
|
|
2093
|
+
}
|
|
2094
|
+
const isCurrent = () => token === runToken;
|
|
2095
|
+
loading.value = true;
|
|
2096
|
+
clearError();
|
|
2097
|
+
try {
|
|
2098
|
+
const response = await operation(controller.signal);
|
|
2099
|
+
if (isCurrent()) {
|
|
2100
|
+
if (options.success === "load") markLoaded();
|
|
2101
|
+
else if (options.success === "save") markSaved();
|
|
2102
|
+
else if (options.success === "run") markRun();
|
|
2103
|
+
}
|
|
2104
|
+
return response;
|
|
2105
|
+
} catch (err) {
|
|
2106
|
+
if (isCurrent()) setError(err, options.errorMessage);
|
|
2107
|
+
throw err;
|
|
2108
|
+
} finally {
|
|
2109
|
+
if (isCurrent()) loading.value = false;
|
|
2110
|
+
if (activeController === controller) activeController = null;
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
return {
|
|
2114
|
+
loading,
|
|
2115
|
+
error,
|
|
2116
|
+
lastLoadedAt,
|
|
2117
|
+
lastSavedAt,
|
|
2118
|
+
lastRunAt,
|
|
2119
|
+
clearError,
|
|
2120
|
+
readErrorMessage: readErrorMessage$1,
|
|
2121
|
+
setError,
|
|
2122
|
+
markLoaded,
|
|
2123
|
+
markSaved,
|
|
2124
|
+
markRun,
|
|
2125
|
+
run,
|
|
2126
|
+
cancel
|
|
2127
|
+
};
|
|
2128
|
+
}
|
|
2129
|
+
//#endregion
|
|
2130
|
+
//#region src/composables/pluginEndpointBuilder.ts
|
|
2131
|
+
function normalizeApiBaseUrl(baseUrl) {
|
|
2132
|
+
if (!baseUrl) return void 0;
|
|
2133
|
+
return baseUrl.length > 1 ? baseUrl.replace(/\/+$/, "") : baseUrl;
|
|
2134
|
+
}
|
|
2135
|
+
function normalizeApiPrefix(prefix) {
|
|
2136
|
+
if (!prefix) return void 0;
|
|
2137
|
+
if (prefix.startsWith("/api/")) return normalizeApiBaseUrl(prefix);
|
|
2138
|
+
if (prefix === "/api") return prefix;
|
|
2139
|
+
if (prefix.startsWith("/")) return normalizeApiBaseUrl(`/api${prefix}`);
|
|
2140
|
+
return normalizeApiBaseUrl(`/api/${prefix}`);
|
|
2141
|
+
}
|
|
2142
|
+
function endpointParamName(param) {
|
|
2143
|
+
return typeof param === "string" ? param : param.name;
|
|
2144
|
+
}
|
|
2145
|
+
function endpointParamFieldName(param) {
|
|
2146
|
+
return typeof param === "string" ? param : param.fieldName ?? param.name;
|
|
2147
|
+
}
|
|
2148
|
+
function escapeRegExp(value) {
|
|
2149
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2150
|
+
}
|
|
2151
|
+
function replacePathParam(path, name, encoded) {
|
|
2152
|
+
const escapedName = escapeRegExp(name);
|
|
2153
|
+
return path.replace(new RegExp(`\\{${escapedName}(?::[^{}]+)?\\}`, "g"), () => encoded).replace(new RegExp(`:${escapedName}(?=/|$)`, "g"), () => encoded);
|
|
2154
|
+
}
|
|
2155
|
+
/** Resolve the runtime plugin API base URL from env, explicit options, platform injection, or contract metadata. */
|
|
2156
|
+
function resolvePluginBaseUrl(contract, explicitBaseUrl) {
|
|
2157
|
+
if (explicitBaseUrl) return normalizeApiBaseUrl(explicitBaseUrl) ?? explicitBaseUrl;
|
|
2158
|
+
const injected = getInjectedPlatformContext();
|
|
2159
|
+
const platformPrefix = normalizeApiBaseUrl(injected?.plugin?.api_prefix) || normalizeApiPrefix(injected?.plugin?.route_prefix);
|
|
2160
|
+
if (platformPrefix) return platformPrefix;
|
|
2161
|
+
return normalizeApiBaseUrl(contract.plugin.apiPrefix) || "/api";
|
|
2162
|
+
}
|
|
2163
|
+
function encodePath(path, payload, pathParams) {
|
|
2164
|
+
let nextPath = path;
|
|
2165
|
+
for (const param of pathParams) {
|
|
2166
|
+
const name = endpointParamName(param);
|
|
2167
|
+
const fieldName = endpointParamFieldName(param);
|
|
2168
|
+
const value = payload[fieldName] ?? payload[name];
|
|
2169
|
+
if (value === void 0 || value === null) throw new Error(`[MINT SDK] Missing path parameter '${fieldName}' for plugin endpoint ${path}`);
|
|
2170
|
+
const encoded = encodeURIComponent(String(value));
|
|
2171
|
+
nextPath = replacePathParam(nextPath, name, encoded);
|
|
2172
|
+
if (fieldName !== name) nextPath = replacePathParam(nextPath, fieldName, encoded);
|
|
2173
|
+
}
|
|
2174
|
+
return nextPath;
|
|
2175
|
+
}
|
|
2176
|
+
function withDefaultPathParams(payload, pathParams) {
|
|
2177
|
+
if (!pathParams.length) return payload;
|
|
2178
|
+
const nextPayload = { ...payload };
|
|
2179
|
+
for (const param of pathParams) {
|
|
2180
|
+
const name = endpointParamName(param);
|
|
2181
|
+
const fieldName = endpointParamFieldName(param);
|
|
2182
|
+
if (nextPayload[fieldName] !== void 0 && nextPayload[fieldName] !== null || nextPayload[name] !== void 0 && nextPayload[name] !== null) continue;
|
|
2183
|
+
if (isExperimentPathParam(fieldName) || isExperimentPathParam(name)) {
|
|
2184
|
+
const experimentId = resolveCurrentExperimentId();
|
|
2185
|
+
if (experimentId !== void 0) nextPayload[fieldName] = experimentId;
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
return nextPayload;
|
|
2189
|
+
}
|
|
2190
|
+
function isExperimentPathParam(param) {
|
|
2191
|
+
return param === "experimentId" || param === "experiment_id";
|
|
2192
|
+
}
|
|
2193
|
+
function withoutPathParams(payload, pathParams) {
|
|
2194
|
+
const copy = { ...payload };
|
|
2195
|
+
for (const param of pathParams) {
|
|
2196
|
+
delete copy[endpointParamFieldName(param)];
|
|
2197
|
+
delete copy[endpointParamName(param)];
|
|
2198
|
+
}
|
|
2199
|
+
return copy;
|
|
2200
|
+
}
|
|
2201
|
+
function queryParamsFromPayload(payload, pathParams, queryParams, endpointPath, includeUnspecifiedParams = false) {
|
|
2202
|
+
if (!queryParams?.length) return includeUnspecifiedParams ? withoutPathParams(payload, pathParams) : {};
|
|
2203
|
+
const query = {};
|
|
2204
|
+
for (const param of queryParams) {
|
|
2205
|
+
const name = typeof param === "string" ? param : param.name;
|
|
2206
|
+
const fieldName = typeof param === "string" ? param : param.fieldName ?? param.name;
|
|
2207
|
+
const required = typeof param !== "string" && param.required === true;
|
|
2208
|
+
const value = payload[fieldName];
|
|
2209
|
+
if (value === void 0 || value === null) {
|
|
2210
|
+
if (required) throw new Error(`[MINT SDK] Missing query parameter '${fieldName}' for plugin endpoint ${endpointPath ?? ""}`);
|
|
2211
|
+
continue;
|
|
2212
|
+
}
|
|
2213
|
+
query[name] = value;
|
|
2214
|
+
}
|
|
2215
|
+
return query;
|
|
2216
|
+
}
|
|
2217
|
+
function hasKeys(value) {
|
|
2218
|
+
return Object.keys(value).length > 0;
|
|
2219
|
+
}
|
|
2220
|
+
function isPlainPayloadObject(value) {
|
|
2221
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
2222
|
+
}
|
|
2223
|
+
function hasOwn(value, key) {
|
|
2224
|
+
return Object.prototype.hasOwnProperty.call(value, key);
|
|
2225
|
+
}
|
|
2226
|
+
function normalizeStructuredEndpointPayload(payload) {
|
|
2227
|
+
const pathParamPayload = payload.pathParams;
|
|
2228
|
+
const queryPayload = payload.query;
|
|
2229
|
+
const hasStructuredPathParams = hasOwn(payload, "pathParams") && isPlainPayloadObject(pathParamPayload);
|
|
2230
|
+
const hasStructuredQuery = hasOwn(payload, "query") && isPlainPayloadObject(queryPayload);
|
|
2231
|
+
if (!hasStructuredPathParams && !hasStructuredQuery) return payload;
|
|
2232
|
+
const nextPayload = { ...payload };
|
|
2233
|
+
delete nextPayload.pathParams;
|
|
2234
|
+
delete nextPayload.query;
|
|
2235
|
+
if (hasStructuredPathParams) Object.assign(nextPayload, pathParamPayload);
|
|
2236
|
+
if (hasStructuredQuery) Object.assign(nextPayload, queryPayload);
|
|
2237
|
+
return nextPayload;
|
|
2238
|
+
}
|
|
2239
|
+
function requestBodyFromPayload(payload, requestPayload, endpoint) {
|
|
2240
|
+
if (!endpoint.hasBody) return void 0;
|
|
2241
|
+
const body = endpoint.pathParams?.length || endpoint.queryParams?.length ? requestPayload.body : payload;
|
|
2242
|
+
if (body === void 0 || body === null) throw new Error(`[MINT SDK] Missing request body for plugin endpoint ${endpoint.path}`);
|
|
2243
|
+
return body;
|
|
2244
|
+
}
|
|
2245
|
+
function pluginEndpointRequestParts(contract, endpoint, payload, explicitBaseUrl) {
|
|
2246
|
+
const payloadObject = isPlainPayloadObject(payload) ? normalizeStructuredEndpointPayload(payload) : {};
|
|
2247
|
+
const pathParams = endpoint.pathParams ?? [];
|
|
2248
|
+
const requestPayload = withDefaultPathParams(payloadObject, pathParams);
|
|
2249
|
+
const path = encodePath(endpoint.path, requestPayload, pathParams);
|
|
2250
|
+
const query = queryParamsFromPayload(requestPayload, pathParams, endpoint.queryParams, endpoint.path, endpoint.method === "get");
|
|
2251
|
+
return {
|
|
2252
|
+
baseUrl: resolvePluginBaseUrl(contract, explicitBaseUrl),
|
|
2253
|
+
path,
|
|
2254
|
+
query,
|
|
2255
|
+
requestPayload
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
function appendQueryValue(params, key, value) {
|
|
2259
|
+
if (value === void 0 || value === null) return;
|
|
2260
|
+
if (Array.isArray(value)) {
|
|
2261
|
+
for (const item of value) appendQueryValue(params, key, item);
|
|
2262
|
+
return;
|
|
2263
|
+
}
|
|
2264
|
+
if (typeof value === "object") {
|
|
2265
|
+
params.append(key, JSON.stringify(value));
|
|
2266
|
+
return;
|
|
2267
|
+
}
|
|
2268
|
+
params.append(key, String(value));
|
|
2269
|
+
}
|
|
2270
|
+
function queryString(query) {
|
|
2271
|
+
const params = new URLSearchParams();
|
|
2272
|
+
for (const [key, value] of Object.entries(query)) appendQueryValue(params, key, value);
|
|
2273
|
+
const serialized = params.toString();
|
|
2274
|
+
return serialized ? `?${serialized}` : "";
|
|
2275
|
+
}
|
|
2276
|
+
function joinBaseUrl(baseUrl, path) {
|
|
2277
|
+
if (!baseUrl) return path;
|
|
2278
|
+
return `${baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
|
|
2279
|
+
}
|
|
2280
|
+
/** Build the concrete URL for a generated plugin endpoint without making a request. */
|
|
2281
|
+
function buildPluginEndpointUrl(contract, endpoint, payload, options = {}) {
|
|
2282
|
+
const parts = pluginEndpointRequestParts(contract, endpoint, payload, options.baseUrl);
|
|
2283
|
+
const pathWithQuery = `${parts.path}${queryString(parts.query)}`;
|
|
2284
|
+
if (options.includeBaseUrl === false) return pathWithQuery;
|
|
2285
|
+
return joinBaseUrl(parts.baseUrl, pathWithQuery);
|
|
2286
|
+
}
|
|
2287
|
+
//#endregion
|
|
2288
|
+
//#region src/composables/usePluginClient.ts
|
|
2289
|
+
function queryConfig(query) {
|
|
2290
|
+
return hasKeys(query) ? { params: query } : void 0;
|
|
2291
|
+
}
|
|
2292
|
+
var OCTET_STREAM = "application/octet-stream";
|
|
2293
|
+
var MULTIPART_FORM_DATA = "multipart/form-data";
|
|
2294
|
+
function endpointRequestConfig(endpoint, query, options) {
|
|
2295
|
+
const config = {};
|
|
2296
|
+
if (hasKeys(query)) config.params = query;
|
|
2297
|
+
if (endpoint.requestContentType === OCTET_STREAM) config.headers = { "Content-Type": OCTET_STREAM };
|
|
2298
|
+
if (endpoint.requestContentType === MULTIPART_FORM_DATA) config.headers = { "Content-Type": void 0 };
|
|
2299
|
+
if (endpoint.responseContentType === OCTET_STREAM) config.responseType = "blob";
|
|
2300
|
+
if (options?.signal) config.signal = options.signal;
|
|
2301
|
+
return hasKeys(config) ? config : void 0;
|
|
2302
|
+
}
|
|
2303
|
+
function isFormData(value) {
|
|
2304
|
+
return typeof FormData !== "undefined" && value instanceof FormData;
|
|
2305
|
+
}
|
|
2306
|
+
function endpointRequestBody(endpoint, body) {
|
|
2307
|
+
return endpoint.requestContentType === MULTIPART_FORM_DATA ? pluginFormDataFromPayload(body) : body;
|
|
2308
|
+
}
|
|
2309
|
+
function endpointHasPayload(endpoint) {
|
|
2310
|
+
return Boolean(endpoint.hasBody || endpoint.pathParams?.length || endpoint.queryParams?.length);
|
|
2311
|
+
}
|
|
2312
|
+
function isBlob(value) {
|
|
2313
|
+
return typeof Blob !== "undefined" && value instanceof Blob;
|
|
2314
|
+
}
|
|
2315
|
+
function appendFormDataValue(formData, key, value) {
|
|
2316
|
+
if (value === void 0 || value === null) return;
|
|
2317
|
+
if (Array.isArray(value)) {
|
|
2318
|
+
for (const item of value) appendFormDataValue(formData, key, item);
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
if (isBlob(value)) {
|
|
2322
|
+
formData.append(key, value);
|
|
2323
|
+
return;
|
|
2324
|
+
}
|
|
2325
|
+
if (typeof value === "object") {
|
|
2326
|
+
formData.append(key, JSON.stringify(value));
|
|
2327
|
+
return;
|
|
2328
|
+
}
|
|
2329
|
+
formData.append(key, String(value));
|
|
2330
|
+
}
|
|
2331
|
+
/** Convert plain upload payloads into FormData using the same encoding across generated plugin clients. */
|
|
2332
|
+
function pluginFormDataFromPayload(payload) {
|
|
2333
|
+
if (isFormData(payload)) return payload;
|
|
2334
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) throw new Error("[MINT SDK] Plugin upload endpoints require a FormData or plain object payload.");
|
|
2335
|
+
const formData = new FormData();
|
|
2336
|
+
for (const [key, value] of Object.entries(payload)) appendFormDataValue(formData, key, value);
|
|
2337
|
+
return formData;
|
|
2338
|
+
}
|
|
2339
|
+
/** Trigger a browser download for an already-created Blob. */
|
|
2340
|
+
function downloadBlob(blob, filename, options = {}) {
|
|
2341
|
+
if (typeof document === "undefined" || typeof URL === "undefined") throw new Error("[MINT SDK] Cannot trigger a file download outside a browser.");
|
|
2342
|
+
const blobUrl = URL.createObjectURL(blob);
|
|
2343
|
+
const link = document.createElement("a");
|
|
2344
|
+
link.href = blobUrl;
|
|
2345
|
+
link.download = filename;
|
|
2346
|
+
document.body.appendChild(link);
|
|
2347
|
+
link.click();
|
|
2348
|
+
document.body.removeChild(link);
|
|
2349
|
+
setTimeout(() => URL.revokeObjectURL(blobUrl), options.revokeObjectUrlDelayMs ?? 100);
|
|
2350
|
+
}
|
|
2351
|
+
function normalizePluginNavPath(path) {
|
|
2352
|
+
const raw = path.trim() || "/";
|
|
2353
|
+
return (raw.startsWith("/") ? raw : `/${raw}`).replace(/\/+$/, "") || "/";
|
|
2354
|
+
}
|
|
2355
|
+
function pluginPageIdFromPath(path, fallbackIndex) {
|
|
2356
|
+
const normalizedPath = normalizePluginNavPath(path);
|
|
2357
|
+
if (normalizedPath === "/") return "dashboard";
|
|
2358
|
+
return normalizedPath.replace(/^\/+/, "").replace(/\/+/g, "-") || `page-${fallbackIndex + 1}`;
|
|
2359
|
+
}
|
|
2360
|
+
/** Convert resolved @mint_plugin nav_items into page selectors for the topbar and homepage PluginCards. */
|
|
2361
|
+
function getPluginPageSelectorItems(contract) {
|
|
2362
|
+
const pluginIcon = contract.plugin.icon ?? "";
|
|
2363
|
+
return (contract.plugin.navItems ?? []).map((item, index) => ({
|
|
2364
|
+
id: item.id || pluginPageIdFromPath(item.path, index),
|
|
2365
|
+
label: item.label,
|
|
2366
|
+
to: normalizePluginNavPath(item.path),
|
|
2367
|
+
icon: item.icon || pluginIcon || void 0
|
|
2368
|
+
}));
|
|
2369
|
+
}
|
|
2370
|
+
/** Create a typed plugin API client from a generated MINT plugin contract. */
|
|
2371
|
+
function createPluginClient(contract, options) {
|
|
2372
|
+
const client = {};
|
|
2373
|
+
for (const [name, endpoint] of Object.entries(options.endpoints)) client[name] = async (payloadOrOptions, explicitOptions) => {
|
|
2374
|
+
const hasPayload = endpointHasPayload(endpoint);
|
|
2375
|
+
const payload = hasPayload ? payloadOrOptions : void 0;
|
|
2376
|
+
const callOptions = hasPayload ? explicitOptions : payloadOrOptions ?? explicitOptions;
|
|
2377
|
+
const parts = pluginEndpointRequestParts(contract, endpoint, payload, options.baseUrl);
|
|
2378
|
+
const api = useApi({
|
|
2379
|
+
baseUrl: parts.baseUrl,
|
|
2380
|
+
withAuth: endpoint.requiresAuth !== false,
|
|
2381
|
+
typedErrors: true
|
|
2382
|
+
});
|
|
2383
|
+
const config = endpointRequestConfig(endpoint, parts.query, callOptions);
|
|
2384
|
+
if (endpoint.method === "get") return api.get(parts.path, config);
|
|
2385
|
+
if (endpoint.method === "delete") {
|
|
2386
|
+
const rawBody = requestBodyFromPayload(payload, parts.requestPayload, endpoint);
|
|
2387
|
+
const body = rawBody === void 0 ? void 0 : endpointRequestBody(endpoint, rawBody);
|
|
2388
|
+
const config = body === void 0 ? endpointRequestConfig(endpoint, parts.query, callOptions) : {
|
|
2389
|
+
...endpointRequestConfig(endpoint, parts.query, callOptions),
|
|
2390
|
+
data: body
|
|
2391
|
+
};
|
|
2392
|
+
return api.delete(parts.path, config);
|
|
2393
|
+
}
|
|
2394
|
+
const body = endpointRequestBody(endpoint, requestBodyFromPayload(payload, parts.requestPayload, endpoint));
|
|
2395
|
+
if (endpoint.method === "post") return api.post(parts.path, body, config);
|
|
2396
|
+
if (endpoint.method === "put") return api.put(parts.path, body, config);
|
|
2397
|
+
if (endpoint.method === "patch") return api.patch(parts.path, body, config);
|
|
2398
|
+
throw new Error(`[MINT SDK] Unsupported plugin endpoint method: ${endpoint.method}`);
|
|
2399
|
+
};
|
|
2400
|
+
return client;
|
|
2401
|
+
}
|
|
2402
|
+
/** Send multipart or raw binary data to a generated endpoint with contract URL, cancellation, and auth handling. */
|
|
2403
|
+
async function uploadPluginEndpoint(contract, endpoint, payload, options = {}) {
|
|
2404
|
+
if (![
|
|
2405
|
+
"post",
|
|
2406
|
+
"put",
|
|
2407
|
+
"patch"
|
|
2408
|
+
].includes(endpoint.method)) throw new Error(`[MINT SDK] Plugin upload endpoints must use POST, PUT, or PATCH; got ${endpoint.method}`);
|
|
2409
|
+
const parts = pluginEndpointRequestParts(contract, endpoint, payload, options.baseUrl);
|
|
2410
|
+
const api = useApi({
|
|
2411
|
+
baseUrl: parts.baseUrl,
|
|
2412
|
+
withAuth: endpoint.requiresAuth !== false,
|
|
2413
|
+
typedErrors: true
|
|
2414
|
+
});
|
|
2415
|
+
const body = endpoint.hasBody ? requestBodyFromPayload(payload, parts.requestPayload, endpoint) : payload;
|
|
2416
|
+
if (endpoint.requestContentType === OCTET_STREAM) {
|
|
2417
|
+
const config = endpointRequestConfig(endpoint, parts.query, { signal: options.signal });
|
|
2418
|
+
if (endpoint.method === "post") return api.post(parts.path, body, config);
|
|
2419
|
+
if (endpoint.method === "put") return api.put(parts.path, body, config);
|
|
2420
|
+
return api.patch(parts.path, body, config);
|
|
2421
|
+
}
|
|
2422
|
+
const formData = pluginFormDataFromPayload(body);
|
|
2423
|
+
const config = {
|
|
2424
|
+
...queryConfig(parts.query) ?? {},
|
|
2425
|
+
...options.signal ? { signal: options.signal } : {},
|
|
2426
|
+
headers: { "Content-Type": void 0 }
|
|
2427
|
+
};
|
|
2428
|
+
if (endpoint.method === "post") return api.post(parts.path, formData, config);
|
|
2429
|
+
if (endpoint.method === "put") return api.put(parts.path, formData, config);
|
|
2430
|
+
return api.patch(parts.path, formData, config);
|
|
2431
|
+
}
|
|
2432
|
+
/** Fetch a generated plugin endpoint as a Blob and optionally trigger a browser download. */
|
|
2433
|
+
async function downloadPluginEndpoint(contract, endpoint, payload, options = {}) {
|
|
2434
|
+
if (endpoint.requestContentType === MULTIPART_FORM_DATA) throw new Error("[MINT SDK] Plugin download endpoints do not support multipart request bodies");
|
|
2435
|
+
const parts = pluginEndpointRequestParts(contract, endpoint, payload, options.baseUrl);
|
|
2436
|
+
const api = useApi({
|
|
2437
|
+
baseUrl: parts.baseUrl,
|
|
2438
|
+
withAuth: endpoint.requiresAuth !== false,
|
|
2439
|
+
typedErrors: true
|
|
2440
|
+
});
|
|
2441
|
+
const config = {
|
|
2442
|
+
...endpointRequestConfig(endpoint, parts.query, { signal: options.signal }) ?? {},
|
|
2443
|
+
responseType: "blob"
|
|
2444
|
+
};
|
|
2445
|
+
const rawBody = requestBodyFromPayload(payload, parts.requestPayload, endpoint);
|
|
2446
|
+
const body = rawBody === void 0 ? void 0 : endpointRequestBody(endpoint, rawBody);
|
|
2447
|
+
let blob;
|
|
2448
|
+
if (endpoint.method === "get") blob = await api.get(parts.path, config);
|
|
2449
|
+
else if (endpoint.method === "post") blob = await api.post(parts.path, body, config);
|
|
2450
|
+
else if (endpoint.method === "put") blob = await api.put(parts.path, body, config);
|
|
2451
|
+
else if (endpoint.method === "patch") blob = await api.patch(parts.path, body, config);
|
|
2452
|
+
else if (endpoint.method === "delete") blob = await api.delete(parts.path, body === void 0 ? config : {
|
|
2453
|
+
...config,
|
|
2454
|
+
data: body
|
|
2455
|
+
});
|
|
2456
|
+
else throw new Error(`[MINT SDK] Unsupported plugin endpoint method: ${endpoint.method}`);
|
|
2457
|
+
if (options.filename) downloadBlob(blob, options.filename, { revokeObjectUrlDelayMs: options.revokeObjectUrlDelayMs });
|
|
2458
|
+
return blob;
|
|
2459
|
+
}
|
|
2460
|
+
function normalizePluginSettingsOptions(options = {}) {
|
|
2461
|
+
return typeof options === "string" ? { pluginName: options } : options;
|
|
2462
|
+
}
|
|
2463
|
+
/** @deprecated Removed in MINT 1.4; use `useGeneratedPluginClient()` for generated clients. */
|
|
2464
|
+
function usePluginClient(client) {
|
|
2465
|
+
return client;
|
|
2466
|
+
}
|
|
2467
|
+
var PLATFORM_OWNED_SETTINGS_KEY = "allowed_experiment_types";
|
|
2468
|
+
/**
|
|
2469
|
+
* Secret settings travel as `{ "$secret": "<id>" }` references, so an untouched
|
|
2470
|
+
* secret round-trips without ever exposing its value. Declaring the format on
|
|
2471
|
+
* every write keeps a `null` unambiguous: it clears the stored secret instead of
|
|
2472
|
+
* meaning "unchanged", which is what the deprecated placeholder form meant.
|
|
2473
|
+
*/
|
|
2474
|
+
function withSecretFormat(payload, secretFormat) {
|
|
2475
|
+
if (!secretFormat) return payload;
|
|
2476
|
+
return {
|
|
2477
|
+
...payload,
|
|
2478
|
+
[PLUGIN_SECRET_FORMAT_KEY]: secretFormat
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
function responseSecretFormat(response) {
|
|
2482
|
+
if (!response || typeof response !== "object") return null;
|
|
2483
|
+
const format = response[PLUGIN_SECRET_FORMAT_KEY];
|
|
2484
|
+
return typeof format === "string" && format ? format : null;
|
|
2485
|
+
}
|
|
2486
|
+
function cloneRecord(value) {
|
|
2487
|
+
return JSON.parse(JSON.stringify(value));
|
|
2488
|
+
}
|
|
2489
|
+
function hasOwnSetting(settings, key) {
|
|
2490
|
+
return Object.prototype.hasOwnProperty.call(settings, key);
|
|
2491
|
+
}
|
|
2492
|
+
function responseSettingsPayload(response) {
|
|
2493
|
+
if (!response || typeof response !== "object") return {};
|
|
2494
|
+
const data = response;
|
|
2495
|
+
if (data.config && typeof data.config === "object") return cloneRecord(data.config);
|
|
2496
|
+
if (data.settings && typeof data.settings === "object") return cloneRecord(data.settings);
|
|
2497
|
+
return {};
|
|
2498
|
+
}
|
|
2499
|
+
function responseSettingsRevision(response) {
|
|
2500
|
+
if (!response || typeof response !== "object") return null;
|
|
2501
|
+
const revision = response.revision;
|
|
2502
|
+
return typeof revision === "string" && revision ? revision : null;
|
|
2503
|
+
}
|
|
2504
|
+
function topLevelSettingsPatch(current, keys) {
|
|
2505
|
+
return Object.fromEntries([...keys].map((key) => [key, hasOwnSetting(current, key) ? current[key] : null]));
|
|
2506
|
+
}
|
|
2507
|
+
function changedTopLevelSettingKeys(current, saved) {
|
|
2508
|
+
return new Set([.../* @__PURE__ */ new Set([...Object.keys(current), ...Object.keys(saved)])].filter((key) => JSON.stringify(current[key]) !== JSON.stringify(saved[key])));
|
|
2509
|
+
}
|
|
2510
|
+
function platformOwnedSettingsPatch(settings) {
|
|
2511
|
+
return { [PLATFORM_OWNED_SETTINGS_KEY]: hasOwnSetting(settings, PLATFORM_OWNED_SETTINGS_KEY) ? settings[PLATFORM_OWNED_SETTINGS_KEY] : null };
|
|
2512
|
+
}
|
|
2513
|
+
/** Load, edit, and persist plugin settings from platform config or the plugin-local settings router. */
|
|
2514
|
+
function usePluginSettings(options = {}) {
|
|
2515
|
+
const resolvedOptions = normalizePluginSettingsOptions(options);
|
|
2516
|
+
const injectedContext = getInjectedPlatformContext();
|
|
2517
|
+
const { plugin, isIntegrated } = usePlatformContext();
|
|
2518
|
+
const platformApi = useApi({ baseUrl: resolvedOptions.platformApiBaseUrl ?? injectedContext?.platformApiUrl });
|
|
2519
|
+
const pluginApi = useApi({ baseUrl: resolvedOptions.apiBaseUrl ?? injectedContext?.plugin?.api_prefix });
|
|
2520
|
+
const values = ref({});
|
|
2521
|
+
const savedValues = shallowRef({});
|
|
2522
|
+
const revision = ref(null);
|
|
2523
|
+
const secretFormat = ref(null);
|
|
2524
|
+
const request = useRequestSyncState("Plugin settings request failed.");
|
|
2525
|
+
const isLoading = ref(false);
|
|
2526
|
+
const isSaving = ref(false);
|
|
2527
|
+
const resolvedPluginName = computed(() => resolvedOptions.pluginName ?? plugin.value?.name ?? injectedContext?.plugin?.name ?? "");
|
|
2528
|
+
const usesPluginSettingsApi = computed(() => {
|
|
2529
|
+
const currentPlugin = injectedContext?.plugin ?? plugin.value;
|
|
2530
|
+
return currentPlugin?.settings_api === "plugin" && resolvedPluginName.value === currentPlugin.name;
|
|
2531
|
+
});
|
|
2532
|
+
const usesPlatformConfig = computed(() => Boolean((isIntegrated.value || injectedContext?.isIntegrated) && resolvedPluginName.value && !usesPluginSettingsApi.value));
|
|
2533
|
+
const settings = computed(() => values.value);
|
|
2534
|
+
const isDirty = computed(() => JSON.stringify(values.value) !== JSON.stringify(savedValues.value));
|
|
2535
|
+
const settingsConfig = computed(() => ({
|
|
2536
|
+
title: resolvedOptions.title,
|
|
2537
|
+
schema: resolvedOptions.schema ?? void 0,
|
|
2538
|
+
showAppearance: resolvedOptions.showAppearance,
|
|
2539
|
+
values: values.value
|
|
2540
|
+
}));
|
|
2541
|
+
function setValues(nextValues) {
|
|
2542
|
+
values.value = cloneRecord(nextValues);
|
|
2543
|
+
}
|
|
2544
|
+
function applyLoaded(nextValues, nextRevision, nextSecretFormat = secretFormat.value) {
|
|
2545
|
+
const cloned = cloneRecord(nextValues);
|
|
2546
|
+
values.value = cloned;
|
|
2547
|
+
savedValues.value = cloneRecord(cloned);
|
|
2548
|
+
revision.value = nextRevision;
|
|
2549
|
+
secretFormat.value = nextSecretFormat;
|
|
2550
|
+
}
|
|
2551
|
+
async function load() {
|
|
2552
|
+
isLoading.value = true;
|
|
2553
|
+
try {
|
|
2554
|
+
const response = usesPlatformConfig.value ? await request.run(() => platformApi.get(`/plugins/${encodeURIComponent(resolvedPluginName.value)}/config`), {
|
|
2555
|
+
success: "load",
|
|
2556
|
+
errorMessage: "Failed to load plugin settings"
|
|
2557
|
+
}) : await request.run(() => pluginApi.get("/settings"), {
|
|
2558
|
+
success: "load",
|
|
2559
|
+
errorMessage: "Failed to load plugin settings"
|
|
2560
|
+
});
|
|
2561
|
+
applyLoaded(responseSettingsPayload(response), responseSettingsRevision(response), responseSecretFormat(response));
|
|
2562
|
+
} catch {} finally {
|
|
2563
|
+
isLoading.value = false;
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
async function save(nextValues) {
|
|
2567
|
+
if (nextValues) setValues(nextValues);
|
|
2568
|
+
isSaving.value = true;
|
|
2569
|
+
try {
|
|
2570
|
+
const payload = cloneRecord(values.value);
|
|
2571
|
+
let response;
|
|
2572
|
+
if (usesPlatformConfig.value) {
|
|
2573
|
+
const dirtyKeys = changedTopLevelSettingKeys(payload, savedValues.value);
|
|
2574
|
+
const platformOwnedChanged = dirtyKeys.has(PLATFORM_OWNED_SETTINGS_KEY);
|
|
2575
|
+
const typedKeys = [...dirtyKeys].filter((key) => key !== PLATFORM_OWNED_SETTINGS_KEY);
|
|
2576
|
+
let nextRevision = revision.value;
|
|
2577
|
+
if (typedKeys.length) {
|
|
2578
|
+
response = await request.run(() => platformApi.patch(`/plugins/${encodeURIComponent(resolvedPluginName.value)}/config`, { config: withSecretFormat(topLevelSettingsPatch(payload, typedKeys), secretFormat.value) }), {
|
|
2579
|
+
success: "save",
|
|
2580
|
+
errorMessage: "Failed to save plugin settings"
|
|
2581
|
+
});
|
|
2582
|
+
nextRevision = responseSettingsRevision(response) ?? nextRevision;
|
|
2583
|
+
}
|
|
2584
|
+
if (platformOwnedChanged) {
|
|
2585
|
+
response = await request.run(() => platformApi.patch(`/plugins/${encodeURIComponent(resolvedPluginName.value)}/config`, { config: platformOwnedSettingsPatch(payload) }), {
|
|
2586
|
+
success: "save",
|
|
2587
|
+
errorMessage: "Failed to save plugin settings"
|
|
2588
|
+
});
|
|
2589
|
+
nextRevision = responseSettingsRevision(response) ?? nextRevision;
|
|
2590
|
+
}
|
|
2591
|
+
if (!response) return true;
|
|
2592
|
+
applyLoaded(responseSettingsPayload(response), nextRevision, responseSecretFormat(response) ?? secretFormat.value);
|
|
2593
|
+
return true;
|
|
2594
|
+
} else {
|
|
2595
|
+
const ifMatch = revision.value ? { headers: { "If-Match": `"${revision.value}"` } } : void 0;
|
|
2596
|
+
response = await request.run(() => pluginApi.put("/settings", withSecretFormat(payload, secretFormat.value), ifMatch), {
|
|
2597
|
+
success: "save",
|
|
2598
|
+
errorMessage: "Failed to save plugin settings"
|
|
2599
|
+
});
|
|
2600
|
+
}
|
|
2601
|
+
applyLoaded(responseSettingsPayload(response), responseSettingsRevision(response) ?? revision.value, responseSecretFormat(response) ?? secretFormat.value);
|
|
2602
|
+
return true;
|
|
2603
|
+
} catch {
|
|
2604
|
+
return false;
|
|
2605
|
+
} finally {
|
|
2606
|
+
isSaving.value = false;
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
function reset() {
|
|
2610
|
+
values.value = cloneRecord(savedValues.value);
|
|
2611
|
+
request.clearError();
|
|
2612
|
+
}
|
|
2613
|
+
onMounted(() => {
|
|
2614
|
+
if (resolvedOptions.loadOnMount !== false) load();
|
|
2615
|
+
});
|
|
2616
|
+
return {
|
|
2617
|
+
settings,
|
|
2618
|
+
values,
|
|
2619
|
+
config: values,
|
|
2620
|
+
settingsConfig,
|
|
2621
|
+
isLoading,
|
|
2622
|
+
isSaving,
|
|
2623
|
+
error: request.error,
|
|
2624
|
+
lastLoadedAt: request.lastLoadedAt,
|
|
2625
|
+
lastSavedAt: request.lastSavedAt,
|
|
2626
|
+
isDirty,
|
|
2627
|
+
setValues,
|
|
2628
|
+
load,
|
|
2629
|
+
save,
|
|
2630
|
+
reset
|
|
2631
|
+
};
|
|
2632
|
+
}
|
|
2633
|
+
function streamRequestAuth(requestHeaders, policy) {
|
|
2634
|
+
const headers = new Headers(requestHeaders);
|
|
2635
|
+
const authStore = useAuthStore();
|
|
2636
|
+
if (!authStore.isInitialized) authStore.initialize();
|
|
2637
|
+
if (policy.requiresAuth && authStore.token && !headers.has("Authorization")) headers.set("Authorization", `Bearer ${authStore.token}`);
|
|
2638
|
+
const token = policy.requiresAuth ? authStore.token : void 0;
|
|
2639
|
+
return {
|
|
2640
|
+
headers,
|
|
2641
|
+
token: token && headers.get("Authorization") === `Bearer ${token}` ? token : void 0
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
function pluginEventStreamTransport(endpoint) {
|
|
2645
|
+
return async (url, init = {}) => {
|
|
2646
|
+
if (typeof fetch !== "function") throw new Error("[MINT SDK] fetch is required for plugin event streams.");
|
|
2647
|
+
const requestAuth = streamRequestAuth(init.headers, { requiresAuth: endpoint.requiresAuth !== false });
|
|
2648
|
+
const response = await fetch(url, {
|
|
2649
|
+
...init,
|
|
2650
|
+
headers: requestAuth.headers
|
|
2651
|
+
});
|
|
2652
|
+
if (response.status === 401) throw authenticationRequiredError(response, requestAuth.token);
|
|
2653
|
+
return response;
|
|
2654
|
+
};
|
|
2655
|
+
}
|
|
2656
|
+
function looksLikeEventStreamOptions(value) {
|
|
2657
|
+
if (!value || typeof value !== "object") return false;
|
|
2658
|
+
const candidate = value;
|
|
2659
|
+
return [
|
|
2660
|
+
"baseUrl",
|
|
2661
|
+
"immediate",
|
|
2662
|
+
"reconnect",
|
|
2663
|
+
"reconnectDelayMs",
|
|
2664
|
+
"parseJson",
|
|
2665
|
+
"credentials",
|
|
2666
|
+
"headers",
|
|
2667
|
+
"onOpen",
|
|
2668
|
+
"onMessage",
|
|
2669
|
+
"onError"
|
|
2670
|
+
].some((key) => key in candidate);
|
|
2671
|
+
}
|
|
2672
|
+
function usePluginEventStream(contract, endpoint, payloadOrOptions, maybeOptions) {
|
|
2673
|
+
if (endpoint.method !== "get") throw new Error(`[MINT SDK] Plugin event streams must use GET; got ${endpoint.method}`);
|
|
2674
|
+
const payload = maybeOptions === void 0 && looksLikeEventStreamOptions(payloadOrOptions) ? void 0 : payloadOrOptions;
|
|
2675
|
+
const options = maybeOptions === void 0 && looksLikeEventStreamOptions(payloadOrOptions) ? payloadOrOptions : maybeOptions ?? {};
|
|
2676
|
+
const responseType = endpoint.responseType?.trim();
|
|
2677
|
+
const parseJson = options.parseJson ?? Boolean(responseType && responseType !== "string");
|
|
2678
|
+
return useEventStream(() => buildPluginEndpointUrl(contract, endpoint, payload, { baseUrl: options.baseUrl }), {
|
|
2679
|
+
transport: pluginEventStreamTransport(endpoint),
|
|
2680
|
+
immediate: options.immediate,
|
|
2681
|
+
reconnect: options.reconnect,
|
|
2682
|
+
reconnectDelayMs: options.reconnectDelayMs,
|
|
2683
|
+
parseJson,
|
|
2684
|
+
credentials: options.credentials,
|
|
2685
|
+
headers: options.headers,
|
|
2686
|
+
shouldReconnect: (cause) => !(cause instanceof AuthenticationRequiredError),
|
|
2687
|
+
onOpen: options.onOpen,
|
|
2688
|
+
onMessage: options.onMessage,
|
|
2689
|
+
onError: options.onError,
|
|
2690
|
+
errorLabel: "Plugin event stream"
|
|
2691
|
+
});
|
|
2692
|
+
}
|
|
2693
|
+
/** Read and optionally load the current platform experiment for integrated plugin views. */
|
|
2694
|
+
function useCurrentExperiment(options = {}) {
|
|
2695
|
+
const api = useApi({ baseUrl: options.apiBaseUrl ?? getInjectedPlatformContext()?.platformApiUrl });
|
|
2696
|
+
const experiment = shallowRef(currentExperimentFromContext());
|
|
2697
|
+
const request = useRequestSyncState("Failed to load current experiment");
|
|
2698
|
+
const isLoading = request.loading;
|
|
2699
|
+
const error = request.error;
|
|
2700
|
+
const lastLoadedAt = request.lastLoadedAt;
|
|
2701
|
+
const experimentId = computed(() => {
|
|
2702
|
+
return resolveCurrentExperimentId();
|
|
851
2703
|
});
|
|
852
|
-
const
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
2704
|
+
const hasExperiment = computed(() => experimentId.value !== void 0);
|
|
2705
|
+
function requireExperimentId() {
|
|
2706
|
+
const id = experimentId.value;
|
|
2707
|
+
if (id === void 0) throw new Error("[MINT SDK] No current experiment is selected.");
|
|
2708
|
+
return id;
|
|
2709
|
+
}
|
|
2710
|
+
async function fetchExperiment(id = experimentId.value) {
|
|
2711
|
+
if (id === void 0) {
|
|
2712
|
+
experiment.value = currentExperimentFromContext();
|
|
2713
|
+
return experiment.value;
|
|
2714
|
+
}
|
|
2715
|
+
try {
|
|
2716
|
+
const result = await request.run(() => api.get(`/experiments/${id}`), {
|
|
2717
|
+
success: "load",
|
|
2718
|
+
errorMessage: "Failed to load current experiment"
|
|
2719
|
+
});
|
|
2720
|
+
experiment.value = result;
|
|
2721
|
+
return result;
|
|
2722
|
+
} catch (e) {
|
|
2723
|
+
experiment.value = void 0;
|
|
2724
|
+
return;
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
async function refresh() {
|
|
2728
|
+
return fetchExperiment();
|
|
2729
|
+
}
|
|
2730
|
+
watch(experimentId, (id) => {
|
|
2731
|
+
if (options.immediate === false) return;
|
|
2732
|
+
if (id === void 0) {
|
|
2733
|
+
experiment.value = currentExperimentFromContext();
|
|
2734
|
+
return;
|
|
2735
|
+
}
|
|
2736
|
+
fetchExperiment(id);
|
|
2737
|
+
}, { immediate: options.immediate !== false });
|
|
877
2738
|
return {
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
subtitle: props.subtitle,
|
|
888
|
-
topBarVariant: props.topBarVariant ?? "card",
|
|
889
|
-
homePath: props.homePath ?? "/",
|
|
890
|
-
showLogo: props.showLogo ?? true,
|
|
891
|
-
pageSelector: props.pageSelector,
|
|
892
|
-
currentPageSelectorId: navigationState.resolvedCurrentPageSelectorId.value,
|
|
893
|
-
pluginSwitcher: props.pluginSwitcher,
|
|
894
|
-
pillNav: navigationState.resolvedPillNav.value,
|
|
895
|
-
currentPillId: navigationState.resolvedCurrentPillId.value,
|
|
896
|
-
showThemeToggle: props.showThemeToggle ?? true,
|
|
897
|
-
showSettings: props.showSettings ?? true,
|
|
898
|
-
settingsConfig: props.settingsConfig,
|
|
899
|
-
showStandaloneLabel: props.showStandaloneLabel ?? false,
|
|
900
|
-
standaloneLabel: props.standaloneLabel ?? "Standalone",
|
|
901
|
-
accountMenu: props.accountMenu,
|
|
902
|
-
showNotifications: props.showNotifications ?? false,
|
|
903
|
-
hasNotificationDot: props.hasNotificationDot ?? false,
|
|
904
|
-
showAdmin: props.showAdmin ?? false,
|
|
905
|
-
adminPath: props.adminPath ?? "/admin",
|
|
906
|
-
userName: props.userName,
|
|
907
|
-
userInitial: props.userInitial,
|
|
908
|
-
userEmail: props.userEmail,
|
|
909
|
-
topBarSlotNames: topBarSlotNames.value,
|
|
910
|
-
controls: controls.value
|
|
911
|
-
})),
|
|
912
|
-
sidebar: computed(() => ({
|
|
913
|
-
title: props.sidebarTitle,
|
|
914
|
-
subtitle: props.sidebarSubtitle,
|
|
915
|
-
badge: props.sidebarBadge,
|
|
916
|
-
panels: props.panels ?? {},
|
|
917
|
-
activeView: navigationState.resolvedActiveView.value,
|
|
918
|
-
floating: false,
|
|
919
|
-
dense: props.dense ?? true,
|
|
920
|
-
width: props.sidebarWidth,
|
|
921
|
-
side: props.sidebarPosition ?? "left",
|
|
922
|
-
forms: props.forms ?? {},
|
|
923
|
-
contentId: props.sidebarContentId,
|
|
924
|
-
showWhenEmpty: (props.showSidebarWhenEmpty ?? false) || props.sidebarContentId !== void 0,
|
|
925
|
-
model: props.model,
|
|
926
|
-
controlSchema: props.controls,
|
|
927
|
-
controlOptions: props.controlOptions ?? {},
|
|
928
|
-
values: controlsState.internalControlValues,
|
|
929
|
-
formEnhancements: props.formEnhancements,
|
|
930
|
-
showFormActions: props.showFormActions ?? false,
|
|
931
|
-
formLoading: props.formLoading ?? false,
|
|
932
|
-
formDisabled: props.formDisabled ?? false,
|
|
933
|
-
formReadonly: props.formReadonly ?? false,
|
|
934
|
-
formSize: props.formSize ?? "sm",
|
|
935
|
-
collapsible: props.sidebarCollapsible ?? true,
|
|
936
|
-
collapsed: props.sidebarCollapsed,
|
|
937
|
-
defaultCollapsed: props.defaultSidebarCollapsed ?? false,
|
|
938
|
-
collapsedWidth: props.sidebarCollapsedWidth ?? "3rem",
|
|
939
|
-
collapseButtonLabel: props.sidebarCollapseButtonLabel ?? "Collapse sidebar",
|
|
940
|
-
expandButtonLabel: props.sidebarExpandButtonLabel ?? "Expand sidebar",
|
|
941
|
-
slotNames: sidebarSlotNames.value
|
|
942
|
-
})),
|
|
943
|
-
controls,
|
|
944
|
-
hasSidebarSurface,
|
|
945
|
-
handleControlValuesUpdate: controlsState.handleControlValuesUpdate,
|
|
946
|
-
handlePageSelectorSelect: navigationState.handlePageSelectorSelect,
|
|
947
|
-
handlePillSelect: navigationState.handlePillSelect
|
|
2739
|
+
experimentId,
|
|
2740
|
+
hasExperiment,
|
|
2741
|
+
experiment,
|
|
2742
|
+
isLoading,
|
|
2743
|
+
error,
|
|
2744
|
+
lastLoadedAt,
|
|
2745
|
+
requireExperimentId,
|
|
2746
|
+
fetch: fetchExperiment,
|
|
2747
|
+
refresh
|
|
948
2748
|
};
|
|
949
2749
|
}
|
|
950
2750
|
//#endregion
|
|
@@ -1070,7 +2870,7 @@ function usePluginJobCenter(options = {}) {
|
|
|
1070
2870
|
}
|
|
1071
2871
|
function replaceLegacyJobs(nextJobs) {
|
|
1072
2872
|
const normalized = nextJobs.map(normalizeJobState);
|
|
1073
|
-
new Set([...jobs.value.map((job) => job.job_id), ...normalized.map((job) => job.job_id)]).forEach(bumpJobRevision);
|
|
2873
|
+
(/* @__PURE__ */ new Set([...jobs.value.map((job) => job.job_id), ...normalized.map((job) => job.job_id)])).forEach(bumpJobRevision);
|
|
1074
2874
|
legacyOverride.value = {
|
|
1075
2875
|
input: toValue(options.jobs),
|
|
1076
2876
|
jobs: normalized
|
|
@@ -1142,8 +2942,10 @@ function usePluginJobCenter(options = {}) {
|
|
|
1142
2942
|
try {
|
|
1143
2943
|
const updated = sourceCancel ? await sourceCancel(job.job_id) : await adapterCancel(job);
|
|
1144
2944
|
if (!active) return false;
|
|
1145
|
-
if (!sourceCancel && revision === (jobRevisions.get(job.job_id) ?? 0))
|
|
1146
|
-
|
|
2945
|
+
if (!sourceCancel && revision === (jobRevisions.get(job.job_id) ?? 0)) {
|
|
2946
|
+
if (updated) upsertLegacyJob(updated);
|
|
2947
|
+
else await refresh();
|
|
2948
|
+
}
|
|
1147
2949
|
return true;
|
|
1148
2950
|
} catch (cause) {
|
|
1149
2951
|
if (active) error.value = errorMessage(cause, "Failed to cancel job");
|
|
@@ -1161,8 +2963,10 @@ function usePluginJobCenter(options = {}) {
|
|
|
1161
2963
|
try {
|
|
1162
2964
|
const deleted = sourceDelete ? await sourceDelete(job.job_id) : await adapterDelete(job);
|
|
1163
2965
|
if (!active) return false;
|
|
1164
|
-
if (!sourceDelete)
|
|
1165
|
-
|
|
2966
|
+
if (!sourceDelete) {
|
|
2967
|
+
if (deleted === false) await refresh();
|
|
2968
|
+
else removeLegacyJobs(/* @__PURE__ */ new Set([job.job_id]));
|
|
2969
|
+
}
|
|
1166
2970
|
return deleted !== false;
|
|
1167
2971
|
} catch (cause) {
|
|
1168
2972
|
if (active) error.value = errorMessage(cause, "Failed to delete job");
|
|
@@ -1243,7 +3047,7 @@ function usePluginJobCenter(options = {}) {
|
|
|
1243
3047
|
isPending: (job) => pendingJobIds.value.has(job.job_id)
|
|
1244
3048
|
};
|
|
1245
3049
|
}
|
|
1246
|
-
/** @deprecated
|
|
3050
|
+
/** @deprecated Removed in MINT 1.4; use `usePluginJobCenter()` for the plugin job-center controller. */
|
|
1247
3051
|
function useJobsStatusTray(options = {}) {
|
|
1248
3052
|
return usePluginJobCenter(options);
|
|
1249
3053
|
}
|
|
@@ -1640,266 +3444,301 @@ function useListReorder(options) {
|
|
|
1640
3444
|
};
|
|
1641
3445
|
}
|
|
1642
3446
|
//#endregion
|
|
1643
|
-
//#region src/composables/
|
|
1644
|
-
var
|
|
1645
|
-
|
|
3447
|
+
//#region src/composables/useRackEditor.ts
|
|
3448
|
+
var SLOT_CYCLE = [
|
|
3449
|
+
"R",
|
|
3450
|
+
"G",
|
|
3451
|
+
"B",
|
|
3452
|
+
"Y"
|
|
3453
|
+
];
|
|
3454
|
+
function wellFromEditData(data) {
|
|
3455
|
+
return {
|
|
3456
|
+
state: "filled",
|
|
3457
|
+
sampleType: data.sampleType || void 0,
|
|
3458
|
+
metadata: {
|
|
3459
|
+
label: data.label,
|
|
3460
|
+
injectionVolume: data.injectionVolume,
|
|
3461
|
+
injectionCount: data.injectionCount,
|
|
3462
|
+
customMethod: data.customMethod || null
|
|
3463
|
+
}
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
3466
|
+
function wellFromDrop(data, rack) {
|
|
3467
|
+
const label = (data.label ?? data.sampleName ?? data.id ?? "").trim();
|
|
3468
|
+
if (!label) return null;
|
|
1646
3469
|
return {
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
3470
|
+
state: "filled",
|
|
3471
|
+
sampleType: data.sampleType ?? "sample",
|
|
3472
|
+
metadata: {
|
|
3473
|
+
label,
|
|
3474
|
+
injectionVolume: data.injectionVolume ?? rack.injectionVolume,
|
|
3475
|
+
injectionCount: data.injectionCount ?? 1,
|
|
3476
|
+
customMethod: data.customMethod ?? null
|
|
3477
|
+
}
|
|
1651
3478
|
};
|
|
1652
3479
|
}
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
const
|
|
1662
|
-
const
|
|
1663
|
-
const
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
const plates = computed(() => internalState.value.plates);
|
|
1674
|
-
const activePlate = computed(() => internalState.value.plates.find((p) => p.id === internalState.value.activePlateId));
|
|
1675
|
-
const samples = computed(() => internalState.value.samples);
|
|
1676
|
-
const selectedWells = computed(() => internalState.value.selectedWells);
|
|
1677
|
-
const activeSampleId = computed(() => internalState.value.activeSampleId);
|
|
1678
|
-
const canUndo = computed(() => historyIndex.value >= 0);
|
|
1679
|
-
const canRedo = computed(() => historyIndex.value < history.value.length - 1);
|
|
1680
|
-
function saveToHistory() {
|
|
1681
|
-
const entry = {
|
|
1682
|
-
plates: cloneValue(internalState.value.plates),
|
|
1683
|
-
samples: cloneValue(internalState.value.samples)
|
|
3480
|
+
var globalIdCounter = 0;
|
|
3481
|
+
function generateId() {
|
|
3482
|
+
globalIdCounter++;
|
|
3483
|
+
return `rack-${Date.now()}-${globalIdCounter}`;
|
|
3484
|
+
}
|
|
3485
|
+
/** Manages a set of sample racks with well-level assignment, series fill, and multi-rack reordering. */
|
|
3486
|
+
function useRackEditor(initialRacks, options) {
|
|
3487
|
+
const defaultFormat = options?.defaultFormat ?? 54;
|
|
3488
|
+
const defaultInjVol = options?.defaultInjectionVolume ?? 5;
|
|
3489
|
+
const maxRacks = options?.maxRacks ?? 10;
|
|
3490
|
+
const minRacks = options?.minRacks ?? 1;
|
|
3491
|
+
function createDefaultRack(name, slotIndex) {
|
|
3492
|
+
const slot = SLOT_CYCLE[(slotIndex ?? 0) % SLOT_CYCLE.length];
|
|
3493
|
+
return {
|
|
3494
|
+
id: generateId(),
|
|
3495
|
+
name: name ?? `Rack ${racks.value.length + 1}`,
|
|
3496
|
+
format: defaultFormat,
|
|
3497
|
+
slot,
|
|
3498
|
+
injectionVolume: defaultInjVol,
|
|
3499
|
+
wells: {}
|
|
1684
3500
|
};
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
}
|
|
1699
|
-
function
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
function addPlate(name, format) {
|
|
1703
|
-
saveToHistory();
|
|
1704
|
-
const plateNumber = internalState.value.plates.length + 1;
|
|
1705
|
-
const plate = createEmptyPlate(name || `Plate ${plateNumber}`, format || defaultFormat);
|
|
1706
|
-
internalState.value.plates.push(plate);
|
|
1707
|
-
internalState.value.activePlateId = plate.id;
|
|
1708
|
-
internalState.value.selectedWells = [];
|
|
1709
|
-
return plate;
|
|
1710
|
-
}
|
|
1711
|
-
function removePlate(plateId) {
|
|
1712
|
-
if (internalState.value.plates.length <= 1) return;
|
|
1713
|
-
saveToHistory();
|
|
1714
|
-
const index = internalState.value.plates.findIndex((p) => p.id === plateId);
|
|
3501
|
+
}
|
|
3502
|
+
const racks = ref(initialRacks && initialRacks.length > 0 ? initialRacks.map((r) => ({
|
|
3503
|
+
...r,
|
|
3504
|
+
wells: { ...r.wells }
|
|
3505
|
+
})) : [createDefaultRack("Rack 1", 0)]);
|
|
3506
|
+
const activeRackId = ref(racks.value[0]?.id ?? "");
|
|
3507
|
+
const activeRack = computed(() => racks.value.find((r) => r.id === activeRackId.value) ?? racks.value[0]);
|
|
3508
|
+
function addRack(name) {
|
|
3509
|
+
if (racks.value.length >= maxRacks) return racks.value[racks.value.length - 1];
|
|
3510
|
+
const rack = createDefaultRack(name, racks.value.length);
|
|
3511
|
+
racks.value.push(rack);
|
|
3512
|
+
activeRackId.value = rack.id;
|
|
3513
|
+
return rack;
|
|
3514
|
+
}
|
|
3515
|
+
function removeRack(rackId) {
|
|
3516
|
+
if (racks.value.length <= minRacks) return;
|
|
3517
|
+
const index = racks.value.findIndex((r) => r.id === rackId);
|
|
1715
3518
|
if (index === -1) return;
|
|
1716
|
-
|
|
1717
|
-
if (
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
3519
|
+
racks.value.splice(index, 1);
|
|
3520
|
+
if (activeRackId.value === rackId && racks.value[0]) activeRackId.value = racks.value[0].id;
|
|
3521
|
+
}
|
|
3522
|
+
function reorderRacks(fromIndex, toIndex) {
|
|
3523
|
+
if (fromIndex < 0 || fromIndex >= racks.value.length) return;
|
|
3524
|
+
if (toIndex < 0 || toIndex >= racks.value.length) return;
|
|
3525
|
+
if (fromIndex === toIndex) return;
|
|
3526
|
+
const [moved] = racks.value.splice(fromIndex, 1);
|
|
3527
|
+
if (moved) racks.value.splice(toIndex, 0, moved);
|
|
3528
|
+
}
|
|
3529
|
+
function updateRack(rackId, data) {
|
|
3530
|
+
const rack = racks.value.find((r) => r.id === rackId);
|
|
3531
|
+
if (!rack) return;
|
|
3532
|
+
if (data.name !== void 0) rack.name = data.name;
|
|
3533
|
+
if (data.format !== void 0) rack.format = data.format;
|
|
3534
|
+
if (data.slot !== void 0) rack.slot = data.slot;
|
|
3535
|
+
if (data.injectionVolume !== void 0) rack.injectionVolume = data.injectionVolume;
|
|
3536
|
+
if (data.wells !== void 0) rack.wells = data.wells;
|
|
3537
|
+
}
|
|
3538
|
+
function setActiveRack(rackId) {
|
|
3539
|
+
if (racks.value.some((r) => r.id === rackId)) activeRackId.value = rackId;
|
|
3540
|
+
}
|
|
3541
|
+
function setWellData(rackId, wellId, data) {
|
|
3542
|
+
const rack = racks.value.find((r) => r.id === rackId);
|
|
3543
|
+
if (!rack) return;
|
|
3544
|
+
rack.wells[wellId] = {
|
|
3545
|
+
...rack.wells[wellId],
|
|
3546
|
+
...data
|
|
1729
3547
|
};
|
|
1730
|
-
internalState.value.samples.push(sample);
|
|
1731
|
-
return sample;
|
|
1732
3548
|
}
|
|
1733
|
-
function
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
col: parseInt(wellId.slice(1)) - 1,
|
|
1755
|
-
state: "empty"
|
|
1756
|
-
};
|
|
1757
|
-
if (sampleId) {
|
|
1758
|
-
well.sampleType = sampleId;
|
|
1759
|
-
well.state = "filled";
|
|
1760
|
-
} else {
|
|
1761
|
-
delete well.sampleType;
|
|
1762
|
-
well.state = "empty";
|
|
1763
|
-
}
|
|
1764
|
-
plate.wells[wellId] = well;
|
|
1765
|
-
}
|
|
1766
|
-
updateSampleCounts();
|
|
1767
|
-
}
|
|
1768
|
-
function clearWells(wellIds) {
|
|
1769
|
-
assignSample(wellIds, void 0);
|
|
1770
|
-
}
|
|
1771
|
-
function updateSampleCounts() {
|
|
1772
|
-
const counts = {};
|
|
1773
|
-
for (const plate of internalState.value.plates) for (const well of Object.values(plate.wells)) if (well.sampleType) counts[well.sampleType] = (counts[well.sampleType] || 0) + 1;
|
|
1774
|
-
for (const sample of internalState.value.samples) sample.count = counts[sample.id] || 0;
|
|
1775
|
-
}
|
|
1776
|
-
function undo() {
|
|
1777
|
-
if (!canUndo.value) return;
|
|
1778
|
-
const entry = history.value[historyIndex.value];
|
|
1779
|
-
historyIndex.value--;
|
|
1780
|
-
internalState.value.plates = cloneValue(entry.plates);
|
|
1781
|
-
internalState.value.samples = cloneValue(entry.samples);
|
|
1782
|
-
if (!internalState.value.plates.some((p) => p.id === internalState.value.activePlateId)) internalState.value.activePlateId = internalState.value.plates[0]?.id || "";
|
|
1783
|
-
internalState.value.selectedWells = [];
|
|
1784
|
-
}
|
|
1785
|
-
function redo() {
|
|
1786
|
-
if (!canRedo.value) return;
|
|
1787
|
-
historyIndex.value++;
|
|
1788
|
-
const entry = history.value[historyIndex.value];
|
|
1789
|
-
internalState.value.plates = cloneValue(entry.plates);
|
|
1790
|
-
internalState.value.samples = cloneValue(entry.samples);
|
|
1791
|
-
internalState.value.selectedWells = [];
|
|
1792
|
-
}
|
|
1793
|
-
function exportData(format) {
|
|
1794
|
-
if (format === "json") return JSON.stringify({
|
|
1795
|
-
plates: internalState.value.plates,
|
|
1796
|
-
samples: internalState.value.samples
|
|
1797
|
-
}, null, 2);
|
|
1798
|
-
const sampleMap = new Map(internalState.value.samples.map((s) => [s.id, s.name]));
|
|
1799
|
-
const rows = ["Plate,Well,Sample Type,Sample Name"];
|
|
1800
|
-
for (const plate of internalState.value.plates) for (const [wellId, well] of Object.entries(plate.wells)) if (well.sampleType) {
|
|
1801
|
-
const sampleName = sampleMap.get(well.sampleType) || "";
|
|
1802
|
-
rows.push(`"${plate.name}","${wellId}","${well.sampleType}","${sampleName}"`);
|
|
1803
|
-
}
|
|
1804
|
-
return rows.join("\n");
|
|
1805
|
-
}
|
|
1806
|
-
function importData(data, format) {
|
|
1807
|
-
try {
|
|
1808
|
-
saveToHistory();
|
|
1809
|
-
if (format === "json") {
|
|
1810
|
-
const parsed = JSON.parse(data);
|
|
1811
|
-
if (parsed.plates && Array.isArray(parsed.plates)) {
|
|
1812
|
-
internalState.value.plates = parsed.plates;
|
|
1813
|
-
internalState.value.activePlateId = parsed.plates[0]?.id || "";
|
|
1814
|
-
}
|
|
1815
|
-
if (parsed.samples && Array.isArray(parsed.samples)) internalState.value.samples = parsed.samples;
|
|
1816
|
-
internalState.value.selectedWells = [];
|
|
1817
|
-
updateSampleCounts();
|
|
1818
|
-
return true;
|
|
1819
|
-
}
|
|
1820
|
-
const lines = data.trim().split("\n");
|
|
1821
|
-
if (lines.length < 2) return false;
|
|
1822
|
-
const plateMap = /* @__PURE__ */ new Map();
|
|
1823
|
-
const sampleMap = /* @__PURE__ */ new Map();
|
|
1824
|
-
for (let i = 1; i < lines.length; i++) {
|
|
1825
|
-
const parts = lines[i].match(/(?:[^",]+|"[^"]*")+/g);
|
|
1826
|
-
if (!parts || parts.length < 4) continue;
|
|
1827
|
-
const plateName = parts[0].replace(/^"|"$/g, "");
|
|
1828
|
-
const wellId = parts[1].replace(/^"|"$/g, "");
|
|
1829
|
-
const sampleId = parts[2].replace(/^"|"$/g, "");
|
|
1830
|
-
const sampleName = parts[3].replace(/^"|"$/g, "");
|
|
1831
|
-
if (!plateMap.has(plateName)) plateMap.set(plateName, createEmptyPlate(plateName, defaultFormat));
|
|
1832
|
-
if (sampleId && !sampleMap.has(sampleId)) sampleMap.set(sampleId, {
|
|
1833
|
-
id: sampleId,
|
|
1834
|
-
name: sampleName || sampleId,
|
|
1835
|
-
color: SAMPLE_COLOR_SCALE[sampleMap.size % SAMPLE_COLOR_SCALE.length],
|
|
1836
|
-
count: 0
|
|
1837
|
-
});
|
|
1838
|
-
const plate = plateMap.get(plateName);
|
|
1839
|
-
plate.wells[wellId] = {
|
|
3549
|
+
function clearWell(rackId, wellId) {
|
|
3550
|
+
const rack = racks.value.find((r) => r.id === rackId);
|
|
3551
|
+
if (!rack) return;
|
|
3552
|
+
delete rack.wells[wellId];
|
|
3553
|
+
}
|
|
3554
|
+
function clearAllWells(rackId) {
|
|
3555
|
+
const rack = racks.value.find((r) => r.id === rackId);
|
|
3556
|
+
if (!rack) return;
|
|
3557
|
+
rack.wells = {};
|
|
3558
|
+
}
|
|
3559
|
+
function fillSeries(rackId, prefix = "S") {
|
|
3560
|
+
const rack = racks.value.find((r) => r.id === rackId);
|
|
3561
|
+
if (!rack) return;
|
|
3562
|
+
const config = PLATE_CONFIGS[rack.format];
|
|
3563
|
+
if (!config) return;
|
|
3564
|
+
let counter = 1;
|
|
3565
|
+
for (let row = 0; row < config.rows; row++) for (let col = 0; col < config.cols; col++) {
|
|
3566
|
+
const wellId = `${rowLabel(row)}${col + 1}`;
|
|
3567
|
+
if (!rack.wells[wellId]?.sampleType) {
|
|
3568
|
+
const paddedNum = String(counter).padStart(3, "0");
|
|
3569
|
+
rack.wells[wellId] = {
|
|
1840
3570
|
id: wellId,
|
|
1841
|
-
row
|
|
1842
|
-
col
|
|
1843
|
-
state:
|
|
1844
|
-
sampleType:
|
|
3571
|
+
row,
|
|
3572
|
+
col,
|
|
3573
|
+
state: "filled",
|
|
3574
|
+
sampleType: "sample",
|
|
3575
|
+
metadata: {
|
|
3576
|
+
label: `${prefix}${paddedNum}`,
|
|
3577
|
+
injectionVolume: rack.injectionVolume,
|
|
3578
|
+
injectionCount: 1
|
|
3579
|
+
}
|
|
1845
3580
|
};
|
|
1846
3581
|
}
|
|
1847
|
-
|
|
1848
|
-
internalState.value.samples = Array.from(sampleMap.values());
|
|
1849
|
-
internalState.value.activePlateId = internalState.value.plates[0]?.id || "";
|
|
1850
|
-
internalState.value.selectedWells = [];
|
|
1851
|
-
updateSampleCounts();
|
|
1852
|
-
return true;
|
|
1853
|
-
} catch {
|
|
1854
|
-
return false;
|
|
3582
|
+
counter++;
|
|
1855
3583
|
}
|
|
1856
3584
|
}
|
|
1857
|
-
function
|
|
1858
|
-
|
|
1859
|
-
if (
|
|
1860
|
-
|
|
1861
|
-
|
|
3585
|
+
function moveWell(rackId, sourceWellId, targetWellId) {
|
|
3586
|
+
const rack = racks.value.find((r) => r.id === rackId);
|
|
3587
|
+
if (!rack) return;
|
|
3588
|
+
const { [sourceWellId]: source, [targetWellId]: target } = rack.wells;
|
|
3589
|
+
delete rack.wells[sourceWellId];
|
|
3590
|
+
delete rack.wells[targetWellId];
|
|
3591
|
+
if (target) rack.wells[sourceWellId] = placed(target, sourceWellId);
|
|
3592
|
+
if (source) rack.wells[targetWellId] = placed(source, targetWellId);
|
|
3593
|
+
}
|
|
3594
|
+
/** A moved well keeps its position fields in step with its new key. */
|
|
3595
|
+
function placed(well, wellId) {
|
|
3596
|
+
if (well.id === void 0) return well;
|
|
3597
|
+
const [, row = "", col = ""] = /^([A-Z]+)(\d+)$/.exec(wellId) ?? [];
|
|
3598
|
+
return {
|
|
3599
|
+
...well,
|
|
3600
|
+
id: wellId,
|
|
3601
|
+
row: rowIndex(row),
|
|
3602
|
+
col: Number(col) - 1
|
|
3603
|
+
};
|
|
3604
|
+
}
|
|
3605
|
+
function assignWells(rackId, wellIds, groupId) {
|
|
3606
|
+
const rack = racks.value.find((r) => r.id === rackId);
|
|
3607
|
+
for (const wellId of wellIds) {
|
|
3608
|
+
const sampleType = rack?.wells[wellId]?.sampleType ?? "sample";
|
|
3609
|
+
if (groupId) setWellData(rackId, wellId, {
|
|
3610
|
+
state: "filled",
|
|
3611
|
+
sampleType,
|
|
3612
|
+
group: groupId
|
|
3613
|
+
});
|
|
3614
|
+
else clearWell(rackId, wellId);
|
|
1862
3615
|
}
|
|
1863
|
-
if (state.samples) internalState.value.samples = cloneValue(state.samples);
|
|
1864
|
-
internalState.value.selectedWells = state.selectedWells ?? [];
|
|
1865
|
-
internalState.value.activeSampleId = state.activeSampleId;
|
|
1866
|
-
updateSampleCounts();
|
|
1867
3616
|
}
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
3617
|
+
const undoStack = ref([]);
|
|
3618
|
+
const redoStack = ref([]);
|
|
3619
|
+
const canUndo = computed(() => undoStack.value.length > 0);
|
|
3620
|
+
const canRedo = computed(() => redoStack.value.length > 0);
|
|
3621
|
+
/** The racks as the history last saw them. */
|
|
3622
|
+
let known = JSON.stringify(racks.value);
|
|
3623
|
+
function pushUndo(snapshot) {
|
|
3624
|
+
undoStack.value = [...undoStack.value, snapshot].slice(-50);
|
|
3625
|
+
redoStack.value = [];
|
|
3626
|
+
}
|
|
3627
|
+
/** Records a change made since the history last looked, as its own step. */
|
|
3628
|
+
function absorb() {
|
|
3629
|
+
const now = JSON.stringify(racks.value);
|
|
3630
|
+
if (now === known) return;
|
|
3631
|
+
pushUndo({
|
|
3632
|
+
racks: known,
|
|
3633
|
+
activeRackId: activeRackId.value
|
|
3634
|
+
});
|
|
3635
|
+
known = now;
|
|
3636
|
+
}
|
|
3637
|
+
watch(racks, absorb, { deep: true });
|
|
3638
|
+
function step(from, to) {
|
|
3639
|
+
absorb();
|
|
3640
|
+
const snapshot = from.value.pop();
|
|
3641
|
+
if (snapshot === void 0) return;
|
|
3642
|
+
to.value.push({
|
|
3643
|
+
racks: known,
|
|
3644
|
+
activeRackId: activeRackId.value
|
|
3645
|
+
});
|
|
3646
|
+
racks.value = JSON.parse(snapshot.racks);
|
|
3647
|
+
known = snapshot.racks;
|
|
3648
|
+
activeRackId.value = racks.value.some((rack) => rack.id === snapshot.activeRackId) ? snapshot.activeRackId : racks.value[0]?.id ?? "";
|
|
3649
|
+
}
|
|
3650
|
+
const undo = () => step(undoStack, redoStack);
|
|
3651
|
+
const redo = () => step(redoStack, undoStack);
|
|
3652
|
+
function clearHistory() {
|
|
3653
|
+
undoStack.value = [];
|
|
3654
|
+
redoStack.value = [];
|
|
3655
|
+
known = JSON.stringify(racks.value);
|
|
3656
|
+
}
|
|
3657
|
+
/** Runs `apply` and records the prior state when it changed anything. */
|
|
3658
|
+
function recorded(apply) {
|
|
3659
|
+
return (...args) => {
|
|
3660
|
+
absorb();
|
|
3661
|
+
const before = {
|
|
3662
|
+
racks: known,
|
|
3663
|
+
activeRackId: activeRackId.value
|
|
3664
|
+
};
|
|
3665
|
+
const result = apply(...args);
|
|
3666
|
+
const after = JSON.stringify(racks.value);
|
|
3667
|
+
if (after !== known) {
|
|
3668
|
+
pushUndo(before);
|
|
3669
|
+
known = after;
|
|
3670
|
+
}
|
|
3671
|
+
return result;
|
|
1876
3672
|
};
|
|
1877
|
-
|
|
1878
|
-
|
|
3673
|
+
}
|
|
3674
|
+
const plateEditorListeners = {
|
|
3675
|
+
"update:activeRackId": setActiveRack,
|
|
3676
|
+
"rack-add": recorded(() => {
|
|
3677
|
+
addRack();
|
|
3678
|
+
}),
|
|
3679
|
+
"rack-remove": recorded(removeRack),
|
|
3680
|
+
"rack-reorder": recorded((rackIds) => {
|
|
3681
|
+
racks.value = rackIds.flatMap((id) => racks.value.filter((rack) => rack.id === id));
|
|
3682
|
+
}),
|
|
3683
|
+
"update:format": recorded((rackId, format) => updateRack(rackId, { format })),
|
|
3684
|
+
"update:slot": recorded((rackId, slot) => updateRack(rackId, { slot })),
|
|
3685
|
+
"well-edit": recorded((rackId, wellId, data) => setWellData(rackId, wellId, wellFromEditData(data))),
|
|
3686
|
+
"well-clear": recorded(clearWell),
|
|
3687
|
+
"well-move": recorded(moveWell),
|
|
3688
|
+
"sample-drop": recorded((rackId, wellId, data) => {
|
|
3689
|
+
const rack = racks.value.find((r) => r.id === rackId);
|
|
3690
|
+
const well = rack && wellFromDrop(data, rack);
|
|
3691
|
+
if (well) setWellData(rackId, wellId, well);
|
|
3692
|
+
}),
|
|
3693
|
+
"assign": recorded(assignWells),
|
|
3694
|
+
"clear": recorded(clearAllWells),
|
|
3695
|
+
"fill-series": recorded((rackId) => fillSeries(rackId)),
|
|
3696
|
+
"undo": undo,
|
|
3697
|
+
"redo": redo
|
|
3698
|
+
};
|
|
3699
|
+
function getAllWells() {
|
|
3700
|
+
const result = [];
|
|
3701
|
+
for (const rack of racks.value) for (const [wellId, well] of Object.entries(rack.wells)) result.push({
|
|
3702
|
+
rackId: rack.id,
|
|
3703
|
+
wellId,
|
|
3704
|
+
well
|
|
3705
|
+
});
|
|
3706
|
+
return result;
|
|
3707
|
+
}
|
|
3708
|
+
const totalSampleCount = computed(() => {
|
|
3709
|
+
let count = 0;
|
|
3710
|
+
for (const rack of racks.value) count += Object.keys(rack.wells).length;
|
|
3711
|
+
return count;
|
|
3712
|
+
});
|
|
3713
|
+
function reset() {
|
|
3714
|
+
const rack = createDefaultRack("Rack 1", 0);
|
|
3715
|
+
racks.value = [rack];
|
|
3716
|
+
activeRackId.value = rack.id;
|
|
3717
|
+
clearHistory();
|
|
1879
3718
|
}
|
|
1880
3719
|
return {
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
assignSample,
|
|
1897
|
-
clearWells,
|
|
3720
|
+
racks,
|
|
3721
|
+
activeRack,
|
|
3722
|
+
activeRackId,
|
|
3723
|
+
addRack: recorded(addRack),
|
|
3724
|
+
removeRack: recorded(removeRack),
|
|
3725
|
+
reorderRacks: recorded(reorderRacks),
|
|
3726
|
+
updateRack: recorded(updateRack),
|
|
3727
|
+
setActiveRack,
|
|
3728
|
+
setWellData: recorded(setWellData),
|
|
3729
|
+
clearWell: recorded(clearWell),
|
|
3730
|
+
clearAllWells: recorded(clearAllWells),
|
|
3731
|
+
fillSeries: recorded(fillSeries),
|
|
3732
|
+
moveWell: recorded(moveWell),
|
|
3733
|
+
assignWells: recorded(assignWells),
|
|
3734
|
+
plateEditorListeners,
|
|
1898
3735
|
undo,
|
|
1899
3736
|
redo,
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
3737
|
+
canUndo,
|
|
3738
|
+
canRedo,
|
|
3739
|
+
clearHistory,
|
|
3740
|
+
getAllWells,
|
|
3741
|
+
totalSampleCount,
|
|
1903
3742
|
reset
|
|
1904
3743
|
};
|
|
1905
3744
|
}
|
|
@@ -2328,8 +4167,10 @@ function buildClassSchema(tokenizedMembers, classKind, subKind, classTagPosition
|
|
|
2328
4167
|
});
|
|
2329
4168
|
}
|
|
2330
4169
|
let usernameAssigned = false;
|
|
2331
|
-
for (const c of columns) if (c.name === "Username")
|
|
2332
|
-
|
|
4170
|
+
for (const c of columns) if (c.name === "Username") {
|
|
4171
|
+
if (usernameAssigned) c.name = "Project";
|
|
4172
|
+
else usernameAssigned = true;
|
|
4173
|
+
}
|
|
2333
4174
|
return {
|
|
2334
4175
|
classKind,
|
|
2335
4176
|
subKind,
|
|
@@ -2348,7 +4189,7 @@ function functionalDependency(rows, srcIdx, dstIdx) {
|
|
|
2348
4189
|
const b = row[dstIdx] ?? "";
|
|
2349
4190
|
const set = map.get(a);
|
|
2350
4191
|
if (set) set.add(b);
|
|
2351
|
-
else map.set(a, new Set([b]));
|
|
4192
|
+
else map.set(a, /* @__PURE__ */ new Set([b]));
|
|
2352
4193
|
}
|
|
2353
4194
|
if (map.size === 0) return 0;
|
|
2354
4195
|
let functional = 0;
|
|
@@ -2923,59 +4764,6 @@ function extractSampleNamesFromDesignData(rawData, options = {}) {
|
|
|
2923
4764
|
return extractSampleOptionsFromDesignData(rawData, options).map((sample) => sample.value);
|
|
2924
4765
|
}
|
|
2925
4766
|
//#endregion
|
|
2926
|
-
//#region src/composables/autoGroup/csv-shim.ts
|
|
2927
|
-
function parseCSVLine(line, delimiter = ",") {
|
|
2928
|
-
const result = [];
|
|
2929
|
-
let current = "";
|
|
2930
|
-
let inQuotes = false;
|
|
2931
|
-
for (let i = 0; i < line.length; i++) {
|
|
2932
|
-
const char = line[i];
|
|
2933
|
-
if (char === "\"") if (inQuotes && line[i + 1] === "\"") {
|
|
2934
|
-
current += "\"";
|
|
2935
|
-
i++;
|
|
2936
|
-
} else inQuotes = !inQuotes;
|
|
2937
|
-
else if (char === delimiter && !inQuotes) {
|
|
2938
|
-
result.push(current.trim());
|
|
2939
|
-
current = "";
|
|
2940
|
-
} else current += char;
|
|
2941
|
-
}
|
|
2942
|
-
result.push(current.trim());
|
|
2943
|
-
return result;
|
|
2944
|
-
}
|
|
2945
|
-
function parseCSV(text) {
|
|
2946
|
-
const lines = text.trim().split("\n");
|
|
2947
|
-
if (lines.length < 2) throw new Error("CSV must have at least a header and one data row");
|
|
2948
|
-
const firstLine = lines[0];
|
|
2949
|
-
const csvDelimiter = firstLine.includes(" ") ? " " : ",";
|
|
2950
|
-
const headers = parseCSVLine(firstLine, csvDelimiter);
|
|
2951
|
-
const rows = [];
|
|
2952
|
-
for (let i = 1; i < lines.length; i++) {
|
|
2953
|
-
const values = parseCSVLine(lines[i], csvDelimiter);
|
|
2954
|
-
if (values.length !== headers.length) continue;
|
|
2955
|
-
const row = {};
|
|
2956
|
-
headers.forEach((h, idx) => {
|
|
2957
|
-
row[h] = values[idx];
|
|
2958
|
-
});
|
|
2959
|
-
rows.push(row);
|
|
2960
|
-
}
|
|
2961
|
-
const sampleKeywords = [
|
|
2962
|
-
"sample",
|
|
2963
|
-
"name",
|
|
2964
|
-
"id",
|
|
2965
|
-
"sample_name",
|
|
2966
|
-
"samplename",
|
|
2967
|
-
"file name",
|
|
2968
|
-
"filename",
|
|
2969
|
-
"file_name"
|
|
2970
|
-
];
|
|
2971
|
-
return {
|
|
2972
|
-
columns: headers,
|
|
2973
|
-
rows,
|
|
2974
|
-
sampleColumn: headers.find((h) => sampleKeywords.includes(h.toLowerCase())) ?? headers[0],
|
|
2975
|
-
delimiter: csvDelimiter
|
|
2976
|
-
};
|
|
2977
|
-
}
|
|
2978
|
-
//#endregion
|
|
2979
4767
|
//#region src/composables/useAutoGroup.ts
|
|
2980
4768
|
var STRUCTURED_MISSING_GROUP = "Outlier";
|
|
2981
4769
|
var deprecationWarned = false;
|
|
@@ -2986,6 +4774,11 @@ function warnDeprecated(api) {
|
|
|
2986
4774
|
console.warn(`[useAutoGroup] ${api} is deprecated; use the new class-first API (setClassDisposition / toggleGroupBy / setValueOps).`);
|
|
2987
4775
|
deprecationWarned = true;
|
|
2988
4776
|
}
|
|
4777
|
+
/**
|
|
4778
|
+
* Sample-grouping engine behind `AutoGroupModal`.
|
|
4779
|
+
*
|
|
4780
|
+
* @deprecated Scheduled for removal in MINT 1.4; no SDK replacement.
|
|
4781
|
+
*/
|
|
2989
4782
|
function useAutoGroup() {
|
|
2990
4783
|
const inputMode = ref("paste");
|
|
2991
4784
|
const rawText = ref("");
|
|
@@ -3031,13 +4824,14 @@ function useAutoGroup() {
|
|
|
3031
4824
|
}
|
|
3032
4825
|
const tokens = csv.rows.map((r) => nonSampleCols.map((col) => r[col] ?? ""));
|
|
3033
4826
|
tokenized.value = tokens;
|
|
3034
|
-
|
|
4827
|
+
const structuredClass = {
|
|
3035
4828
|
kind: "unknown",
|
|
3036
4829
|
label: "Unknown",
|
|
3037
4830
|
members: csv.rows.map((_, i) => i),
|
|
3038
4831
|
classTagPositions: [],
|
|
3039
4832
|
disposition: "group"
|
|
3040
|
-
}
|
|
4833
|
+
};
|
|
4834
|
+
classes.value = [structuredClass];
|
|
3041
4835
|
const schema = buildClassSchema(tokens, "unknown", void 0, []);
|
|
3042
4836
|
schema.columns = schema.columns.map((c, i) => ({
|
|
3043
4837
|
...c,
|
|
@@ -3438,6 +5232,11 @@ function useAutoGroup() {
|
|
|
3438
5232
|
renameField
|
|
3439
5233
|
};
|
|
3440
5234
|
}
|
|
5235
|
+
/**
|
|
5236
|
+
* Reads experiment `design_data` samples into the `useAutoGroup` CSV shape.
|
|
5237
|
+
*
|
|
5238
|
+
* @deprecated Scheduled for removal in MINT 1.4 together with `useAutoGroup`.
|
|
5239
|
+
*/
|
|
3441
5240
|
function extractSamplesFromDesignData(rawData, options = {}) {
|
|
3442
5241
|
const designData = unwrapExperimentDesignData(rawData);
|
|
3443
5242
|
if (!designData) return null;
|
|
@@ -3538,7 +5337,8 @@ function useFileImport(options = {}) {
|
|
|
3538
5337
|
result.value = parsed;
|
|
3539
5338
|
return parsed;
|
|
3540
5339
|
} catch (err) {
|
|
3541
|
-
|
|
5340
|
+
const message = readErrorMessage(err, "Failed to import file.");
|
|
5341
|
+
error.value = message;
|
|
3542
5342
|
throw err;
|
|
3543
5343
|
} finally {
|
|
3544
5344
|
loading.value = false;
|
|
@@ -3645,8 +5445,8 @@ function countChar(value, char) {
|
|
|
3645
5445
|
}
|
|
3646
5446
|
function formatBytes(bytes) {
|
|
3647
5447
|
if (bytes < 1024) return `${bytes} B`;
|
|
3648
|
-
if (bytes <
|
|
3649
|
-
return `${Math.round(bytes /
|
|
5448
|
+
if (bytes < 1048576) return `${Math.round(bytes / 1024)} KB`;
|
|
5449
|
+
return `${Math.round(bytes / 1048576)} MB`;
|
|
3650
5450
|
}
|
|
3651
5451
|
//#endregion
|
|
3652
5452
|
//#region src/utils/sampleGroupPath.ts
|
|
@@ -3797,7 +5597,7 @@ function useExpansionSet(options = {}) {
|
|
|
3797
5597
|
}
|
|
3798
5598
|
function expand(id) {
|
|
3799
5599
|
if (expandedIds.value.has(id)) return;
|
|
3800
|
-
expandedIds.value = new Set([...expandedIds.value, id]);
|
|
5600
|
+
expandedIds.value = /* @__PURE__ */ new Set([...expandedIds.value, id]);
|
|
3801
5601
|
}
|
|
3802
5602
|
function collapse(id) {
|
|
3803
5603
|
if (!expandedIds.value.has(id)) return;
|
|
@@ -3814,7 +5614,7 @@ function useExpansionSet(options = {}) {
|
|
|
3814
5614
|
return true;
|
|
3815
5615
|
}
|
|
3816
5616
|
function expandMany(ids) {
|
|
3817
|
-
expandedIds.value = new Set([...expandedIds.value, ...normalizeIds(ids)]);
|
|
5617
|
+
expandedIds.value = /* @__PURE__ */ new Set([...expandedIds.value, ...normalizeIds(ids)]);
|
|
3818
5618
|
}
|
|
3819
5619
|
function setExpanded(ids) {
|
|
3820
5620
|
expandedIds.value = new Set(normalizeIds(ids));
|
|
@@ -4165,15 +5965,18 @@ function minutesToDate(baseDate, minutes) {
|
|
|
4165
5965
|
}
|
|
4166
5966
|
function clampRange(start, end, min, max) {
|
|
4167
5967
|
const s = Math.max(start, min);
|
|
4168
|
-
const e = Math.min(end, max);
|
|
4169
5968
|
return {
|
|
4170
5969
|
start: s,
|
|
4171
|
-
end: Math.max(
|
|
5970
|
+
end: Math.max(Math.min(end, max), s)
|
|
4172
5971
|
};
|
|
4173
5972
|
}
|
|
4174
5973
|
//#endregion
|
|
4175
5974
|
//#region src/composables/useExperimentSave.ts
|
|
4176
|
-
/**
|
|
5975
|
+
/**
|
|
5976
|
+
* Persists experiment design, analysis, and built-in template preset data through the platform API.
|
|
5977
|
+
*
|
|
5978
|
+
* @deprecated Scheduled for removal in MINT 1.4; no SDK replacement.
|
|
5979
|
+
*/
|
|
4177
5980
|
function useExperimentSave(options = {}) {
|
|
4178
5981
|
const injectedContext = getInjectedPlatformContext();
|
|
4179
5982
|
const api = useApi({ baseUrl: options.apiBaseUrl ?? injectedContext?.platformApiUrl });
|
|
@@ -4371,7 +6174,11 @@ function useExperimentSave(options = {}) {
|
|
|
4371
6174
|
}
|
|
4372
6175
|
//#endregion
|
|
4373
6176
|
//#region src/composables/useTemplateCollection.ts
|
|
4374
|
-
/**
|
|
6177
|
+
/**
|
|
6178
|
+
* Manages a biology template collection in the current experiment's design_data.
|
|
6179
|
+
*
|
|
6180
|
+
* @deprecated Scheduled for removal in MINT 1.4; no SDK replacement.
|
|
6181
|
+
*/
|
|
4375
6182
|
function useTemplateCollection(options = {}) {
|
|
4376
6183
|
const experimentSave = useExperimentSave(options);
|
|
4377
6184
|
const templateCollection = ref(createInitialCollection(options));
|
|
@@ -4467,7 +6274,11 @@ function readCollectionMetadata(value) {
|
|
|
4467
6274
|
}
|
|
4468
6275
|
//#endregion
|
|
4469
6276
|
//#region src/composables/useBioTemplateControls.ts
|
|
4470
|
-
/**
|
|
6277
|
+
/**
|
|
6278
|
+
* Build FormBuilder schema, AppSidebar panels, and defaults for a built-in biology template or preset.
|
|
6279
|
+
*
|
|
6280
|
+
* @deprecated Scheduled for removal in MINT 1.4; no SDK replacement.
|
|
6281
|
+
*/
|
|
4471
6282
|
function useBioTemplateControls(target, options = {}) {
|
|
4472
6283
|
const { initialValues, topBarSettings: _topBarSettings, ...schemaOptions } = options;
|
|
4473
6284
|
const controls = createBioTemplateControlToolkit(target, schemaOptions);
|
|
@@ -4482,15 +6293,27 @@ function useBioTemplateControls(target, options = {}) {
|
|
|
4482
6293
|
}
|
|
4483
6294
|
//#endregion
|
|
4484
6295
|
//#region src/composables/useBioTemplateComponents.ts
|
|
4485
|
-
/**
|
|
6296
|
+
/**
|
|
6297
|
+
* Return component props grouped by SDK component name for direct WellPlate/DoseCalculator binding.
|
|
6298
|
+
*
|
|
6299
|
+
* @deprecated Scheduled for removal in MINT 1.4; no SDK replacement.
|
|
6300
|
+
*/
|
|
4486
6301
|
function toBioTemplateComponentPropsByComponent(target) {
|
|
4487
6302
|
return toBioTemplateComponentPropsByComponent$1(target);
|
|
4488
6303
|
}
|
|
4489
|
-
/**
|
|
6304
|
+
/**
|
|
6305
|
+
* Return the first props object for an SDK component, optionally restricted by template id or binding id.
|
|
6306
|
+
*
|
|
6307
|
+
* @deprecated Scheduled for removal in MINT 1.4; no SDK replacement.
|
|
6308
|
+
*/
|
|
4490
6309
|
function getBioTemplateComponentProps(target, component, options = {}) {
|
|
4491
6310
|
return getBioTemplateComponentProps$1(target, component, options);
|
|
4492
6311
|
}
|
|
4493
|
-
/**
|
|
6312
|
+
/**
|
|
6313
|
+
* Map built-in biology templates to SDK components such as WellPlate, DataFrame, DoseCalculator, and PlateEditor.
|
|
6314
|
+
*
|
|
6315
|
+
* @deprecated Scheduled for removal in MINT 1.4; no SDK replacement.
|
|
6316
|
+
*/
|
|
4494
6317
|
function useBioTemplateComponents(target) {
|
|
4495
6318
|
return {
|
|
4496
6319
|
bindings: getBioTemplateComponentBindings(target),
|
|
@@ -4558,7 +6381,11 @@ function liftWorkspaceComponentBindings(workspace) {
|
|
|
4558
6381
|
}
|
|
4559
6382
|
//#endregion
|
|
4560
6383
|
//#region src/composables/useBioTemplateWorkspace.ts
|
|
4561
|
-
/**
|
|
6384
|
+
/**
|
|
6385
|
+
* One-stop wiring for BioTemplateRenderer, FormBuilder, AppSidebar, and template-aware SDK components.
|
|
6386
|
+
*
|
|
6387
|
+
* @deprecated Scheduled for removal in MINT 1.4; no SDK replacement.
|
|
6388
|
+
*/
|
|
4562
6389
|
function useBioTemplateWorkspace(target, options = {}) {
|
|
4563
6390
|
const controls = useBioTemplateControls(target, options);
|
|
4564
6391
|
const workspace = useControlWorkspace(controls.controls, options);
|
|
@@ -4615,6 +6442,8 @@ function isTemplateObjectTarget(target) {
|
|
|
4615
6442
|
* const pack = useBioTemplatePackWorkspace('cell-culture-screen')
|
|
4616
6443
|
* await pack.loadCurrent()
|
|
4617
6444
|
* ```
|
|
6445
|
+
*
|
|
6446
|
+
* @deprecated Scheduled for removal in MINT 1.4; no SDK replacement.
|
|
4618
6447
|
*/
|
|
4619
6448
|
function useBioTemplatePackWorkspace(packId, options = {}) {
|
|
4620
6449
|
const pack = getBioTemplatePackInfo(packId);
|
|
@@ -4695,7 +6524,11 @@ function useBioTemplatePackWorkspace(packId, options = {}) {
|
|
|
4695
6524
|
}
|
|
4696
6525
|
//#endregion
|
|
4697
6526
|
//#region src/composables/useBioTemplatePresetWorkspace.ts
|
|
4698
|
-
/**
|
|
6527
|
+
/**
|
|
6528
|
+
* Create a complete editable WellPlate/DoseCalculator workspace for dose design from a built-in biology template preset with current-experiment save wiring.
|
|
6529
|
+
*
|
|
6530
|
+
* @deprecated Scheduled for removal in MINT 1.4; no SDK replacement.
|
|
6531
|
+
*/
|
|
4699
6532
|
function useBioTemplatePresetWorkspace(presetId, options = {}) {
|
|
4700
6533
|
const { controlOptions, initialValues, ...collectionOptions } = options;
|
|
4701
6534
|
const resolvedControlOptions = mergeInitialValues(controlOptions, initialValues);
|
|
@@ -4779,241 +6612,98 @@ function useBioTemplatePresetWorkspace(presetId, options = {}) {
|
|
|
4779
6612
|
function applyCollection(value) {
|
|
4780
6613
|
return templateCollection.apply(value);
|
|
4781
6614
|
}
|
|
4782
|
-
function setActiveControlView(viewId) {
|
|
4783
|
-
if (!controlViewIds.value.includes(viewId)) return;
|
|
4784
|
-
activeControlView.value = viewId;
|
|
4785
|
-
}
|
|
4786
|
-
function setControlValues(values) {
|
|
4787
|
-
const nextValues = { ...values };
|
|
4788
|
-
controlValues.value = nextValues;
|
|
4789
|
-
syncControlValueBindings(nextValues);
|
|
4790
|
-
return templateCollection.apply(createBioTemplatePresetCollectionFromControls(presetId, nextValues));
|
|
4791
|
-
}
|
|
4792
|
-
function updateControlValues(values) {
|
|
4793
|
-
return setControlValues({
|
|
4794
|
-
...controlValues.value,
|
|
4795
|
-
...values
|
|
4796
|
-
});
|
|
4797
|
-
}
|
|
4798
|
-
function resetToDefaultCollection() {
|
|
4799
|
-
return setControlValues({ ...controls.initialValues });
|
|
4800
|
-
}
|
|
4801
|
-
function syncControlValueBindings(values) {
|
|
4802
|
-
form.modelValue = values;
|
|
4803
|
-
sidebar.modelValue = values;
|
|
4804
|
-
sidebar.values = values;
|
|
4805
|
-
topBarSettings.settingsConfig = {
|
|
4806
|
-
...topBarSettings.settingsConfig,
|
|
4807
|
-
values
|
|
4808
|
-
};
|
|
4809
|
-
}
|
|
4810
|
-
return {
|
|
4811
|
-
presetId,
|
|
4812
|
-
presetInfo,
|
|
4813
|
-
presetLabel,
|
|
4814
|
-
collection,
|
|
4815
|
-
templates: templateCollection.templates,
|
|
4816
|
-
templateIds: templateCollection.templateIds,
|
|
4817
|
-
currentExperimentId: templateCollection.currentExperimentId,
|
|
4818
|
-
hasCurrentExperiment: templateCollection.hasCurrentExperiment,
|
|
4819
|
-
saving: templateCollection.isSaving,
|
|
4820
|
-
error: templateCollection.error,
|
|
4821
|
-
lastSavedAt: templateCollection.lastSavedAt,
|
|
4822
|
-
controls,
|
|
4823
|
-
controlSchema: controls.controls,
|
|
4824
|
-
controlValues,
|
|
4825
|
-
controlViewIds,
|
|
4826
|
-
controlViewItems,
|
|
4827
|
-
activeControlView,
|
|
4828
|
-
form,
|
|
4829
|
-
sidebar,
|
|
4830
|
-
topBar: topBarProps,
|
|
4831
|
-
pillNav,
|
|
4832
|
-
topBarSettings,
|
|
4833
|
-
bindings,
|
|
4834
|
-
renderer,
|
|
4835
|
-
workspace,
|
|
4836
|
-
componentBindings,
|
|
4837
|
-
componentBindingsById,
|
|
4838
|
-
componentImports,
|
|
4839
|
-
componentProps,
|
|
4840
|
-
componentPropsById,
|
|
4841
|
-
componentPropsByComponent,
|
|
4842
|
-
componentSnippets,
|
|
4843
|
-
componentUsage,
|
|
4844
|
-
getComponentProps,
|
|
4845
|
-
setActiveControlView,
|
|
4846
|
-
setControlValues,
|
|
4847
|
-
updateControlValues,
|
|
4848
|
-
applyCollection,
|
|
4849
|
-
resetToDefaultCollection,
|
|
4850
|
-
save: templateCollection.save,
|
|
4851
|
-
saveCurrent: templateCollection.saveCurrent
|
|
4852
|
-
};
|
|
4853
|
-
}
|
|
4854
|
-
function humanizePresetId(value) {
|
|
4855
|
-
return value.replace(/[-_]+/g, " ").replace(/\b\w/g, (match) => match.toUpperCase());
|
|
4856
|
-
}
|
|
4857
|
-
function mergeInitialValues(controlOptions, initialValues) {
|
|
4858
|
-
if (initialValues === void 0) return controlOptions;
|
|
4859
|
-
return {
|
|
4860
|
-
...controlOptions ?? {},
|
|
4861
|
-
initialValues: {
|
|
4862
|
-
...controlOptions?.initialValues ?? {},
|
|
4863
|
-
...initialValues
|
|
4864
|
-
}
|
|
4865
|
-
};
|
|
4866
|
-
}
|
|
4867
|
-
//#endregion
|
|
4868
|
-
//#region src/composables/useRackEditor.ts
|
|
4869
|
-
var SLOT_CYCLE = [
|
|
4870
|
-
"R",
|
|
4871
|
-
"G",
|
|
4872
|
-
"B",
|
|
4873
|
-
"Y"
|
|
4874
|
-
];
|
|
4875
|
-
var globalIdCounter = 0;
|
|
4876
|
-
function generateId() {
|
|
4877
|
-
globalIdCounter++;
|
|
4878
|
-
return `rack-${Date.now()}-${globalIdCounter}`;
|
|
4879
|
-
}
|
|
4880
|
-
/** Manages a set of sample racks with well-level assignment, series fill, and multi-rack reordering. */
|
|
4881
|
-
function useRackEditor(initialRacks, options) {
|
|
4882
|
-
const defaultFormat = options?.defaultFormat ?? 54;
|
|
4883
|
-
const defaultInjVol = options?.defaultInjectionVolume ?? 5;
|
|
4884
|
-
const maxRacks = options?.maxRacks ?? 10;
|
|
4885
|
-
const minRacks = options?.minRacks ?? 1;
|
|
4886
|
-
function createDefaultRack(name, slotIndex) {
|
|
4887
|
-
const slot = SLOT_CYCLE[(slotIndex ?? 0) % SLOT_CYCLE.length];
|
|
4888
|
-
return {
|
|
4889
|
-
id: generateId(),
|
|
4890
|
-
name: name ?? `Rack ${racks.value.length + 1}`,
|
|
4891
|
-
format: defaultFormat,
|
|
4892
|
-
slot,
|
|
4893
|
-
injectionVolume: defaultInjVol,
|
|
4894
|
-
wells: {}
|
|
4895
|
-
};
|
|
4896
|
-
}
|
|
4897
|
-
const racks = ref(initialRacks && initialRacks.length > 0 ? initialRacks.map((r) => ({ ...r })) : [createDefaultRack("Rack 1", 0)]);
|
|
4898
|
-
const activeRackId = ref(racks.value[0]?.id ?? "");
|
|
4899
|
-
const activeRack = computed(() => racks.value.find((r) => r.id === activeRackId.value) ?? racks.value[0]);
|
|
4900
|
-
function addRack(name) {
|
|
4901
|
-
if (racks.value.length >= maxRacks) return racks.value[racks.value.length - 1];
|
|
4902
|
-
const rack = createDefaultRack(name, racks.value.length);
|
|
4903
|
-
racks.value.push(rack);
|
|
4904
|
-
activeRackId.value = rack.id;
|
|
4905
|
-
return rack;
|
|
4906
|
-
}
|
|
4907
|
-
function removeRack(rackId) {
|
|
4908
|
-
if (racks.value.length <= minRacks) return;
|
|
4909
|
-
const index = racks.value.findIndex((r) => r.id === rackId);
|
|
4910
|
-
if (index === -1) return;
|
|
4911
|
-
racks.value.splice(index, 1);
|
|
4912
|
-
if (activeRackId.value === rackId && racks.value[0]) activeRackId.value = racks.value[0].id;
|
|
4913
|
-
}
|
|
4914
|
-
function reorderRacks(fromIndex, toIndex) {
|
|
4915
|
-
if (fromIndex < 0 || fromIndex >= racks.value.length) return;
|
|
4916
|
-
if (toIndex < 0 || toIndex >= racks.value.length) return;
|
|
4917
|
-
if (fromIndex === toIndex) return;
|
|
4918
|
-
const [moved] = racks.value.splice(fromIndex, 1);
|
|
4919
|
-
if (moved) racks.value.splice(toIndex, 0, moved);
|
|
4920
|
-
}
|
|
4921
|
-
function updateRack(rackId, data) {
|
|
4922
|
-
const rack = racks.value.find((r) => r.id === rackId);
|
|
4923
|
-
if (!rack) return;
|
|
4924
|
-
if (data.name !== void 0) rack.name = data.name;
|
|
4925
|
-
if (data.format !== void 0) rack.format = data.format;
|
|
4926
|
-
if (data.slot !== void 0) rack.slot = data.slot;
|
|
4927
|
-
if (data.injectionVolume !== void 0) rack.injectionVolume = data.injectionVolume;
|
|
4928
|
-
if (data.wells !== void 0) rack.wells = data.wells;
|
|
4929
|
-
}
|
|
4930
|
-
function setActiveRack(rackId) {
|
|
4931
|
-
if (racks.value.some((r) => r.id === rackId)) activeRackId.value = rackId;
|
|
4932
|
-
}
|
|
4933
|
-
function setWellData(rackId, wellId, data) {
|
|
4934
|
-
const rack = racks.value.find((r) => r.id === rackId);
|
|
4935
|
-
if (!rack) return;
|
|
4936
|
-
rack.wells[wellId] = {
|
|
4937
|
-
...rack.wells[wellId],
|
|
4938
|
-
...data
|
|
4939
|
-
};
|
|
4940
|
-
}
|
|
4941
|
-
function clearWell(rackId, wellId) {
|
|
4942
|
-
const rack = racks.value.find((r) => r.id === rackId);
|
|
4943
|
-
if (!rack) return;
|
|
4944
|
-
delete rack.wells[wellId];
|
|
4945
|
-
}
|
|
4946
|
-
function clearAllWells(rackId) {
|
|
4947
|
-
const rack = racks.value.find((r) => r.id === rackId);
|
|
4948
|
-
if (!rack) return;
|
|
4949
|
-
rack.wells = {};
|
|
4950
|
-
}
|
|
4951
|
-
function fillSeries(rackId, prefix = "S") {
|
|
4952
|
-
const rack = racks.value.find((r) => r.id === rackId);
|
|
4953
|
-
if (!rack) return;
|
|
4954
|
-
const config = PLATE_CONFIGS[rack.format];
|
|
4955
|
-
if (!config) return;
|
|
4956
|
-
let counter = 1;
|
|
4957
|
-
for (let row = 0; row < config.rows; row++) for (let col = 0; col < config.cols; col++) {
|
|
4958
|
-
const wellId = `${rowLabel(row)}${col + 1}`;
|
|
4959
|
-
if (!rack.wells[wellId]?.sampleType) {
|
|
4960
|
-
const paddedNum = String(counter).padStart(3, "0");
|
|
4961
|
-
rack.wells[wellId] = {
|
|
4962
|
-
id: wellId,
|
|
4963
|
-
row,
|
|
4964
|
-
col,
|
|
4965
|
-
state: "filled",
|
|
4966
|
-
sampleType: "sample",
|
|
4967
|
-
metadata: {
|
|
4968
|
-
label: `${prefix}${paddedNum}`,
|
|
4969
|
-
injectionVolume: rack.injectionVolume,
|
|
4970
|
-
injectionCount: 1
|
|
4971
|
-
}
|
|
4972
|
-
};
|
|
4973
|
-
}
|
|
4974
|
-
counter++;
|
|
4975
|
-
}
|
|
6615
|
+
function setActiveControlView(viewId) {
|
|
6616
|
+
if (!controlViewIds.value.includes(viewId)) return;
|
|
6617
|
+
activeControlView.value = viewId;
|
|
4976
6618
|
}
|
|
4977
|
-
function
|
|
4978
|
-
const
|
|
4979
|
-
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
6619
|
+
function setControlValues(values) {
|
|
6620
|
+
const nextValues = { ...values };
|
|
6621
|
+
controlValues.value = nextValues;
|
|
6622
|
+
syncControlValueBindings(nextValues);
|
|
6623
|
+
return templateCollection.apply(createBioTemplatePresetCollectionFromControls(presetId, nextValues));
|
|
6624
|
+
}
|
|
6625
|
+
function updateControlValues(values) {
|
|
6626
|
+
return setControlValues({
|
|
6627
|
+
...controlValues.value,
|
|
6628
|
+
...values
|
|
4983
6629
|
});
|
|
4984
|
-
return result;
|
|
4985
6630
|
}
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
4991
|
-
|
|
4992
|
-
|
|
4993
|
-
|
|
4994
|
-
|
|
6631
|
+
function resetToDefaultCollection() {
|
|
6632
|
+
return setControlValues({ ...controls.initialValues });
|
|
6633
|
+
}
|
|
6634
|
+
function syncControlValueBindings(values) {
|
|
6635
|
+
form.modelValue = values;
|
|
6636
|
+
sidebar.modelValue = values;
|
|
6637
|
+
sidebar.values = values;
|
|
6638
|
+
topBarSettings.settingsConfig = {
|
|
6639
|
+
...topBarSettings.settingsConfig,
|
|
6640
|
+
values
|
|
6641
|
+
};
|
|
4995
6642
|
}
|
|
4996
6643
|
return {
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
|
|
5001
|
-
|
|
5002
|
-
|
|
5003
|
-
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
|
|
5008
|
-
|
|
5009
|
-
|
|
5010
|
-
|
|
5011
|
-
|
|
6644
|
+
presetId,
|
|
6645
|
+
presetInfo,
|
|
6646
|
+
presetLabel,
|
|
6647
|
+
collection,
|
|
6648
|
+
templates: templateCollection.templates,
|
|
6649
|
+
templateIds: templateCollection.templateIds,
|
|
6650
|
+
currentExperimentId: templateCollection.currentExperimentId,
|
|
6651
|
+
hasCurrentExperiment: templateCollection.hasCurrentExperiment,
|
|
6652
|
+
saving: templateCollection.isSaving,
|
|
6653
|
+
error: templateCollection.error,
|
|
6654
|
+
lastSavedAt: templateCollection.lastSavedAt,
|
|
6655
|
+
controls,
|
|
6656
|
+
controlSchema: controls.controls,
|
|
6657
|
+
controlValues,
|
|
6658
|
+
controlViewIds,
|
|
6659
|
+
controlViewItems,
|
|
6660
|
+
activeControlView,
|
|
6661
|
+
form,
|
|
6662
|
+
sidebar,
|
|
6663
|
+
topBar: topBarProps,
|
|
6664
|
+
pillNav,
|
|
6665
|
+
topBarSettings,
|
|
6666
|
+
bindings,
|
|
6667
|
+
renderer,
|
|
6668
|
+
workspace,
|
|
6669
|
+
componentBindings,
|
|
6670
|
+
componentBindingsById,
|
|
6671
|
+
componentImports,
|
|
6672
|
+
componentProps,
|
|
6673
|
+
componentPropsById,
|
|
6674
|
+
componentPropsByComponent,
|
|
6675
|
+
componentSnippets,
|
|
6676
|
+
componentUsage,
|
|
6677
|
+
getComponentProps,
|
|
6678
|
+
setActiveControlView,
|
|
6679
|
+
setControlValues,
|
|
6680
|
+
updateControlValues,
|
|
6681
|
+
applyCollection,
|
|
6682
|
+
resetToDefaultCollection,
|
|
6683
|
+
save: templateCollection.save,
|
|
6684
|
+
saveCurrent: templateCollection.saveCurrent
|
|
6685
|
+
};
|
|
6686
|
+
}
|
|
6687
|
+
function humanizePresetId(value) {
|
|
6688
|
+
return value.replace(/[-_]+/g, " ").replace(/\b\w/g, (match) => match.toUpperCase());
|
|
6689
|
+
}
|
|
6690
|
+
function mergeInitialValues(controlOptions, initialValues) {
|
|
6691
|
+
if (initialValues === void 0) return controlOptions;
|
|
6692
|
+
return {
|
|
6693
|
+
...controlOptions ?? {},
|
|
6694
|
+
initialValues: {
|
|
6695
|
+
...controlOptions?.initialValues ?? {},
|
|
6696
|
+
...initialValues
|
|
6697
|
+
}
|
|
5012
6698
|
};
|
|
5013
6699
|
}
|
|
5014
6700
|
//#endregion
|
|
5015
6701
|
//#region src/composables/useGroupAssignment.ts
|
|
5016
|
-
/**
|
|
6702
|
+
/**
|
|
6703
|
+
* Shared two-zone group assignment model for control/treatment style workflows.
|
|
6704
|
+
*
|
|
6705
|
+
* @deprecated Scheduled for removal in MINT 1.4; no SDK replacement.
|
|
6706
|
+
*/
|
|
5017
6707
|
function useGroupAssignment(options) {
|
|
5018
6708
|
const groups = computed(() => [...toValue(options.groups)]);
|
|
5019
6709
|
const group1 = computed(() => [...toValue(options.group1)]);
|
|
@@ -5619,6 +7309,510 @@ function useProtocolTemplates() {
|
|
|
5619
7309
|
};
|
|
5620
7310
|
}
|
|
5621
7311
|
//#endregion
|
|
7312
|
+
//#region src/composables/useDebouncedWatch.ts
|
|
7313
|
+
/** Watch a Vue source with debounced callback execution and explicit cancel/flush controls. */
|
|
7314
|
+
function useDebouncedWatch(source, callback, options = {}) {
|
|
7315
|
+
const { delay = 300, ...watchOptions } = options;
|
|
7316
|
+
const isPending = ref(false);
|
|
7317
|
+
let timer = null;
|
|
7318
|
+
let hasLatestValue = false;
|
|
7319
|
+
let latestValue;
|
|
7320
|
+
let latestOldValue;
|
|
7321
|
+
let callbackCleanup = null;
|
|
7322
|
+
let stopped = false;
|
|
7323
|
+
function clearTimer() {
|
|
7324
|
+
if (timer) {
|
|
7325
|
+
clearTimeout(timer);
|
|
7326
|
+
timer = null;
|
|
7327
|
+
}
|
|
7328
|
+
isPending.value = false;
|
|
7329
|
+
}
|
|
7330
|
+
function runCallbackCleanup() {
|
|
7331
|
+
if (!callbackCleanup) return;
|
|
7332
|
+
callbackCleanup();
|
|
7333
|
+
callbackCleanup = null;
|
|
7334
|
+
}
|
|
7335
|
+
function cancel() {
|
|
7336
|
+
clearTimer();
|
|
7337
|
+
hasLatestValue = false;
|
|
7338
|
+
latestOldValue = void 0;
|
|
7339
|
+
}
|
|
7340
|
+
function flush() {
|
|
7341
|
+
if (!hasLatestValue) return;
|
|
7342
|
+
const value = latestValue;
|
|
7343
|
+
const oldValue = latestOldValue;
|
|
7344
|
+
cancel();
|
|
7345
|
+
runCallbackCleanup();
|
|
7346
|
+
callback(value, oldValue, (cleanup) => {
|
|
7347
|
+
callbackCleanup = cleanup;
|
|
7348
|
+
});
|
|
7349
|
+
}
|
|
7350
|
+
function schedule(value, oldValue) {
|
|
7351
|
+
cancel();
|
|
7352
|
+
hasLatestValue = true;
|
|
7353
|
+
latestValue = value;
|
|
7354
|
+
latestOldValue = oldValue;
|
|
7355
|
+
isPending.value = true;
|
|
7356
|
+
timer = setTimeout(flush, delay);
|
|
7357
|
+
}
|
|
7358
|
+
const stopWatch = watch(source, (value, oldValue, onCleanup) => {
|
|
7359
|
+
schedule(value, oldValue);
|
|
7360
|
+
onCleanup(() => {
|
|
7361
|
+
cancel();
|
|
7362
|
+
runCallbackCleanup();
|
|
7363
|
+
});
|
|
7364
|
+
}, watchOptions);
|
|
7365
|
+
function stop() {
|
|
7366
|
+
if (stopped) return;
|
|
7367
|
+
stopped = true;
|
|
7368
|
+
stopWatch();
|
|
7369
|
+
cancel();
|
|
7370
|
+
runCallbackCleanup();
|
|
7371
|
+
}
|
|
7372
|
+
onScopeDispose(stop);
|
|
7373
|
+
return {
|
|
7374
|
+
isPending,
|
|
7375
|
+
cancel,
|
|
7376
|
+
flush,
|
|
7377
|
+
stop
|
|
7378
|
+
};
|
|
7379
|
+
}
|
|
7380
|
+
//#endregion
|
|
7381
|
+
//#region src/composables/useExperimentSelector.ts
|
|
7382
|
+
function getPlatformContext() {
|
|
7383
|
+
if (typeof window === "undefined") return void 0;
|
|
7384
|
+
return window.__MINT_PLATFORM__;
|
|
7385
|
+
}
|
|
7386
|
+
function getPlatformApiUrl() {
|
|
7387
|
+
return getPlatformContext()?.platformApiUrl;
|
|
7388
|
+
}
|
|
7389
|
+
/** Fetches a paginated, filtered experiment list from the platform API for picker and selector UIs. */
|
|
7390
|
+
function useExperimentSelector(options = {}) {
|
|
7391
|
+
const { limit = 100, immediate = false, experimentType, allowedExperimentTypes: optionAllowedExperimentTypes, apiBaseUrl } = options;
|
|
7392
|
+
const platformBase = apiBaseUrl ?? getPlatformApiUrl();
|
|
7393
|
+
const api = useApi();
|
|
7394
|
+
const experiments = ref([]);
|
|
7395
|
+
const total = ref(0);
|
|
7396
|
+
const selectedExperiment = ref(null);
|
|
7397
|
+
const request = useRequestSyncState("Failed to fetch experiments");
|
|
7398
|
+
const isLoading = request.loading;
|
|
7399
|
+
const error = request.error;
|
|
7400
|
+
const lastLoadedAt = request.lastLoadedAt;
|
|
7401
|
+
const page = ref(0);
|
|
7402
|
+
let fetchGeneration = 0;
|
|
7403
|
+
const sortKey = ref("created_at:desc");
|
|
7404
|
+
const experimentTypes = ref([]);
|
|
7405
|
+
const projects = ref([]);
|
|
7406
|
+
let filterOptionsFetched = false;
|
|
7407
|
+
const hasMore = computed(() => experiments.value.length < total.value);
|
|
7408
|
+
const filters = reactive({
|
|
7409
|
+
search: void 0,
|
|
7410
|
+
status: void 0,
|
|
7411
|
+
project: void 0,
|
|
7412
|
+
experimentType: void 0,
|
|
7413
|
+
datePreset: void 0
|
|
7414
|
+
});
|
|
7415
|
+
function normalizeExperimentTypes(values) {
|
|
7416
|
+
if (values == null) return null;
|
|
7417
|
+
return [...new Set(values.filter((value) => typeof value === "string" && value.length > 0))];
|
|
7418
|
+
}
|
|
7419
|
+
function currentAllowedExperimentTypes() {
|
|
7420
|
+
if (optionAllowedExperimentTypes !== void 0) return normalizeExperimentTypes(optionAllowedExperimentTypes);
|
|
7421
|
+
return normalizeExperimentTypes(getPlatformContext()?.allowedExperimentTypes);
|
|
7422
|
+
}
|
|
7423
|
+
function parseSortKey() {
|
|
7424
|
+
const [field, order] = sortKey.value.split(":");
|
|
7425
|
+
return {
|
|
7426
|
+
sortBy: field || "created_at",
|
|
7427
|
+
sortOrder: order || "desc"
|
|
7428
|
+
};
|
|
7429
|
+
}
|
|
7430
|
+
function resolveExperimentTypeScope() {
|
|
7431
|
+
const allowedTypes = currentAllowedExperimentTypes();
|
|
7432
|
+
const selectedType = experimentType ?? filters.experimentType ?? void 0;
|
|
7433
|
+
if (selectedType) {
|
|
7434
|
+
if (allowedTypes && !allowedTypes.includes(selectedType)) return { kind: "blocked" };
|
|
7435
|
+
return {
|
|
7436
|
+
kind: "single",
|
|
7437
|
+
experimentType: selectedType
|
|
7438
|
+
};
|
|
7439
|
+
}
|
|
7440
|
+
if (allowedTypes) {
|
|
7441
|
+
if (allowedTypes.length === 0) return { kind: "blocked" };
|
|
7442
|
+
if (allowedTypes.length === 1) return {
|
|
7443
|
+
kind: "single",
|
|
7444
|
+
experimentType: allowedTypes[0]
|
|
7445
|
+
};
|
|
7446
|
+
return {
|
|
7447
|
+
kind: "multi",
|
|
7448
|
+
experimentTypes: allowedTypes
|
|
7449
|
+
};
|
|
7450
|
+
}
|
|
7451
|
+
return { kind: "all" };
|
|
7452
|
+
}
|
|
7453
|
+
function buildExperimentListUrl(typeFilter, skip, pageLimit) {
|
|
7454
|
+
const params = new URLSearchParams();
|
|
7455
|
+
if (typeFilter) params.set("experiment_type", typeFilter);
|
|
7456
|
+
if (filters.status) params.set("status", filters.status);
|
|
7457
|
+
if (filters.search) params.set("search", filters.search);
|
|
7458
|
+
if (filters.project) params.set("project", filters.project);
|
|
7459
|
+
const { sortBy, sortOrder } = parseSortKey();
|
|
7460
|
+
params.set("sort_by", sortBy);
|
|
7461
|
+
params.set("sort_order", sortOrder);
|
|
7462
|
+
if (filters.datePreset) params.set("created_after", datePresetToISO(filters.datePreset));
|
|
7463
|
+
params.set("limit", String(pageLimit));
|
|
7464
|
+
params.set("skip", String(skip));
|
|
7465
|
+
const query = params.toString();
|
|
7466
|
+
return `${platformBase ?? ""}/experiments${query ? `?${query}` : ""}`;
|
|
7467
|
+
}
|
|
7468
|
+
function sortExperiments(items) {
|
|
7469
|
+
const { sortBy, sortOrder } = parseSortKey();
|
|
7470
|
+
const direction = sortOrder === "desc" ? -1 : 1;
|
|
7471
|
+
return [...items].sort((a, b) => {
|
|
7472
|
+
const aValue = a[sortBy] ?? "";
|
|
7473
|
+
const bValue = b[sortBy] ?? "";
|
|
7474
|
+
if (aValue < bValue) return -1 * direction;
|
|
7475
|
+
if (aValue > bValue) return 1 * direction;
|
|
7476
|
+
return 0;
|
|
7477
|
+
});
|
|
7478
|
+
}
|
|
7479
|
+
async function fetchExperimentList(typeFilter, skip, pageLimit) {
|
|
7480
|
+
return await api.get(buildExperimentListUrl(typeFilter, skip, pageLimit));
|
|
7481
|
+
}
|
|
7482
|
+
async function fetchScopedExperimentPage(requestedPage) {
|
|
7483
|
+
const scope = resolveExperimentTypeScope();
|
|
7484
|
+
const pageSkip = requestedPage * limit;
|
|
7485
|
+
if (scope.kind === "blocked") return {
|
|
7486
|
+
experiments: [],
|
|
7487
|
+
total: 0
|
|
7488
|
+
};
|
|
7489
|
+
if (scope.kind === "multi") {
|
|
7490
|
+
const perTypeLimit = pageSkip + limit;
|
|
7491
|
+
const responses = await Promise.all(scope.experimentTypes.map((type) => fetchExperimentList(type, 0, perTypeLimit)));
|
|
7492
|
+
return {
|
|
7493
|
+
experiments: sortExperiments(responses.flatMap((response) => response.experiments)).slice(pageSkip, pageSkip + limit),
|
|
7494
|
+
total: responses.reduce((sum, response) => sum + response.total, 0)
|
|
7495
|
+
};
|
|
7496
|
+
}
|
|
7497
|
+
return await fetchExperimentList(scope.kind === "single" ? scope.experimentType : void 0, pageSkip, limit);
|
|
7498
|
+
}
|
|
7499
|
+
async function fetchExperiments() {
|
|
7500
|
+
const generation = ++fetchGeneration;
|
|
7501
|
+
const requestedPage = page.value;
|
|
7502
|
+
isLoading.value = true;
|
|
7503
|
+
request.clearError();
|
|
7504
|
+
try {
|
|
7505
|
+
const data = await fetchScopedExperimentPage(requestedPage);
|
|
7506
|
+
if (generation !== fetchGeneration) return;
|
|
7507
|
+
if (requestedPage === 0) experiments.value = data.experiments;
|
|
7508
|
+
else experiments.value = [...experiments.value, ...data.experiments];
|
|
7509
|
+
total.value = data.total;
|
|
7510
|
+
request.markLoaded();
|
|
7511
|
+
} catch (err) {
|
|
7512
|
+
if (generation !== fetchGeneration) return;
|
|
7513
|
+
request.setError(err, "Failed to fetch experiments");
|
|
7514
|
+
if (requestedPage === 0) {
|
|
7515
|
+
experiments.value = [];
|
|
7516
|
+
total.value = 0;
|
|
7517
|
+
}
|
|
7518
|
+
} finally {
|
|
7519
|
+
if (generation === fetchGeneration) isLoading.value = false;
|
|
7520
|
+
}
|
|
7521
|
+
}
|
|
7522
|
+
function invalidateFetch() {
|
|
7523
|
+
fetchGeneration += 1;
|
|
7524
|
+
isLoading.value = false;
|
|
7525
|
+
}
|
|
7526
|
+
async function fetchFilterOptions() {
|
|
7527
|
+
if (filterOptionsFetched) return;
|
|
7528
|
+
filterOptionsFetched = true;
|
|
7529
|
+
const base = platformBase ?? "";
|
|
7530
|
+
const [typesRes, projectsRes] = await Promise.allSettled([api.get(`${base}/experiments/experiment-types`), api.get(`${base}/projects`)]);
|
|
7531
|
+
if (typesRes.status === "fulfilled" && Array.isArray(typesRes.value)) {
|
|
7532
|
+
const allowedTypes = currentAllowedExperimentTypes();
|
|
7533
|
+
const allowedSet = allowedTypes ? new Set(allowedTypes) : null;
|
|
7534
|
+
experimentTypes.value = typesRes.value.filter((t) => !allowedSet || allowedSet.has(t.value)).map((t) => ({
|
|
7535
|
+
value: t.value,
|
|
7536
|
+
label: t.label,
|
|
7537
|
+
color: t.color
|
|
7538
|
+
}));
|
|
7539
|
+
if (filters.experimentType && allowedSet && !allowedSet.has(filters.experimentType)) filters.experimentType = void 0;
|
|
7540
|
+
}
|
|
7541
|
+
if (projectsRes.status === "fulfilled" && projectsRes.value?.projects && Array.isArray(projectsRes.value.projects)) projects.value = projectsRes.value.projects.map((p) => ({
|
|
7542
|
+
value: p.name,
|
|
7543
|
+
label: p.name
|
|
7544
|
+
}));
|
|
7545
|
+
}
|
|
7546
|
+
async function loadMore() {
|
|
7547
|
+
if (!hasMore.value || isLoading.value) return;
|
|
7548
|
+
page.value++;
|
|
7549
|
+
await fetchExperiments();
|
|
7550
|
+
}
|
|
7551
|
+
function reset() {
|
|
7552
|
+
page.value = 0;
|
|
7553
|
+
experiments.value = [];
|
|
7554
|
+
total.value = 0;
|
|
7555
|
+
fetchExperiments();
|
|
7556
|
+
}
|
|
7557
|
+
function select(experiment) {
|
|
7558
|
+
selectedExperiment.value = experiment;
|
|
7559
|
+
}
|
|
7560
|
+
function clear() {
|
|
7561
|
+
invalidateFetch();
|
|
7562
|
+
selectedExperiment.value = null;
|
|
7563
|
+
filters.search = void 0;
|
|
7564
|
+
filters.status = void 0;
|
|
7565
|
+
filters.project = void 0;
|
|
7566
|
+
filters.experimentType = void 0;
|
|
7567
|
+
filters.datePreset = void 0;
|
|
7568
|
+
sortKey.value = "created_at:desc";
|
|
7569
|
+
page.value = 0;
|
|
7570
|
+
request.clearError();
|
|
7571
|
+
}
|
|
7572
|
+
const groupedByProject = computed(() => {
|
|
7573
|
+
const groups = /* @__PURE__ */ new Map();
|
|
7574
|
+
for (const exp of experiments.value) {
|
|
7575
|
+
const key = exp.project_name ?? exp.project ?? "No project";
|
|
7576
|
+
const list = groups.get(key);
|
|
7577
|
+
if (list) list.push(exp);
|
|
7578
|
+
else groups.set(key, [exp]);
|
|
7579
|
+
}
|
|
7580
|
+
return [...groups.entries()].sort(([a], [b]) => {
|
|
7581
|
+
if (a === "No project") return 1;
|
|
7582
|
+
if (b === "No project") return -1;
|
|
7583
|
+
return a.localeCompare(b);
|
|
7584
|
+
});
|
|
7585
|
+
});
|
|
7586
|
+
watch(() => [
|
|
7587
|
+
filters.search,
|
|
7588
|
+
filters.status,
|
|
7589
|
+
filters.project,
|
|
7590
|
+
filters.experimentType,
|
|
7591
|
+
filters.datePreset,
|
|
7592
|
+
sortKey.value
|
|
7593
|
+
], () => invalidateFetch(), { flush: "sync" });
|
|
7594
|
+
const searchWatch = useDebouncedWatch(() => filters.search, () => {
|
|
7595
|
+
page.value = 0;
|
|
7596
|
+
fetchExperiments();
|
|
7597
|
+
}, { delay: 300 });
|
|
7598
|
+
watch(() => [
|
|
7599
|
+
filters.status,
|
|
7600
|
+
filters.project,
|
|
7601
|
+
filters.experimentType,
|
|
7602
|
+
filters.datePreset,
|
|
7603
|
+
sortKey.value
|
|
7604
|
+
], () => {
|
|
7605
|
+
searchWatch.cancel();
|
|
7606
|
+
page.value = 0;
|
|
7607
|
+
fetchExperiments();
|
|
7608
|
+
});
|
|
7609
|
+
if (immediate) fetchExperiments();
|
|
7610
|
+
return {
|
|
7611
|
+
experiments,
|
|
7612
|
+
total,
|
|
7613
|
+
selectedExperiment,
|
|
7614
|
+
filters,
|
|
7615
|
+
isLoading,
|
|
7616
|
+
error,
|
|
7617
|
+
lastLoadedAt,
|
|
7618
|
+
page,
|
|
7619
|
+
hasMore,
|
|
7620
|
+
sortKey,
|
|
7621
|
+
experimentTypes,
|
|
7622
|
+
projects,
|
|
7623
|
+
groupedByProject,
|
|
7624
|
+
fetch: fetchExperiments,
|
|
7625
|
+
loadMore,
|
|
7626
|
+
reset,
|
|
7627
|
+
select,
|
|
7628
|
+
clear,
|
|
7629
|
+
fetchFilterOptions
|
|
7630
|
+
};
|
|
7631
|
+
}
|
|
7632
|
+
//#endregion
|
|
7633
|
+
//#region src/composables/useAnalysisArtifacts.ts
|
|
7634
|
+
/**
|
|
7635
|
+
* Lists and manages analysis artifacts through the user-facing platform API.
|
|
7636
|
+
*
|
|
7637
|
+
* @example Recommended
|
|
7638
|
+
* ```ts
|
|
7639
|
+
* const artifacts = useAnalysisArtifacts({ pluginId: 'LEAF' })
|
|
7640
|
+
* await artifacts.list()
|
|
7641
|
+
* ```
|
|
7642
|
+
*/
|
|
7643
|
+
function useAnalysisArtifacts(options = {}) {
|
|
7644
|
+
const injectedContext = getInjectedPlatformContext();
|
|
7645
|
+
const api = useApi({ baseUrl: options.apiBaseUrl ?? injectedContext?.platformApiUrl });
|
|
7646
|
+
const currentExperimentId = computed(() => toValue(options.experimentId) ?? resolveCurrentExperimentId(injectedContext));
|
|
7647
|
+
const hasCurrentExperiment = computed(() => currentExperimentId.value !== void 0);
|
|
7648
|
+
const scopePluginId = computed(() => toValue(options.pluginId) ?? null);
|
|
7649
|
+
const request = useRequestSyncState("Analysis artifact request failed.");
|
|
7650
|
+
const fetchedArtifacts = ref([]);
|
|
7651
|
+
const selectedDetail = ref(null);
|
|
7652
|
+
const loadingCount = ref(0);
|
|
7653
|
+
const mutatingCount = ref(0);
|
|
7654
|
+
const isLoading = computed(() => loadingCount.value > 0);
|
|
7655
|
+
const isMutating = computed(() => mutatingCount.value > 0);
|
|
7656
|
+
const search = ref("");
|
|
7657
|
+
const statusFilter = ref("active");
|
|
7658
|
+
const pluginFilter = ref(null);
|
|
7659
|
+
const needsArchived = computed(() => Boolean(toValue(options.includeArchived)) || statusFilter.value !== "active");
|
|
7660
|
+
const artifacts = computed(() => scopePluginId.value ? fetchedArtifacts.value.filter((artifact) => artifact.plugin_id === scopePluginId.value) : fetchedArtifacts.value);
|
|
7661
|
+
const visibleArtifacts = computed(() => {
|
|
7662
|
+
const query = search.value.trim().toLowerCase();
|
|
7663
|
+
return artifacts.value.filter((artifact) => {
|
|
7664
|
+
if (statusFilter.value !== "all" && artifact.status !== statusFilter.value) return false;
|
|
7665
|
+
if (pluginFilter.value && artifact.plugin_id !== pluginFilter.value) return false;
|
|
7666
|
+
if (!query) return true;
|
|
7667
|
+
return artifact.display_name.toLowerCase().includes(query) || artifact.artifact_key.toLowerCase().includes(query);
|
|
7668
|
+
});
|
|
7669
|
+
});
|
|
7670
|
+
let listToken = 0;
|
|
7671
|
+
let detailToken = 0;
|
|
7672
|
+
const hasRequestedList = ref(false);
|
|
7673
|
+
function currentExperimentIdOrError(action) {
|
|
7674
|
+
const id = currentExperimentId.value;
|
|
7675
|
+
if (id === void 0) {
|
|
7676
|
+
request.setError(`No current experiment is selected for ${action}`);
|
|
7677
|
+
return;
|
|
7678
|
+
}
|
|
7679
|
+
return id;
|
|
7680
|
+
}
|
|
7681
|
+
function replaceArtifact(summary) {
|
|
7682
|
+
const index = fetchedArtifacts.value.findIndex((artifact) => artifact.id === summary.id);
|
|
7683
|
+
if (index >= 0) fetchedArtifacts.value.splice(index, 1, summary);
|
|
7684
|
+
}
|
|
7685
|
+
async function list() {
|
|
7686
|
+
const experimentId = currentExperimentIdOrError("listing analysis artifacts");
|
|
7687
|
+
if (experimentId === void 0) {
|
|
7688
|
+
fetchedArtifacts.value = [];
|
|
7689
|
+
return [];
|
|
7690
|
+
}
|
|
7691
|
+
const token = ++listToken;
|
|
7692
|
+
hasRequestedList.value = true;
|
|
7693
|
+
loadingCount.value += 1;
|
|
7694
|
+
request.clearError();
|
|
7695
|
+
try {
|
|
7696
|
+
const data = await api.get(`/experiments/${experimentId}/analysis-artifacts?include_archived=${needsArchived.value}`);
|
|
7697
|
+
if (token !== listToken) return artifacts.value;
|
|
7698
|
+
fetchedArtifacts.value = [...data?.artifacts ?? []];
|
|
7699
|
+
request.markLoaded();
|
|
7700
|
+
return artifacts.value;
|
|
7701
|
+
} catch (err) {
|
|
7702
|
+
if (token !== listToken) return [];
|
|
7703
|
+
request.setError(err);
|
|
7704
|
+
fetchedArtifacts.value = [];
|
|
7705
|
+
return [];
|
|
7706
|
+
} finally {
|
|
7707
|
+
loadingCount.value -= 1;
|
|
7708
|
+
}
|
|
7709
|
+
}
|
|
7710
|
+
async function getDetail(artifactId) {
|
|
7711
|
+
const experimentId = currentExperimentIdOrError("loading an analysis artifact");
|
|
7712
|
+
if (experimentId === void 0) return null;
|
|
7713
|
+
const token = ++detailToken;
|
|
7714
|
+
loadingCount.value += 1;
|
|
7715
|
+
request.clearError();
|
|
7716
|
+
try {
|
|
7717
|
+
const detail = await api.get(`/experiments/${experimentId}/analysis-artifacts/${artifactId}`);
|
|
7718
|
+
if (token !== detailToken) return null;
|
|
7719
|
+
selectedDetail.value = detail;
|
|
7720
|
+
request.markLoaded();
|
|
7721
|
+
return detail;
|
|
7722
|
+
} catch (err) {
|
|
7723
|
+
if (token !== detailToken) return null;
|
|
7724
|
+
request.setError(err);
|
|
7725
|
+
return null;
|
|
7726
|
+
} finally {
|
|
7727
|
+
loadingCount.value -= 1;
|
|
7728
|
+
}
|
|
7729
|
+
}
|
|
7730
|
+
async function resolveTarget(target) {
|
|
7731
|
+
const experimentId = currentExperimentId.value;
|
|
7732
|
+
if (experimentId !== target.experimentId) {
|
|
7733
|
+
request.setError(`Analysis artifact target belongs to experiment ${target.experimentId}, not ${experimentId ?? "the current selection"}.`);
|
|
7734
|
+
return null;
|
|
7735
|
+
}
|
|
7736
|
+
const summary = (await list()).find((artifact) => artifact.plugin_id === target.artifactPluginId && artifact.artifact_key === target.artifactKey && artifact.status === "active");
|
|
7737
|
+
if (!summary) {
|
|
7738
|
+
request.setError(`Analysis artifact ${target.artifactPluginId}/${target.artifactKey} is not available.`);
|
|
7739
|
+
return null;
|
|
7740
|
+
}
|
|
7741
|
+
const detail = await getDetail(summary.id);
|
|
7742
|
+
if (detail && (detail.experiment_id !== target.experimentId || detail.plugin_id !== target.artifactPluginId || detail.artifact_key !== target.artifactKey)) {
|
|
7743
|
+
selectedDetail.value = null;
|
|
7744
|
+
request.setError(`Analysis artifact ${target.artifactPluginId}/${target.artifactKey} did not match the requested identity.`);
|
|
7745
|
+
return null;
|
|
7746
|
+
}
|
|
7747
|
+
return detail;
|
|
7748
|
+
}
|
|
7749
|
+
async function mutate(action, operation) {
|
|
7750
|
+
const experimentId = currentExperimentIdOrError(action);
|
|
7751
|
+
if (experimentId === void 0) return null;
|
|
7752
|
+
mutatingCount.value += 1;
|
|
7753
|
+
request.clearError();
|
|
7754
|
+
try {
|
|
7755
|
+
const summary = await operation(experimentId);
|
|
7756
|
+
replaceArtifact(summary);
|
|
7757
|
+
request.markSaved();
|
|
7758
|
+
return summary;
|
|
7759
|
+
} catch (err) {
|
|
7760
|
+
request.setError(err);
|
|
7761
|
+
return null;
|
|
7762
|
+
} finally {
|
|
7763
|
+
mutatingCount.value -= 1;
|
|
7764
|
+
}
|
|
7765
|
+
}
|
|
7766
|
+
async function updateMetadata(artifactId, updates) {
|
|
7767
|
+
return mutate("updating artifact metadata", (experimentId) => api.patch(`/experiments/${experimentId}/analysis-artifacts/${artifactId}`, updates));
|
|
7768
|
+
}
|
|
7769
|
+
async function archive(artifactId) {
|
|
7770
|
+
return mutate("archiving an analysis artifact", (experimentId) => api.post(`/experiments/${experimentId}/analysis-artifacts/${artifactId}/archive`));
|
|
7771
|
+
}
|
|
7772
|
+
async function restore(artifactId) {
|
|
7773
|
+
return mutate("restoring an analysis artifact", (experimentId) => api.post(`/experiments/${experimentId}/analysis-artifacts/${artifactId}/restore`));
|
|
7774
|
+
}
|
|
7775
|
+
function hasArtifactKey(artifactKey) {
|
|
7776
|
+
return artifacts.value.some((artifact) => artifact.artifact_key === artifactKey);
|
|
7777
|
+
}
|
|
7778
|
+
/** Drop experiment-scoped state and orphan every in-flight response. */
|
|
7779
|
+
function clear() {
|
|
7780
|
+
listToken += 1;
|
|
7781
|
+
detailToken += 1;
|
|
7782
|
+
fetchedArtifacts.value = [];
|
|
7783
|
+
selectedDetail.value = null;
|
|
7784
|
+
request.clearError();
|
|
7785
|
+
}
|
|
7786
|
+
watch([currentExperimentId, needsArchived], ([experimentId], [previousExperimentId]) => {
|
|
7787
|
+
if (experimentId !== previousExperimentId) clear();
|
|
7788
|
+
if (experimentId === void 0) return;
|
|
7789
|
+
if (options.immediate || hasRequestedList.value) list();
|
|
7790
|
+
}, { immediate: options.immediate });
|
|
7791
|
+
return {
|
|
7792
|
+
artifacts,
|
|
7793
|
+
visibleArtifacts,
|
|
7794
|
+
selectedDetail,
|
|
7795
|
+
currentExperimentId,
|
|
7796
|
+
hasCurrentExperiment,
|
|
7797
|
+
isLoading,
|
|
7798
|
+
isMutating,
|
|
7799
|
+
error: request.error,
|
|
7800
|
+
lastLoadedAt: request.lastLoadedAt,
|
|
7801
|
+
search,
|
|
7802
|
+
statusFilter,
|
|
7803
|
+
pluginFilter,
|
|
7804
|
+
list,
|
|
7805
|
+
refresh: list,
|
|
7806
|
+
getDetail,
|
|
7807
|
+
resolveTarget,
|
|
7808
|
+
updateMetadata,
|
|
7809
|
+
archive,
|
|
7810
|
+
restore,
|
|
7811
|
+
hasArtifactKey,
|
|
7812
|
+
clear
|
|
7813
|
+
};
|
|
7814
|
+
}
|
|
7815
|
+
//#endregion
|
|
5622
7816
|
//#region src/composables/filePicker/validation.ts
|
|
5623
7817
|
function normalizePickerFormat(format) {
|
|
5624
7818
|
return format.trim().replace(/^\./, "").toLowerCase();
|
|
@@ -5664,6 +7858,6 @@ function pickerFormat(name) {
|
|
|
5664
7858
|
return compound ? `${compound[1]}.gz` : name.includes(".") ? name.split(".").pop() : "";
|
|
5665
7859
|
}
|
|
5666
7860
|
//#endregion
|
|
5667
|
-
export {
|
|
7861
|
+
export { pluginFormDataFromPayload as $, parseDelimitedText as A, getPresenceClientId as At, useRackEditor as B, useRovingFocus as Bt, useExperimentSamples as C, controlValuesToComponentBindingsById as Ct, getSampleGroupMajorPrefix as D, useAppExperiment as Dt, useSampleGroups as E, APP_EXPERIMENT_KEY as Et, useAutoGroup as F, candidateMatchesSearch as Ft, useDoseCalculator as G, reorderList as H, extractSampleNamesFromDesignData as I, normalizeSearchQuery as It, usePluginJobCenter as J, combinePluginJobSources as K, extractSampleOptionsFromDesignData as L, useTextSearch as Lt, useFileImport as M, usePlatformContext as Mt, validateImportFile as N, useTheme as Nt, getSampleGroupParentPath as O, getInjectedFrontendRevision as Ot, extractSamplesFromDesignData as P, useToast as Pt, getPluginPageSelectorItems as Q, unwrapExperimentDesignData as R, compareSortValues as Rt, useScheduleDrag as S, controlValuesToComponentBindings as St, useExpansionSet as T, mergeControlWorkspaceOptions as Tt, useListReorder as U, REORDER_ANNOUNCER_STYLE as V, useReorderAnnouncer as W, downloadBlob as X, createPluginClient as Y, downloadPluginEndpoint as Z, toBioTemplateComponentPropsByComponent as _, defineWellPlateDoseComponentBindings as _t, useExperimentSelector as a, buildPluginEndpointUrl as at, useTemplateCollection as b, defineControlModel as bt, DEFAULT_PRESETS as c, usePluginWorkspace as ct, useReagentSeries as d, useFormBuilder as dt, uploadPluginEndpoint as et, useGroupAssignment as f, evaluateCondition as ft, getBioTemplateComponentProps as g, defineWellPlateControlProps as gt, useBioTemplateWorkspace as h, defineDoseDesignControlModel as ht, useAnalysisArtifacts as i, usePluginSettings as it, readFileAsText as j, usePresenceHeartbeat as jt, detectDelimiter as k, useRuntimeAlignment as kt, DEFAULT_UNITS as l, DEFAULT_MOBILE_VIEWPORT_QUERY as lt, useBioTemplatePackWorkspace as m, defineDoseCalculatorControlProps as mt, pickerFormat as n, usePluginClient as nt, useDebouncedWatch as o, resolvePluginBaseUrl as ot, useBioTemplatePresetWorkspace as p, useForm as pt, useJobsStatusTray as q, pickerReason as r, usePluginEventStream as rt, useProtocolTemplates as s, useRequestSyncState as st, pickerExclusion as t, useCurrentExperiment as tt, generateDilutionSeries as u, useMobileSupportGate as ut, useBioTemplateComponents as v, defineWellPlateDoseControlProps as vt, useExperimentData as w, controlValuesToComponentProps as wt, useExperimentSave as x, resolveControlModel as xt, useBioTemplateControls as y, useControlWorkspace as yt, DEFAULT_COLORS as z, useSortedItems as zt };
|
|
5668
7862
|
|
|
5669
|
-
//# sourceMappingURL=validation-
|
|
7863
|
+
//# sourceMappingURL=validation-Dmp9Q5W0.js.map
|