@morscherlab/mint-sdk 1.2.0-beta.2 → 1.2.0-beta.20
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 +65 -5
- package/dist/{ExperimentPopover-tLwJ7XF4.js → ExperimentPopover-CJdNlCZb.js} +24 -15
- package/dist/ExperimentPopover-CJdNlCZb.js.map +1 -0
- package/dist/{ExperimentSelectorModal-DyPAUCLw.js → ExperimentSelectorModal-DCXmwKNS.js} +5 -5
- package/dist/ExperimentSelectorModal-DCXmwKNS.js.map +1 -0
- package/dist/{SettingsModal-BgKY93zh.js → SettingsModal-BYR20I4c.js} +5 -8
- package/dist/SettingsModal-BYR20I4c.js.map +1 -0
- package/dist/{EmptyState-p90quq8Z.js → Skeleton-BAF3CKY7.js} +111 -93
- package/dist/Skeleton-BAF3CKY7.js.map +1 -0
- package/dist/__tests__/components/DataStates.test.d.ts +1 -0
- package/dist/__tests__/components/ExperimentTimeline.test.d.ts +1 -0
- package/dist/__tests__/components/FileBrowserModal.test.d.ts +1 -0
- package/dist/__tests__/components/FilePicker.state.test.d.ts +1 -0
- package/dist/__tests__/components/FilePicker.test.d.ts +1 -0
- package/dist/__tests__/components/PlotlyChart.test.d.ts +1 -0
- package/dist/__tests__/components/PluginWorkspaceView.internal.test.d.ts +1 -0
- package/dist/__tests__/components/SecretInput.test.d.ts +1 -0
- package/dist/__tests__/components/WellEditPopupInternal.test.d.ts +1 -0
- package/dist/__tests__/components/iconButtonIconSizing.test.d.ts +1 -0
- package/dist/__tests__/composables/useFileBrowser.test.d.ts +1 -0
- package/dist/__tests__/composables/useListReorder.test.d.ts +1 -0
- package/dist/__tests__/composables/usePlatformFilePickerAdapter.test.d.ts +1 -0
- package/dist/__tests__/composables/usePluginConfig.test.d.ts +15 -0
- package/dist/__tests__/styles/cssTokens.test.d.ts +1 -0
- package/dist/apiErrors.d.ts +26 -0
- package/dist/components/ActionMenu.vue.d.ts +1 -1
- package/dist/components/AppAvatarMenu.vue.d.ts +4 -0
- package/dist/components/AppPluginSwitcher.vue.d.ts +2 -0
- package/dist/components/AppSidebar.vue.d.ts +1 -1
- package/dist/components/AuditTrail.vue.d.ts +13 -0
- package/dist/components/AutoGroupModal.vue.d.ts +1 -1
- package/dist/components/BaseInput.vue.d.ts +1 -1
- package/dist/components/BaseSelect.vue.d.ts +1 -1
- package/dist/components/BaseTextarea.vue.d.ts +1 -1
- package/dist/components/BatchProgressList.vue.d.ts +17 -0
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +4 -4
- package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +2 -2
- package/dist/components/ConcentrationInput.vue.d.ts +1 -1
- package/dist/components/DataFrame.vue.d.ts +14 -2
- package/dist/components/DatePicker.vue.d.ts +2 -2
- package/dist/components/DateTimePicker.vue.d.ts +3 -1
- package/dist/components/DropdownButton.vue.d.ts +2 -0
- package/dist/components/ExperimentDataViewer.vue.d.ts +26 -1
- package/dist/components/ExperimentPopover.vue.d.ts +1 -0
- package/dist/components/ExperimentTimeline.vue.d.ts +29 -1
- package/dist/components/FileBrowserModal.vue.d.ts +99 -0
- package/dist/components/FilePicker.vue.d.ts +32 -0
- package/dist/components/FileUploader.vue.d.ts +5 -2
- package/dist/components/FitPanel.vue.d.ts +1 -1
- package/dist/components/FormulaInput.vue.d.ts +1 -1
- package/dist/components/GeneratedPlotlyResult.vue.d.ts +1 -3
- package/dist/components/JobsStatusTray.vue.d.ts +9 -2
- package/dist/components/LcmsSequenceTable.vue.d.ts +29 -1
- package/dist/components/MobileSupportGate.vue.d.ts +1 -1
- package/dist/components/MoleculeInput.vue.d.ts +1 -1
- package/dist/components/NumberInput.vue.d.ts +1 -1
- package/dist/components/PlotlyChart.vue.d.ts +59 -0
- package/dist/components/PluginWorkspaceView.props.d.ts +29 -1
- package/dist/components/PluginWorkspaceView.vue.d.ts +5 -5
- package/dist/components/ProgressBar.vue.d.ts +1 -1
- package/dist/components/ProtocolStepEditor.vue.d.ts +1 -0
- package/dist/components/ReagentList.vue.d.ts +29 -1
- package/dist/components/ResourceCard.vue.d.ts +1 -1
- package/dist/components/SampleHierarchyTree.vue.d.ts +29 -1
- package/dist/components/SampleLegend.vue.d.ts +1 -1
- package/dist/components/SampleSelector.drag.d.ts +13 -0
- package/dist/components/SampleSelector.tree.d.ts +2 -0
- package/dist/components/SampleSelector.vue.d.ts +35 -2
- package/dist/components/ScheduleCalendar.vue.d.ts +20 -0
- package/dist/components/SecretInput.vue.d.ts +28 -0
- package/dist/components/SequenceInput.vue.d.ts +1 -1
- package/dist/components/SettingsModal.vue.d.ts +1 -1
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +2 -1
- package/dist/components/StatusIndicator.vue.d.ts +1 -1
- package/dist/components/TagsInput.vue.d.ts +1 -1
- package/dist/components/TimePicker.vue.d.ts +2 -1
- package/dist/components/TimeRangeInput.vue.d.ts +1 -1
- package/dist/components/UnitInput.vue.d.ts +1 -1
- package/dist/components/WellPlate.geometry.d.ts +15 -0
- package/dist/components/WellPlate.interaction.d.ts +13 -0
- package/dist/components/WellPlate.vue.d.ts +30 -1
- package/dist/components/fileBrowserPresentation.d.ts +10 -0
- package/dist/components/index.d.ts +13 -0
- package/dist/components/index.js +8 -8
- package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -0
- package/dist/components/internal/DataStateInternal.vue.d.ts +43 -0
- package/dist/components/internal/FileBrowserDialogInternal.vue.d.ts +34 -0
- package/dist/components/internal/FileBrowserListingInternal.vue.d.ts +25 -0
- package/dist/components/internal/FileBrowserToolbarInternal.vue.d.ts +26 -0
- package/dist/components/internal/FilePickerPreviewInternal.vue.d.ts +18 -0
- package/dist/components/internal/FilePickerTreeInternal.vue.d.ts +31 -0
- package/dist/components/internal/WellEditPopupInternal.vue.d.ts +3 -1
- package/dist/components/internal/WorkspaceControlsInternal.vue.d.ts +23 -0
- package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +70 -0
- package/dist/components/internal/WorkspaceSidebarInternal.vue.d.ts +43 -0
- package/dist/{components-DkHddRMl.js → components-DIum5hkx.js} +5882 -3200
- package/dist/components-DIum5hkx.js.map +1 -0
- package/dist/composables/controlSchemaTypes.d.ts +34 -0
- package/dist/composables/filePicker/localFiles.d.ts +3 -0
- package/dist/composables/filePicker/usePickerNavigation.d.ts +96 -0
- package/dist/composables/filePicker/usePickerSelection.d.ts +31 -0
- package/dist/composables/filePicker/validation.d.ts +5 -0
- package/dist/composables/flattenWorkspaceBindings.d.ts +61 -0
- package/dist/composables/index.d.ts +11 -2
- package/dist/composables/index.js +10 -10
- package/dist/composables/pluginWorkspaceModel.d.ts +164 -0
- package/dist/composables/useApi.d.ts +14 -7
- package/dist/composables/useAsync.d.ts +5 -1
- package/dist/composables/useBioTemplateWorkspace.d.ts +4 -20
- package/dist/composables/useCommandHistory.d.ts +5 -1
- package/dist/composables/useDropdownState.d.ts +18 -2
- package/dist/composables/useEventStream.d.ts +9 -0
- package/dist/composables/useFileBrowser.d.ts +55 -0
- package/dist/composables/useFilePicker.d.ts +189 -0
- package/dist/composables/useFocusTrap.d.ts +21 -0
- package/dist/composables/useGeneratedAnalysis.d.ts +3 -1
- package/dist/composables/useJobsStatusTray.d.ts +25 -1
- package/dist/composables/useListReorder.d.ts +90 -0
- package/dist/composables/useMenuKeyboard.d.ts +29 -0
- package/dist/composables/useOptimisticMutation.d.ts +5 -1
- package/dist/composables/usePlatformFilePickerAdapter.d.ts +11 -0
- package/dist/composables/usePluginClient.d.ts +2 -0
- package/dist/composables/usePluginConfig.d.ts +27 -0
- package/dist/composables/usePluginWorkspace.d.ts +51 -0
- package/dist/composables/useRequestSyncState.d.ts +42 -2
- package/dist/composables/useResourceCrud.d.ts +5 -1
- package/dist/composables/useRovingFocus.d.ts +24 -0
- package/dist/composables/useScheduleDrag.d.ts +1 -0
- package/dist/composables/useWellPainting.d.ts +5 -1
- package/dist/composables/useWellPlateAdapter.d.ts +5 -1
- package/dist/composables/useWellPlateValidation.d.ts +5 -1
- package/dist/composables/workspaceRecipes.d.ts +188 -0
- package/dist/{composables-C_i90sGS.js → composables-mFXsTA4g.js} +502 -86
- package/dist/composables-mFXsTA4g.js.map +1 -0
- package/dist/{useApi-J6zFkHmn.js → errors-BLuqMee5.js} +179 -16
- package/dist/errors-BLuqMee5.js.map +1 -0
- package/dist/{experiment-Cq145oEW.js → experiment-CVDRAnqp.js} +3 -8
- package/dist/experiment-CVDRAnqp.js.map +1 -0
- package/dist/{experiment-utils-CLnzPPq5.js → experiment-utils-CV2Wza3z.js} +70 -51
- package/dist/experiment-utils-CV2Wza3z.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +15 -15
- package/dist/install.js +2 -7
- package/dist/install.js.map +1 -1
- package/dist/permissions.d.ts +42 -1
- package/dist/permissions.js +20 -10
- package/dist/permissions.js.map +1 -1
- package/dist/stores/auth.d.ts +6 -0
- package/dist/stores/index.d.ts +1 -1
- package/dist/stores/index.js +3 -3
- package/dist/stores/settings.d.ts +31 -1
- package/dist/styles.css +15135 -32397
- package/dist/templates/builderReadUtils.d.ts +1 -1
- package/dist/templates/index.js +2 -4
- package/dist/{templates-DsP51i-S.js → templates-CmGfmBRA.js} +303 -106
- package/dist/templates-CmGfmBRA.js.map +1 -0
- package/dist/types/auth.d.ts +1 -0
- package/dist/types/fileBrowserTypes.d.ts +77 -0
- package/dist/types/filePicker.d.ts +77 -0
- package/dist/types/form-builder.d.ts +1 -1
- package/dist/types/index.d.ts +2 -0
- package/dist/{useControlSchema-BDx2Olha.js → useControlSchema-DldpxNOP.js} +499 -1162
- package/dist/useControlSchema-DldpxNOP.js.map +1 -0
- package/dist/{useDropdownState-DSb3BC3N.js → useDropdownState-C7hRiVQf.js} +22 -6
- package/dist/useDropdownState-C7hRiVQf.js.map +1 -0
- package/dist/{useExperimentSelector-CV4iNfMS.js → useExperimentSelector-CEcIaueJ.js} +4 -4
- package/dist/{useExperimentSelector-CV4iNfMS.js.map → useExperimentSelector-CEcIaueJ.js.map} +1 -1
- package/dist/{useFormBuilder-BLxG3BOX.js → useFormBuilder-z5L_dUIy.js} +2 -2
- package/dist/{useFormBuilder-BLxG3BOX.js.map → useFormBuilder-z5L_dUIy.js.map} +1 -1
- package/dist/utils/color.d.ts +13 -0
- package/dist/utils/errors.d.ts +9 -0
- package/dist/utils/pluginSecrets.d.ts +53 -0
- package/dist/utils/records.d.ts +4 -0
- package/dist/{useProtocolTemplates-CS_SQl6U.js → validation-uwoJGEb6.js} +811 -212
- package/dist/validation-uwoJGEb6.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/apiErrors.test.ts +81 -0
- package/src/__tests__/components/ActionMenu.test.ts +94 -0
- package/src/__tests__/components/AppAvatarMenu.test.ts +123 -0
- package/src/__tests__/components/BaseTabs.test.ts +67 -0
- package/src/__tests__/components/DataFrame.test.ts +183 -11
- package/src/__tests__/components/DataStates.test.ts +176 -0
- package/src/__tests__/components/DatePicker.test.ts +76 -0
- package/src/__tests__/components/DateTimePicker.test.ts +36 -0
- package/src/__tests__/components/DropdownButton.test.ts +134 -0
- package/src/__tests__/components/ExperimentPopover.test.ts +30 -0
- package/src/__tests__/components/ExperimentTimeline.test.ts +63 -0
- package/src/__tests__/components/FileBrowserModal.test.ts +460 -0
- package/src/__tests__/components/FilePicker.state.test.ts +287 -0
- package/src/__tests__/components/FilePicker.test.ts +487 -0
- package/src/__tests__/components/GeneratedPlotlyResult.test.ts +52 -5
- package/src/__tests__/components/GroupAssigner.test.ts +53 -0
- package/src/__tests__/components/LcmsSequenceTable.test.ts +22 -0
- package/src/__tests__/components/PlotlyChart.test.ts +305 -0
- package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +230 -0
- package/src/__tests__/components/RackEditor.test.ts +35 -0
- package/src/__tests__/components/ReagentList.test.ts +64 -2
- package/src/__tests__/components/RetiredStyles.test.ts +31 -0
- package/src/__tests__/components/SampleSelector.drag.test.ts +51 -1
- package/src/__tests__/components/ScheduleCalendar.test.ts +63 -0
- package/src/__tests__/components/SecretInput.test.ts +87 -0
- package/src/__tests__/components/SegmentedControl.test.ts +107 -0
- package/src/__tests__/components/TimePicker.test.ts +61 -0
- package/src/__tests__/components/WellEditPopupInternal.test.ts +108 -0
- package/src/__tests__/components/WellPlate.interaction.test.ts +59 -0
- package/src/__tests__/components/iconButtonIconSizing.test.ts +18 -0
- package/src/__tests__/composables/useApi.test.ts +92 -9
- package/src/__tests__/composables/useAuth.test.ts +2 -0
- package/src/__tests__/composables/useDropdownState.test.ts +58 -1
- package/src/__tests__/composables/useFileBrowser.test.ts +305 -0
- package/src/__tests__/composables/useGeneratedAnalysis.test.ts +7 -2
- package/src/__tests__/composables/useListReorder.test.ts +145 -0
- package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +67 -0
- package/src/__tests__/composables/usePluginClient.test.ts +143 -0
- package/src/__tests__/composables/usePluginConfig.test.ts +497 -0
- package/src/__tests__/composables/useRequestSyncState.test.ts +140 -0
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +39 -7
- package/src/__tests__/stores/settings.test.ts +71 -1
- package/src/__tests__/styles/cssTokens.test.ts +131 -0
- package/src/__tests__/utils/permissions.test.ts +68 -0
- package/src/apiErrors.ts +85 -0
- package/src/components/ActionMenu.vue +45 -88
- package/src/components/AlertBox.vue +0 -4
- package/src/components/AppAvatarMenu.vue +26 -7
- package/src/components/AppLayout.vue +0 -4
- package/src/components/AppPluginSwitcher.story.vue +87 -0
- package/src/components/AppPluginSwitcher.vue +2 -5
- package/src/components/AppSidebar.vue +0 -4
- package/src/components/AppToastContainer.vue +6 -8
- package/src/components/AppTopBar.vue +0 -4
- package/src/components/ArtifactSaveDialog.vue +0 -4
- package/src/components/ArtifactSelectorModal.vue +0 -4
- package/src/components/AuditTrail.story.vue +18 -0
- package/src/components/AuditTrail.vue +27 -7
- package/src/components/AutoGroupModal.vue +0 -4
- package/src/components/Avatar.vue +0 -4
- package/src/components/BaseButton.vue +0 -4
- package/src/components/BaseCheckbox.vue +0 -4
- package/src/components/BaseInput.vue +0 -4
- package/src/components/BaseModal.vue +8 -60
- package/src/components/BasePill.vue +0 -4
- package/src/components/BaseRadioGroup.vue +0 -4
- package/src/components/BaseSelect.vue +0 -4
- package/src/components/BaseSlider.vue +0 -4
- package/src/components/BaseTabs.vue +26 -5
- package/src/components/BaseTextarea.vue +0 -4
- package/src/components/BaseToggle.vue +0 -4
- package/src/components/BatchProgressList.story.vue +18 -0
- package/src/components/BatchProgressList.vue +32 -4
- package/src/components/BioTemplateExperimentWorkspaceView.vue +10 -21
- package/src/components/BioTemplatePackWorkspaceView.vue +11 -15
- package/src/components/BioTemplatePresetWorkspaceView.vue +8 -13
- package/src/components/Breadcrumb.vue +0 -4
- package/src/components/Calendar.vue +0 -4
- package/src/components/ChartContainer.story.vue +1 -1
- package/src/components/ChartContainer.vue +0 -4
- package/src/components/ChemicalFormula.vue +0 -4
- package/src/components/CollapsibleCard.vue +0 -4
- package/src/components/ColorSlider.vue +0 -4
- package/src/components/ComponentBindingRenderer.vue +1 -3
- package/src/components/ConcentrationInput.vue +0 -4
- package/src/components/ConfirmDialog.vue +0 -4
- package/src/components/ControlWorkspaceView.vue +11 -22
- package/src/components/DataFrame.story.vue +14 -0
- package/src/components/DataFrame.vue +97 -23
- package/src/components/DatePicker.vue +73 -27
- package/src/components/DateTimePicker.vue +45 -17
- package/src/components/Divider.vue +0 -4
- package/src/components/DoseCalculator.vue +0 -4
- package/src/components/DoseDesignWorkspaceView.vue +6 -25
- package/src/components/DropdownButton.vue +24 -6
- package/src/components/EmptyState.vue +0 -4
- package/src/components/ExperimentCodeBadge.vue +0 -4
- package/src/components/ExperimentDataViewer.story.vue +3 -0
- package/src/components/ExperimentDataViewer.vue +33 -13
- package/src/components/ExperimentPopover.vue +12 -7
- package/src/components/ExperimentSelectorModal.vue +0 -4
- package/src/components/ExperimentTimeline.story.vue +18 -0
- package/src/components/ExperimentTimeline.vue +81 -75
- package/src/components/FileBrowserModal.story.vue +203 -0
- package/src/components/FileBrowserModal.vue +384 -0
- package/src/components/FilePicker.story.vue +44 -0
- package/src/components/FilePicker.vue +284 -0
- package/src/components/FileUploader.vue +32 -18
- package/src/components/FitPanel.vue +0 -4
- package/src/components/FormActions.story.vue +96 -0
- package/src/components/FormActions.vue +0 -4
- package/src/components/FormBuilder.vue +0 -4
- package/src/components/FormField.vue +0 -4
- package/src/components/FormulaInput.vue +0 -4
- package/src/components/GeneratedPathInput.story.vue +68 -0
- package/src/components/GeneratedPlotlyResult.story.vue +96 -0
- package/src/components/GeneratedPlotlyResult.vue +34 -69
- package/src/components/GeneratedPluginView.story.vue +145 -0
- package/src/components/GeneratedPluginView.vue +10 -10
- package/src/components/GeneratedResultRenderer.story.vue +117 -0
- package/src/components/GeneratedResultRenderer.vue +4 -9
- package/src/components/GroupAssigner.vue +67 -5
- package/src/components/IconButton.vue +0 -4
- package/src/components/InstrumentAlertLog.vue +0 -4
- package/src/components/InstrumentStateBadge.vue +0 -4
- package/src/components/InstrumentStatusCard.vue +0 -4
- package/src/components/JobsStatusTray.vue +16 -5
- package/src/components/LcmsSequenceTable.vue +81 -50
- package/src/components/LoadingSpinner.vue +0 -4
- package/src/components/MobileSupportGate.vue +0 -4
- package/src/components/MoleculeInput.vue +0 -4
- package/src/components/MultiSelect.vue +0 -4
- package/src/components/NumberInput.vue +0 -4
- package/src/components/PlateMapEditor.vue +3 -14
- package/src/components/PlotlyChart.story.vue +61 -0
- package/src/components/PlotlyChart.vue +233 -0
- package/src/components/PluginIcon.vue +0 -4
- package/src/components/PluginWorkspaceView.props.ts +38 -0
- package/src/components/PluginWorkspaceView.vue +53 -224
- package/src/components/ProgressBar.vue +0 -4
- package/src/components/ProtocolStepEditor.vue +2 -4
- package/src/components/RackEditor.vue +51 -70
- package/src/components/ReagentEditor.vue +0 -4
- package/src/components/ReagentList.story.vue +18 -0
- package/src/components/ReagentList.vue +127 -82
- package/src/components/ResourceCard.vue +0 -4
- package/src/components/RuntimeUpdateNotice.story.vue +49 -0
- package/src/components/RuntimeUpdateNotice.vue +4 -4
- package/src/components/SampleHierarchyTree.story.vue +18 -0
- package/src/components/SampleHierarchyTree.vue +32 -9
- package/src/components/SampleLegend.vue +2 -8
- package/src/components/SampleSelector.drag.ts +51 -0
- package/src/components/SampleSelector.story.vue +6 -0
- package/src/components/SampleSelector.tree.ts +2 -0
- package/src/components/SampleSelector.vue +49 -10
- package/src/components/SampleSelectorGroupNode.vue +19 -0
- package/src/components/ScheduleCalendar.story.vue +6 -0
- package/src/components/ScheduleCalendar.vue +82 -7
- package/src/components/ScientificNumber.vue +0 -4
- package/src/components/SecretInput.story.vue +79 -0
- package/src/components/SecretInput.vue +150 -0
- package/src/components/SectionCard.vue +0 -4
- package/src/components/SegmentedControl.vue +50 -7
- package/src/components/SequenceInput.vue +0 -4
- package/src/components/SequenceProgressBar.story.vue +110 -0
- package/src/components/SequenceProgressBar.vue +0 -4
- package/src/components/SettingsModal.vue +0 -4
- package/src/components/Skeleton.vue +0 -4
- package/src/components/SmartGroupFieldRecipe.vue +8 -6
- package/src/components/SmartGroupManual.vue +0 -4
- package/src/components/StatusIndicator.vue +0 -4
- package/src/components/StepWizard.vue +0 -4
- package/src/components/TagsInput.vue +4 -6
- package/src/components/ThemeToggle.vue +0 -4
- package/src/components/TimePicker.vue +36 -7
- package/src/components/TimeRangeInput.vue +0 -4
- package/src/components/Tooltip.story.vue +3 -3
- package/src/components/Tooltip.vue +0 -4
- package/src/components/UnitInput.vue +0 -4
- package/src/components/WellPlate.geometry.ts +60 -1
- package/src/components/WellPlate.interaction.ts +44 -0
- package/src/components/WellPlate.story.vue +8 -0
- package/src/components/WellPlate.vue +53 -7
- package/src/components/fileBrowserPresentation.ts +39 -0
- package/src/components/index.ts +14 -0
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +2 -5
- package/src/components/internal/AppTopBarPillNavInternal.vue +0 -4
- package/src/components/internal/DataStateInternal.vue +74 -0
- package/src/components/internal/FileBrowserDialogInternal.vue +75 -0
- package/src/components/internal/FileBrowserListingInternal.vue +197 -0
- package/src/components/internal/FileBrowserToolbarInternal.vue +136 -0
- package/src/components/internal/FilePickerPreviewInternal.vue +100 -0
- package/src/components/internal/FilePickerTreeInternal.vue +238 -0
- package/src/components/internal/FormSectionRenderer.vue +0 -4
- package/src/components/internal/WellEditPopupInternal.vue +20 -5
- package/src/components/internal/WorkspaceControlsInternal.vue +20 -0
- package/src/components/internal/WorkspaceShellInternal.vue +125 -0
- package/src/components/internal/WorkspaceSidebarInternal.vue +82 -0
- package/src/composables/controlSchemaTypes.ts +34 -0
- package/src/composables/experimentDesignData.ts +1 -3
- package/src/composables/filePicker/localFiles.ts +58 -0
- package/src/composables/filePicker/usePickerNavigation.ts +220 -0
- package/src/composables/filePicker/usePickerSelection.ts +259 -0
- package/src/composables/filePicker/validation.ts +41 -0
- package/src/composables/flattenWorkspaceBindings.ts +104 -0
- package/src/composables/formBuilderRegistry.ts +5 -0
- package/src/composables/index.ts +50 -0
- package/src/composables/pluginWorkspaceModel.ts +182 -0
- package/src/composables/useApi.ts +57 -13
- package/src/composables/useAsync.ts +5 -1
- package/src/composables/useAuth.ts +3 -0
- package/src/composables/useBioTemplatePackWorkspace.ts +15 -9
- package/src/composables/useBioTemplatePresetWorkspace.ts +14 -15
- package/src/composables/useBioTemplateWorkspace.ts +18 -43
- package/src/composables/useCommandHistory.ts +5 -1
- package/src/composables/useDropdownState.ts +35 -5
- package/src/composables/useEventStream.ts +14 -2
- package/src/composables/useFileBrowser.ts +314 -0
- package/src/composables/useFileImport.ts +2 -7
- package/src/composables/useFilePicker.ts +366 -0
- package/src/composables/useFocusTrap.ts +91 -0
- package/src/composables/useGeneratedAnalysis.ts +32 -27
- package/src/composables/useJobsStatusTray.ts +184 -17
- package/src/composables/useListReorder.story.vue +143 -0
- package/src/composables/useListReorder.ts +242 -0
- package/src/composables/useMenuKeyboard.ts +90 -0
- package/src/composables/useOptimisticMutation.ts +6 -16
- package/src/composables/usePlatformFilePickerAdapter.ts +104 -0
- package/src/composables/usePluginClient.ts +50 -4
- package/src/composables/usePluginConfig.ts +252 -0
- package/src/composables/usePluginWorkspace.ts +208 -0
- package/src/composables/useRackEditor.ts +2 -11
- package/src/composables/useRequestSyncState.ts +98 -28
- package/src/composables/useResourceCrud.ts +6 -16
- package/src/composables/useRovingFocus.ts +66 -0
- package/src/composables/useScheduleDrag.ts +37 -0
- package/src/composables/useWellPainting.ts +10 -30
- package/src/composables/useWellPlateAdapter.ts +5 -1
- package/src/composables/useWellPlateEditor.ts +1 -11
- package/src/composables/useWellPlateValidation.ts +5 -1
- package/src/composables/workspaceRecipes.ts +267 -0
- package/src/index.ts +19 -0
- package/src/permissions.ts +48 -9
- package/src/stores/auth.ts +1 -0
- package/src/stores/experiment.ts +2 -7
- package/src/stores/index.ts +9 -1
- package/src/stores/settings.ts +93 -1
- package/src/styles/components/audit-trail.css +5 -5
- package/src/styles/components/batch-progress-list.css +7 -0
- package/src/styles/components/color-slider.css +2 -2
- package/src/styles/components/data-state.css +30 -0
- package/src/styles/components/dataframe.css +29 -35
- package/src/styles/components/experiment-data-viewer.css +4 -4
- package/src/styles/components/experiment-timeline.css +3 -30
- package/src/styles/components/file-browser-modal.css +489 -0
- package/src/styles/components/file-picker.css +502 -0
- package/src/styles/components/form-builder.css +5 -5
- package/src/styles/components/icon-button.css +9 -0
- package/src/styles/components/lcms-sequence-table.css +16 -1
- package/src/styles/components/multi-select.css +1 -1
- package/src/styles/components/reagent-list.css +38 -31
- package/src/styles/components/sample-hierarchy-tree.css +5 -19
- package/src/styles/components/sample-selector.css +9 -2
- package/src/styles/components/schedule-calendar.css +8 -0
- package/src/styles/components/scientific-number.css +1 -1
- package/src/styles/components/secret-input.css +60 -0
- package/src/styles/components/well-plate.css +8 -0
- package/src/styles/index.css +12 -0
- package/src/styles/variables.css +29 -0
- package/src/templates/builderPresetControls.ts +1 -1
- package/src/templates/builderReadUtils.ts +1 -3
- package/src/templates/componentPlateHelpers.ts +3 -1
- package/src/templates/componentTargetResolvers.ts +1 -3
- package/src/templates/controlSchemaTargets.ts +1 -3
- package/src/templates/doseResponseAdapters.ts +2 -13
- package/src/templates/elisaAssayCollectionBuilder.ts +2 -2
- package/src/templates/plateMapBuilder.ts +2 -2
- package/src/templates/qpcrPlateBuilder.ts +1 -1
- package/src/templates/templateValidators.ts +1 -1
- package/src/templates/timeCourseBuilder.ts +2 -2
- package/src/templates/wellPlateScreenCollectionBuilder.ts +2 -2
- package/src/types/auth.ts +1 -0
- package/src/types/fileBrowserTypes.ts +85 -0
- package/src/types/filePicker.ts +72 -0
- package/src/types/form-builder.ts +1 -0
- package/src/types/index.ts +14 -0
- package/src/utils/color.ts +51 -0
- package/src/utils/errors.ts +29 -0
- package/src/utils/formModelSync.ts +1 -3
- package/src/utils/pluginIcon.ts +1 -3
- package/src/utils/pluginSecrets.ts +102 -0
- package/src/utils/records.ts +9 -0
- package/dist/EmptyState-p90quq8Z.js.map +0 -1
- package/dist/ExperimentPopover-tLwJ7XF4.js.map +0 -1
- package/dist/ExperimentSelectorModal-DyPAUCLw.js.map +0 -1
- package/dist/SettingsModal-BgKY93zh.js.map +0 -1
- package/dist/components-DkHddRMl.js.map +0 -1
- package/dist/composables/autoGroupConstants.d.ts +0 -1
- package/dist/composables/autoGroupGrouping.d.ts +0 -3
- package/dist/composables/useAutoGroupInputSources.d.ts +0 -32
- package/dist/composables-C_i90sGS.js.map +0 -1
- package/dist/experiment-Cq145oEW.js.map +0 -1
- package/dist/experiment-utils-CLnzPPq5.js.map +0 -1
- package/dist/templates/builderDefaults.d.ts +0 -1
- package/dist/templates/plateGeometry.d.ts +0 -4
- package/dist/templates-DsP51i-S.js.map +0 -1
- package/dist/useApi-J6zFkHmn.js.map +0 -1
- package/dist/useControlSchema-BDx2Olha.js.map +0 -1
- package/dist/useDropdownState-DSb3BC3N.js.map +0 -1
- package/dist/useProtocolTemplates-CS_SQl6U.js.map +0 -1
- package/src/__tests__/composables/useAutoGroupInputSources.test.ts +0 -127
- package/src/composables/autoGroupConstants.ts +0 -4
- package/src/composables/autoGroupGrouping.ts +0 -148
- package/src/composables/useAutoGroupInputSources.ts +0 -147
- package/src/templates/builderDefaults.ts +0 -11
- package/src/templates/plateGeometry.ts +0 -62
- /package/dist/__tests__/{composables/useAutoGroupInputSources.test.d.ts → apiErrors.test.d.ts} +0 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
getCurrentScope,
|
|
4
|
+
onScopeDispose,
|
|
5
|
+
ref,
|
|
6
|
+
shallowRef,
|
|
7
|
+
toValue,
|
|
8
|
+
watch,
|
|
9
|
+
type ComputedRef,
|
|
10
|
+
type MaybeRefOrGetter,
|
|
11
|
+
type Ref,
|
|
12
|
+
} from 'vue'
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
FileBrowserSortKey,
|
|
16
|
+
FileDirectoryListing,
|
|
17
|
+
FileEntry,
|
|
18
|
+
FilePathCrumb,
|
|
19
|
+
FileSelection,
|
|
20
|
+
ServerMount,
|
|
21
|
+
} from '../types/fileBrowserTypes'
|
|
22
|
+
import { useApi } from './useApi'
|
|
23
|
+
import { getInjectedPlatformContext } from './platformContextHelpers'
|
|
24
|
+
import { useRequestSyncState } from './useRequestSyncState'
|
|
25
|
+
|
|
26
|
+
export interface UseFileBrowserOptions {
|
|
27
|
+
/** Extensions the caller cares about; entries are flagged, never removed. */
|
|
28
|
+
typeRules?: MaybeRefOrGetter<string[] | undefined>
|
|
29
|
+
/** Mount to open first; defaults to the first available one. */
|
|
30
|
+
initialMountId?: MaybeRefOrGetter<string | undefined>
|
|
31
|
+
/** Override the platform API base URL (standalone plugin dev). */
|
|
32
|
+
apiBaseUrl?: string
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface UseFileBrowserReturn {
|
|
36
|
+
/** Every configured mount, offline ones included. */
|
|
37
|
+
mounts: Ref<ServerMount[]>
|
|
38
|
+
/** Mount the current listing belongs to. */
|
|
39
|
+
mountId: Ref<string>
|
|
40
|
+
/** Mount-relative path of the current listing. */
|
|
41
|
+
path: Ref<string>
|
|
42
|
+
/** Parent path, or null at a mount root. */
|
|
43
|
+
parent: Ref<string | null>
|
|
44
|
+
entries: Ref<FileEntry[]>
|
|
45
|
+
breadcrumbs: Ref<FilePathCrumb[]>
|
|
46
|
+
/** Directory size before any search filter. */
|
|
47
|
+
totalCount: Ref<number | null>
|
|
48
|
+
/** How many entries satisfy the type rules. */
|
|
49
|
+
matchCount: Ref<number | null>
|
|
50
|
+
/** How many rows this listing carries, after search and any cap. */
|
|
51
|
+
shownCount: Ref<number | null>
|
|
52
|
+
/** True when the server capped the listing. */
|
|
53
|
+
truncated: Ref<boolean>
|
|
54
|
+
search: Ref<string>
|
|
55
|
+
sort: Ref<FileBrowserSortKey>
|
|
56
|
+
/** Controlled selection, ready to bind to FileBrowserModal. */
|
|
57
|
+
selected: Ref<FileSelection[]>
|
|
58
|
+
isLoading: ComputedRef<boolean>
|
|
59
|
+
error: Ref<string | null>
|
|
60
|
+
/** Load the mount list and open the first available mount. */
|
|
61
|
+
init: () => Promise<void>
|
|
62
|
+
/** Open a directory, or a mount root when path is empty. */
|
|
63
|
+
navigate: (location: { mount_id: string; path: string }) => Promise<void>
|
|
64
|
+
/** Re-read the current directory. */
|
|
65
|
+
refresh: () => Promise<void>
|
|
66
|
+
/** Take the directory currently being viewed as the input. */
|
|
67
|
+
selectCurrentFolder: () => void
|
|
68
|
+
clear: () => void
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Drives FileBrowserModal against the platform's `/api/filesystem` routes.
|
|
73
|
+
*
|
|
74
|
+
* Requires the `filesystem.browse` permission and a deployment that declares
|
|
75
|
+
* mounts; with none configured the routes are absent and `mounts` stays empty.
|
|
76
|
+
*/
|
|
77
|
+
export function useFileBrowser(options: UseFileBrowserOptions = {}): UseFileBrowserReturn {
|
|
78
|
+
const injectedContext = getInjectedPlatformContext()
|
|
79
|
+
const api = useApi({ baseUrl: options.apiBaseUrl ?? injectedContext?.platformApiUrl })
|
|
80
|
+
const sync = useRequestSyncState('Could not read the server mounts.')
|
|
81
|
+
|
|
82
|
+
const mounts = ref<ServerMount[]>([])
|
|
83
|
+
const mountId = shallowRef('')
|
|
84
|
+
const path = shallowRef('')
|
|
85
|
+
const parent = ref<string | null>(null)
|
|
86
|
+
const entries = ref<FileEntry[]>([])
|
|
87
|
+
const breadcrumbs = ref<FilePathCrumb[]>([])
|
|
88
|
+
const totalCount = ref<number | null>(null)
|
|
89
|
+
const matchCount = ref<number | null>(null)
|
|
90
|
+
const shownCount = ref<number | null>(null)
|
|
91
|
+
const truncated = ref(false)
|
|
92
|
+
const search = shallowRef('')
|
|
93
|
+
const sort = shallowRef<FileBrowserSortKey>('name')
|
|
94
|
+
const selected = ref<FileSelection[]>([])
|
|
95
|
+
|
|
96
|
+
const mountsLoading = shallowRef(false)
|
|
97
|
+
const listingLoading = shallowRef(false)
|
|
98
|
+
const listingReady = shallowRef(false)
|
|
99
|
+
const isLoading = computed(() => mountsLoading.value || listingLoading.value)
|
|
100
|
+
let listToken = 0
|
|
101
|
+
let mountsToken = 0
|
|
102
|
+
let disposed = false
|
|
103
|
+
let listingController: AbortController | undefined
|
|
104
|
+
let mountsController: AbortController | undefined
|
|
105
|
+
let searchTimer: ReturnType<typeof setTimeout> | undefined
|
|
106
|
+
|
|
107
|
+
function resetListing(): void {
|
|
108
|
+
listingReady.value = false
|
|
109
|
+
entries.value = []
|
|
110
|
+
breadcrumbs.value = []
|
|
111
|
+
parent.value = null
|
|
112
|
+
totalCount.value = null
|
|
113
|
+
matchCount.value = null
|
|
114
|
+
shownCount.value = null
|
|
115
|
+
truncated.value = false
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function typeRuleParams(): string {
|
|
119
|
+
const rules = toValue(options.typeRules) ?? []
|
|
120
|
+
return rules.map((rule) => `&type_rules=${encodeURIComponent(rule)}`).join('')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Prefer the API's own `detail` over axios's "Request failed with status N". */
|
|
124
|
+
function describe(cause: unknown, fallback: string): string {
|
|
125
|
+
const detail = (cause as { response?: { data?: { detail?: unknown } } })?.response?.data?.detail
|
|
126
|
+
return typeof detail === 'string' && detail ? detail : sync.readErrorMessage(cause, fallback)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function fail(cause: unknown, fallback: string): void {
|
|
130
|
+
sync.setError(new Error(describe(cause, fallback)), fallback)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function statusOf(cause: unknown): number | undefined {
|
|
134
|
+
return (cause as { response?: { status?: number } })?.response?.status
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function loadMounts(): Promise<boolean> {
|
|
138
|
+
const token = ++mountsToken
|
|
139
|
+
mountsController?.abort()
|
|
140
|
+
mountsController = new AbortController()
|
|
141
|
+
mountsLoading.value = true
|
|
142
|
+
try {
|
|
143
|
+
const data = await api.get<{ mounts: ServerMount[] }>('/filesystem/mounts', {
|
|
144
|
+
signal: mountsController.signal,
|
|
145
|
+
})
|
|
146
|
+
if (disposed || token !== mountsToken) return false
|
|
147
|
+
mounts.value = data.mounts ?? []
|
|
148
|
+
sync.clearError()
|
|
149
|
+
sync.markLoaded()
|
|
150
|
+
return true
|
|
151
|
+
} catch (cause) {
|
|
152
|
+
if (disposed || token !== mountsToken) return false
|
|
153
|
+
// The platform only registers these routes once mounts are configured, so
|
|
154
|
+
// a 404 means "none declared" — an empty sidebar, not a failure.
|
|
155
|
+
if (statusOf(cause) === 404) {
|
|
156
|
+
mounts.value = []
|
|
157
|
+
sync.clearError()
|
|
158
|
+
sync.markLoaded()
|
|
159
|
+
return true
|
|
160
|
+
}
|
|
161
|
+
mounts.value = []
|
|
162
|
+
fail(cause, 'Could not read the server mounts.')
|
|
163
|
+
return false
|
|
164
|
+
} finally {
|
|
165
|
+
if (token === mountsToken) mountsLoading.value = false
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function navigate(location: { mount_id: string; path: string }): Promise<void> {
|
|
170
|
+
if (disposed || !location.mount_id) return
|
|
171
|
+
clearTimeout(searchTimer)
|
|
172
|
+
const token = ++listToken
|
|
173
|
+
listingController?.abort()
|
|
174
|
+
listingController = new AbortController()
|
|
175
|
+
listingLoading.value = true
|
|
176
|
+
mountId.value = location.mount_id
|
|
177
|
+
path.value = location.path
|
|
178
|
+
resetListing()
|
|
179
|
+
sync.clearError()
|
|
180
|
+
try {
|
|
181
|
+
const query =
|
|
182
|
+
`mount_id=${encodeURIComponent(location.mount_id)}` +
|
|
183
|
+
`&path=${encodeURIComponent(location.path)}` +
|
|
184
|
+
`&sort=${sort.value}` +
|
|
185
|
+
(search.value ? `&search=${encodeURIComponent(search.value)}` : '') +
|
|
186
|
+
typeRuleParams()
|
|
187
|
+
const listing = await api.get<FileDirectoryListing>(`/filesystem/browse?${query}`, {
|
|
188
|
+
signal: listingController.signal,
|
|
189
|
+
})
|
|
190
|
+
if (disposed || token !== listToken) return
|
|
191
|
+
mountId.value = listing.mount_id
|
|
192
|
+
path.value = listing.path
|
|
193
|
+
parent.value = listing.parent ?? null
|
|
194
|
+
entries.value = listing.entries ?? []
|
|
195
|
+
breadcrumbs.value = listing.breadcrumbs ?? []
|
|
196
|
+
totalCount.value = listing.total_count ?? null
|
|
197
|
+
matchCount.value = listing.match_count ?? null
|
|
198
|
+
shownCount.value = listing.shown_count ?? listing.entries?.length ?? null
|
|
199
|
+
truncated.value = listing.truncated ?? false
|
|
200
|
+
listingReady.value = true
|
|
201
|
+
sync.clearError()
|
|
202
|
+
sync.markLoaded()
|
|
203
|
+
} catch (cause) {
|
|
204
|
+
if (disposed || token !== listToken) return
|
|
205
|
+
entries.value = []
|
|
206
|
+
fail(cause, 'Could not read that folder.')
|
|
207
|
+
} finally {
|
|
208
|
+
if (token === listToken) listingLoading.value = false
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function init(): Promise<void> {
|
|
213
|
+
if (disposed) return
|
|
214
|
+
const navigationToken = listToken
|
|
215
|
+
if (!(await loadMounts()) || disposed || navigationToken !== listToken) return
|
|
216
|
+
const preferred = toValue(options.initialMountId)
|
|
217
|
+
// An offline mount is not navigable, so a remembered preference for one
|
|
218
|
+
// falls through to the first reachable mount instead of opening on an error.
|
|
219
|
+
const reachable = (mount: ServerMount): boolean => mount.available !== false
|
|
220
|
+
const target =
|
|
221
|
+
mounts.value.find((mount) => mount.id === preferred && reachable(mount)) ??
|
|
222
|
+
mounts.value.find(reachable)
|
|
223
|
+
if (target) await navigate({ mount_id: target.id, path: '' })
|
|
224
|
+
else {
|
|
225
|
+
mountId.value = ''
|
|
226
|
+
path.value = ''
|
|
227
|
+
resetListing()
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function refresh(): Promise<void> {
|
|
232
|
+
if (disposed) return
|
|
233
|
+
if (mountId.value) await navigate({ mount_id: mountId.value, path: path.value })
|
|
234
|
+
else await init()
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function selectCurrentFolder(): void {
|
|
238
|
+
if (!mountId.value || !listingReady.value || isLoading.value || sync.error.value) return
|
|
239
|
+
const crumb = breadcrumbs.value[breadcrumbs.value.length - 1]
|
|
240
|
+
const key = JSON.stringify([mountId.value, path.value])
|
|
241
|
+
if (selected.value.some((item) => JSON.stringify([item.mount_id, item.path]) === key)) return
|
|
242
|
+
selected.value = [
|
|
243
|
+
...selected.value,
|
|
244
|
+
{
|
|
245
|
+
mount_id: mountId.value,
|
|
246
|
+
path: path.value,
|
|
247
|
+
name: crumb?.label ?? path.value,
|
|
248
|
+
kind: 'directory',
|
|
249
|
+
child_count: totalCount.value,
|
|
250
|
+
},
|
|
251
|
+
]
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function clear(): void {
|
|
255
|
+
selected.value = []
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Both controls are server-side, so they only take effect on a re-fetch.
|
|
259
|
+
// Without these watchers the search box and sort button look live but are
|
|
260
|
+
// inert until the user happens to navigate.
|
|
261
|
+
watch(search, () => {
|
|
262
|
+
if (!mountId.value) return
|
|
263
|
+
clearTimeout(searchTimer)
|
|
264
|
+
// Debounced: search changes on every keystroke.
|
|
265
|
+
searchTimer = setTimeout(() => {
|
|
266
|
+
void refresh()
|
|
267
|
+
}, 250)
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
watch(sort, () => {
|
|
271
|
+
if (mountId.value) void refresh()
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
watch(
|
|
275
|
+
() => toValue(options.typeRules),
|
|
276
|
+
() => {
|
|
277
|
+
if (mountId.value) void refresh()
|
|
278
|
+
},
|
|
279
|
+
{ deep: true },
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
if (getCurrentScope())
|
|
283
|
+
onScopeDispose(() => {
|
|
284
|
+
disposed = true
|
|
285
|
+
++listToken
|
|
286
|
+
++mountsToken
|
|
287
|
+
clearTimeout(searchTimer)
|
|
288
|
+
listingController?.abort()
|
|
289
|
+
mountsController?.abort()
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
mounts,
|
|
294
|
+
mountId,
|
|
295
|
+
path,
|
|
296
|
+
parent,
|
|
297
|
+
entries,
|
|
298
|
+
breadcrumbs,
|
|
299
|
+
totalCount,
|
|
300
|
+
matchCount,
|
|
301
|
+
shownCount,
|
|
302
|
+
truncated,
|
|
303
|
+
search,
|
|
304
|
+
sort,
|
|
305
|
+
selected,
|
|
306
|
+
isLoading,
|
|
307
|
+
error: sync.error,
|
|
308
|
+
init,
|
|
309
|
+
navigate,
|
|
310
|
+
refresh,
|
|
311
|
+
selectCurrentFolder,
|
|
312
|
+
clear,
|
|
313
|
+
}
|
|
314
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ref, shallowRef, type Ref, type ShallowRef } from 'vue'
|
|
2
|
+
import { readErrorMessage } from '../utils/errors'
|
|
2
3
|
|
|
3
4
|
export interface DelimitedTable {
|
|
4
5
|
columns: string[]
|
|
@@ -63,7 +64,7 @@ export function useFileImport<TResult = string>(
|
|
|
63
64
|
result.value = parsed
|
|
64
65
|
return parsed
|
|
65
66
|
} catch (err) {
|
|
66
|
-
const message = readErrorMessage(err)
|
|
67
|
+
const message = readErrorMessage(err, 'Failed to import file.')
|
|
67
68
|
error.value = message
|
|
68
69
|
throw err
|
|
69
70
|
} finally {
|
|
@@ -223,9 +224,3 @@ function formatBytes(bytes: number): string {
|
|
|
223
224
|
if (bytes < 1024 * 1024) return `${Math.round(bytes / 1024)} KB`
|
|
224
225
|
return `${Math.round(bytes / (1024 * 1024))} MB`
|
|
225
226
|
}
|
|
226
|
-
|
|
227
|
-
function readErrorMessage(value: unknown): string {
|
|
228
|
-
if (value instanceof Error && value.message) return value.message
|
|
229
|
-
if (typeof value === 'string' && value.trim()) return value
|
|
230
|
-
return 'Failed to import file.'
|
|
231
|
-
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { computed, nextTick, onScopeDispose, ref, shallowRef, toRaw, watch } from 'vue'
|
|
2
|
+
import type {
|
|
3
|
+
PickerAdapter,
|
|
4
|
+
PickerCapabilities,
|
|
5
|
+
PickerNode,
|
|
6
|
+
PickerSelection,
|
|
7
|
+
PickerSelectionMode,
|
|
8
|
+
PickerSource,
|
|
9
|
+
} from '../types/filePicker'
|
|
10
|
+
import { usePickerNavigation } from './filePicker/usePickerNavigation'
|
|
11
|
+
import { usePickerSelection } from './filePicker/usePickerSelection'
|
|
12
|
+
import { localFilesAdapter } from './filePicker/localFiles'
|
|
13
|
+
|
|
14
|
+
interface PickerStateProps {
|
|
15
|
+
open: boolean
|
|
16
|
+
adapter: PickerAdapter
|
|
17
|
+
selectionMode: PickerSelectionMode
|
|
18
|
+
sources: PickerSource[]
|
|
19
|
+
capabilities: PickerCapabilities
|
|
20
|
+
initialPath: string
|
|
21
|
+
initialSelection: PickerSelection | null
|
|
22
|
+
systemFilter?: (node: PickerNode, source: PickerSource) => boolean
|
|
23
|
+
}
|
|
24
|
+
type PickerEmit = {
|
|
25
|
+
(event: 'update:open', value: boolean): void
|
|
26
|
+
(event: 'select', value: PickerSelection): void
|
|
27
|
+
(event: 'cancel'): void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Internal orchestration; public consumers use FilePicker's props and events. */
|
|
31
|
+
export function useFilePicker(props: PickerStateProps, emit: PickerEmit) {
|
|
32
|
+
const sourceNames: Record<PickerSource, string> = {
|
|
33
|
+
server: 'Server',
|
|
34
|
+
localFile: 'Local files',
|
|
35
|
+
}
|
|
36
|
+
const sourceIcons: Record<PickerSource, string> = {
|
|
37
|
+
server: 'M4 6h16v5H4zM4 13h16v5H4zM7 8.5h.01M7 15.5h.01',
|
|
38
|
+
localFile: 'M4 17h16M6 5h12v9H6z',
|
|
39
|
+
}
|
|
40
|
+
const allowedSources = computed(() => [...new Set(props.sources)])
|
|
41
|
+
const source = shallowRef<PickerSource>(
|
|
42
|
+
allowedSources.value.includes(props.initialSelection?.source as PickerSource)
|
|
43
|
+
? props.initialSelection!.source
|
|
44
|
+
: (allowedSources.value[0] ?? 'server'),
|
|
45
|
+
)
|
|
46
|
+
const query = shallowRef('')
|
|
47
|
+
const recursive = shallowRef(false)
|
|
48
|
+
const skipBlanks = shallowRef(false)
|
|
49
|
+
const reviewing = shallowRef(false)
|
|
50
|
+
const localFiles = shallowRef<File[]>([])
|
|
51
|
+
const localPaths = new WeakMap<File, string>()
|
|
52
|
+
const localAdapter = computed(() =>
|
|
53
|
+
localFilesAdapter(localFiles.value, localFiles.value.map((file) => localPaths.get(toRaw(file)))),
|
|
54
|
+
)
|
|
55
|
+
const nav = usePickerNavigation(
|
|
56
|
+
() => (source.value === 'localFile' ? localAdapter.value : props.adapter),
|
|
57
|
+
() => ({ source: source.value }),
|
|
58
|
+
(node) => props.systemFilter?.(node, source.value) !== false,
|
|
59
|
+
)
|
|
60
|
+
const capability = computed(() => props.capabilities[source.value])
|
|
61
|
+
const selection = usePickerSelection(
|
|
62
|
+
nav,
|
|
63
|
+
() => props.selectionMode,
|
|
64
|
+
() => capability.value,
|
|
65
|
+
() => skipBlanks.value,
|
|
66
|
+
)
|
|
67
|
+
const { path, trail, loading, error, rows } = nav
|
|
68
|
+
const { roots, chosen, focused, resolving, resolutionError } = selection
|
|
69
|
+
const previewNode = computed(() => {
|
|
70
|
+
const node = focused.value
|
|
71
|
+
if (!node || node.kind !== 'folder' || !selection.selected(node) || resolving.value) return node
|
|
72
|
+
const files = chosen.value.files.filter((file) =>
|
|
73
|
+
selection.ancestorSelected(file, new Set([node.path])),
|
|
74
|
+
)
|
|
75
|
+
const formats: Record<string, number> = {}
|
|
76
|
+
files.forEach((file) => {
|
|
77
|
+
formats[file.format] = (formats[file.format] ?? 0) + 1
|
|
78
|
+
})
|
|
79
|
+
return {
|
|
80
|
+
...node,
|
|
81
|
+
bytes: files.reduce((sum, file) => sum + (file.bytes ?? 0), 0),
|
|
82
|
+
fileCount: files.length,
|
|
83
|
+
formats,
|
|
84
|
+
}
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
const searchArea = ref<HTMLElement | null>(null)
|
|
88
|
+
const confirmLabel = computed(
|
|
89
|
+
() =>
|
|
90
|
+
({
|
|
91
|
+
'single-file': 'Use file',
|
|
92
|
+
'multi-file': 'Use files',
|
|
93
|
+
folder: 'Use folder',
|
|
94
|
+
'folder+files': 'Use selection',
|
|
95
|
+
})[props.selectionMode],
|
|
96
|
+
)
|
|
97
|
+
const folderCount = computed(() => roots.value.filter((node) => node.kind === 'folder').length)
|
|
98
|
+
const canConfirm = computed(
|
|
99
|
+
() =>
|
|
100
|
+
allowedSources.value.includes(source.value) &&
|
|
101
|
+
roots.value.length > 0 &&
|
|
102
|
+
chosen.value.files.length > 0 &&
|
|
103
|
+
!resolving.value &&
|
|
104
|
+
!loading.value &&
|
|
105
|
+
!error.value &&
|
|
106
|
+
!resolutionError.value,
|
|
107
|
+
)
|
|
108
|
+
let lastConfirmed: PickerSelection | null = null
|
|
109
|
+
let initialized = false
|
|
110
|
+
let timer: ReturnType<typeof setTimeout> | undefined
|
|
111
|
+
let openGeneration = 0
|
|
112
|
+
const remembered = new Map<
|
|
113
|
+
PickerSource,
|
|
114
|
+
{ path: string; trail: { path: string; label: string }[] }
|
|
115
|
+
>()
|
|
116
|
+
|
|
117
|
+
const knownFolders = new Map<PickerSource, PickerNode[]>()
|
|
118
|
+
function rememberFolders() {
|
|
119
|
+
knownFolders.set(
|
|
120
|
+
source.value,
|
|
121
|
+
[...nav.nodes.value.values()].filter((node) => node.kind === 'folder'),
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
function currentSeed() {
|
|
125
|
+
return props.initialSelection ?? lastConfirmed
|
|
126
|
+
}
|
|
127
|
+
async function restore() {
|
|
128
|
+
const token = ++openGeneration
|
|
129
|
+
const seed = currentSeed()
|
|
130
|
+
nav.register(knownFolders.get(source.value) ?? [])
|
|
131
|
+
selection.reset(seed?.source === source.value ? seed : null)
|
|
132
|
+
reviewing.value = false
|
|
133
|
+
if (!allowedSources.value.includes(source.value)) return
|
|
134
|
+
if (seed?.source === 'localFile' && source.value === 'localFile') {
|
|
135
|
+
seed.files.forEach((file, index) => {
|
|
136
|
+
if (seed.relativePaths?.[index]) localPaths.set(toRaw(file), seed.relativePaths[index]!)
|
|
137
|
+
})
|
|
138
|
+
localFiles.value = seed.files
|
|
139
|
+
}
|
|
140
|
+
await nav.load(query.value, recursive.value)
|
|
141
|
+
if (token !== openGeneration || !props.open) return
|
|
142
|
+
if (seed?.source === 'localFile' && source.value === 'localFile') {
|
|
143
|
+
// Restored local File objects are already the confirmed subset.
|
|
144
|
+
await selectLocalFiles()
|
|
145
|
+
}
|
|
146
|
+
if (selection.keys.value.size) await selection.resolve()
|
|
147
|
+
}
|
|
148
|
+
watch(
|
|
149
|
+
() => props.open,
|
|
150
|
+
(open) => {
|
|
151
|
+
clearTimeout(timer)
|
|
152
|
+
if (open) {
|
|
153
|
+
if (!initialized) {
|
|
154
|
+
path.value = props.initialPath
|
|
155
|
+
if (path.value) trail.value = [{ path: path.value, label: 'Current folder' }]
|
|
156
|
+
initialized = true
|
|
157
|
+
}
|
|
158
|
+
const seed = currentSeed()
|
|
159
|
+
if (seed && allowedSources.value.includes(seed.source) && seed.source !== source.value)
|
|
160
|
+
void switchSource(seed.source)
|
|
161
|
+
else void restore()
|
|
162
|
+
} else {
|
|
163
|
+
++openGeneration
|
|
164
|
+
rememberFolders()
|
|
165
|
+
selection.reset()
|
|
166
|
+
nav.clear()
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
{ immediate: true },
|
|
170
|
+
)
|
|
171
|
+
watch([query, recursive], () => {
|
|
172
|
+
clearTimeout(timer)
|
|
173
|
+
if (props.open)
|
|
174
|
+
timer = setTimeout(() => {
|
|
175
|
+
void nav.load(query.value, recursive.value)
|
|
176
|
+
}, 200)
|
|
177
|
+
})
|
|
178
|
+
watch(
|
|
179
|
+
() => [...selection.keys.value],
|
|
180
|
+
() => {
|
|
181
|
+
if (props.open) void selection.resolve()
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
watch(
|
|
185
|
+
() => props.selectionMode,
|
|
186
|
+
() => {
|
|
187
|
+
if (props.open) void restore()
|
|
188
|
+
},
|
|
189
|
+
)
|
|
190
|
+
watch(allowedSources, (sources) => {
|
|
191
|
+
if (!sources.includes(source.value) && sources[0]) void switchSource(sources[0])
|
|
192
|
+
})
|
|
193
|
+
onScopeDispose(() => {
|
|
194
|
+
++openGeneration
|
|
195
|
+
clearTimeout(timer)
|
|
196
|
+
selection.reset()
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
async function switchSource(next: PickerSource) {
|
|
200
|
+
if (next === source.value || !allowedSources.value.includes(next)) return
|
|
201
|
+
rememberFolders()
|
|
202
|
+
remembered.set(source.value, { path: path.value, trail: [...trail.value] })
|
|
203
|
+
clearTimeout(timer)
|
|
204
|
+
nav.clear()
|
|
205
|
+
source.value = next
|
|
206
|
+
query.value = ''
|
|
207
|
+
path.value = remembered.get(next)?.path ?? ''
|
|
208
|
+
trail.value = remembered.get(next)?.trail ?? []
|
|
209
|
+
await restore()
|
|
210
|
+
}
|
|
211
|
+
function selectNode(node: PickerNode, event: MouseEvent | KeyboardEvent) {
|
|
212
|
+
// The second click in a double-click only opens the row; it must not toggle twice.
|
|
213
|
+
if (event.type === 'click' && event.detail > 1) return
|
|
214
|
+
selection.select(node, {
|
|
215
|
+
...{ metaKey: event.metaKey, ctrlKey: event.ctrlKey, shiftKey: event.shiftKey },
|
|
216
|
+
toggle: event.type === 'click' || (event instanceof KeyboardEvent && event.key === ' '),
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
async function enter(node: PickerNode) {
|
|
220
|
+
const token = openGeneration
|
|
221
|
+
if (selection.exclusion(node)) return
|
|
222
|
+
if (node.kind === 'file') {
|
|
223
|
+
if (props.selectionMode === 'single-file') {
|
|
224
|
+
selection.select(node)
|
|
225
|
+
await nextTick()
|
|
226
|
+
await selection.resolve()
|
|
227
|
+
if (token === openGeneration && props.open) confirm()
|
|
228
|
+
}
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
clearTimeout(timer)
|
|
232
|
+
query.value = ''
|
|
233
|
+
nav.enter(node)
|
|
234
|
+
await nav.load()
|
|
235
|
+
}
|
|
236
|
+
async function up(target?: string) {
|
|
237
|
+
query.value = ''
|
|
238
|
+
clearTimeout(timer)
|
|
239
|
+
nav.up(target)
|
|
240
|
+
await nav.load()
|
|
241
|
+
}
|
|
242
|
+
async function refresh() {
|
|
243
|
+
nav.invalidate()
|
|
244
|
+
await nav.load(query.value, recursive.value, true)
|
|
245
|
+
if (selection.keys.value.size) await selection.resolve()
|
|
246
|
+
}
|
|
247
|
+
function selectParent(node: PickerNode) {
|
|
248
|
+
if (!node.parentPath) return
|
|
249
|
+
const parent = nav.nodes.value.get(node.parentPath) ?? {
|
|
250
|
+
kind: 'folder' as const,
|
|
251
|
+
path: node.parentPath,
|
|
252
|
+
name: node.parentName ?? 'Containing folder',
|
|
253
|
+
}
|
|
254
|
+
nav.register([parent])
|
|
255
|
+
selection.select(parent)
|
|
256
|
+
}
|
|
257
|
+
async function chooseLocal(files: File[], _rootName?: string, relativePaths?: string[]) {
|
|
258
|
+
const token = openGeneration
|
|
259
|
+
if (!files.length || !props.open || source.value !== 'localFile') return
|
|
260
|
+
files.forEach((file, index) => {
|
|
261
|
+
if (relativePaths?.[index]) localPaths.set(toRaw(file), relativePaths[index]!)
|
|
262
|
+
})
|
|
263
|
+
localFiles.value = files
|
|
264
|
+
clearTimeout(timer)
|
|
265
|
+
nav.clear()
|
|
266
|
+
selection.reset()
|
|
267
|
+
path.value = ''
|
|
268
|
+
trail.value = []
|
|
269
|
+
query.value = ''
|
|
270
|
+
await nav.load()
|
|
271
|
+
if (token !== openGeneration || !props.open || source.value !== 'localFile') return
|
|
272
|
+
await selectLocalFiles()
|
|
273
|
+
}
|
|
274
|
+
async function selectLocalFiles() {
|
|
275
|
+
const token = openGeneration
|
|
276
|
+
const items = props.selectionMode === 'single-file' || props.selectionMode === 'multi-file'
|
|
277
|
+
? await localAdapter.value.search('', { path: '', recursive: true })
|
|
278
|
+
: nav.entries.value
|
|
279
|
+
if (token !== openGeneration || !props.open || source.value !== 'localFile') return
|
|
280
|
+
nav.register(items)
|
|
281
|
+
const candidates = items.filter((node) => selection.selectable(node))
|
|
282
|
+
for (const node of props.selectionMode === 'single-file' || props.selectionMode === 'folder'
|
|
283
|
+
? candidates.slice(0, 1)
|
|
284
|
+
: candidates)
|
|
285
|
+
selection.select(node, { ctrlKey: true })
|
|
286
|
+
}
|
|
287
|
+
function cancel() {
|
|
288
|
+
emit('cancel')
|
|
289
|
+
emit('update:open', false)
|
|
290
|
+
}
|
|
291
|
+
function confirm() {
|
|
292
|
+
if (!canConfirm.value) return
|
|
293
|
+
const result: PickerSelection =
|
|
294
|
+
source.value === 'localFile'
|
|
295
|
+
? {
|
|
296
|
+
source: 'localFile',
|
|
297
|
+
files: chosen.value.files.flatMap((node) => (node.file ? [toRaw(node.file)] : [])),
|
|
298
|
+
}
|
|
299
|
+
: {
|
|
300
|
+
source: 'server',
|
|
301
|
+
folders: roots.value.filter((node) => node.kind === 'folder').map((node) => node.path),
|
|
302
|
+
files: [...chosen.value.files],
|
|
303
|
+
excluded: [...chosen.value.excluded],
|
|
304
|
+
}
|
|
305
|
+
if (result.source === 'localFile') {
|
|
306
|
+
const relativePaths = result.files.map((file) =>
|
|
307
|
+
localPaths.get(file) || file.webkitRelativePath || file.name,
|
|
308
|
+
)
|
|
309
|
+
if (relativePaths.some((path, index) =>
|
|
310
|
+
path !== (result.files[index]!.webkitRelativePath || result.files[index]!.name),
|
|
311
|
+
)) result.relativePaths = relativePaths
|
|
312
|
+
}
|
|
313
|
+
lastConfirmed = result
|
|
314
|
+
emit('select', result)
|
|
315
|
+
emit('update:open', false)
|
|
316
|
+
}
|
|
317
|
+
function keyboard(event: KeyboardEvent) {
|
|
318
|
+
if (
|
|
319
|
+
event.key !== '/' ||
|
|
320
|
+
event.target instanceof HTMLInputElement ||
|
|
321
|
+
event.target instanceof HTMLTextAreaElement
|
|
322
|
+
)
|
|
323
|
+
return
|
|
324
|
+
event.preventDefault()
|
|
325
|
+
searchArea.value?.querySelector('input')?.focus()
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
previewNode,
|
|
330
|
+
sourceNames,
|
|
331
|
+
sourceIcons,
|
|
332
|
+
allowedSources,
|
|
333
|
+
source,
|
|
334
|
+
query,
|
|
335
|
+
recursive,
|
|
336
|
+
skipBlanks,
|
|
337
|
+
reviewing,
|
|
338
|
+
nav,
|
|
339
|
+
selection,
|
|
340
|
+
capability,
|
|
341
|
+
path,
|
|
342
|
+
trail,
|
|
343
|
+
loading,
|
|
344
|
+
error,
|
|
345
|
+
rows,
|
|
346
|
+
roots,
|
|
347
|
+
chosen,
|
|
348
|
+
focused,
|
|
349
|
+
resolving,
|
|
350
|
+
resolutionError,
|
|
351
|
+
searchArea,
|
|
352
|
+
confirmLabel,
|
|
353
|
+
folderCount,
|
|
354
|
+
canConfirm,
|
|
355
|
+
switchSource,
|
|
356
|
+
selectNode,
|
|
357
|
+
enter,
|
|
358
|
+
up,
|
|
359
|
+
refresh,
|
|
360
|
+
selectParent,
|
|
361
|
+
chooseLocal,
|
|
362
|
+
cancel,
|
|
363
|
+
confirm,
|
|
364
|
+
keyboard,
|
|
365
|
+
}
|
|
366
|
+
}
|