@morscherlab/mint-sdk 1.2.0-beta.8 → 1.2.0
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 +60 -0
- package/dist/{ExperimentPopover-CfOdh5Rm.js → ExperimentPopover-CJdNlCZb.js} +24 -15
- package/dist/ExperimentPopover-CJdNlCZb.js.map +1 -0
- package/dist/{ExperimentSelectorModal-DvIagrxY.js → ExperimentSelectorModal-DCXmwKNS.js} +5 -5
- package/dist/ExperimentSelectorModal-DCXmwKNS.js.map +1 -0
- package/dist/{SettingsModal-W3TYfnP9.js → SettingsModal-BYR20I4c.js} +5 -5
- package/dist/SettingsModal-BYR20I4c.js.map +1 -0
- package/dist/{EmptyState-NBqf2_8z.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/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__/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/AppPluginSwitcher.vue.d.ts +2 -0
- package/dist/components/AuditTrail.vue.d.ts +13 -0
- 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 +2 -2
- 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/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/FormulaInput.vue.d.ts +1 -1
- package/dist/components/GeneratedPlotlyResult.vue.d.ts +1 -3
- 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/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/SampleSelector.drag.d.ts +13 -0
- package/dist/components/SampleSelector.tree.d.ts +2 -0
- package/dist/components/SampleSelector.vue.d.ts +34 -1
- package/dist/components/ScheduleCalendar.vue.d.ts +20 -0
- package/dist/components/SecretInput.vue.d.ts +1 -1
- package/dist/components/SequenceInput.vue.d.ts +1 -1
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -0
- 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 +4 -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-B80gV0P6.js → components-DIum5hkx.js} +5415 -2753
- 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/index.d.ts +3 -0
- package/dist/composables/index.js +10 -10
- package/dist/composables/useApi.d.ts +14 -7
- 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/useListReorder.d.ts +90 -0
- package/dist/composables/useMenuKeyboard.d.ts +2 -2
- package/dist/composables/usePlatformFilePickerAdapter.d.ts +11 -0
- package/dist/composables/usePluginConfig.d.ts +3 -1
- package/dist/composables/useRequestSyncState.d.ts +42 -2
- package/dist/composables/useScheduleDrag.d.ts +1 -0
- package/dist/{composables-C8mmD5ta.js → composables-mFXsTA4g.js} +307 -74
- package/dist/composables-mFXsTA4g.js.map +1 -0
- package/dist/{useApi-CDBq6z23.js → errors-BLuqMee5.js} +178 -16
- package/dist/errors-BLuqMee5.js.map +1 -0
- package/dist/{experiment-Y9Iz7KYi.js → experiment-CVDRAnqp.js} +3 -8
- package/dist/experiment-CVDRAnqp.js.map +1 -0
- package/dist/{experiment-utils-DV3DrvE_.js → experiment-utils-CV2Wza3z.js} +1 -1
- package/dist/experiment-utils-CV2Wza3z.js.map +1 -0
- package/dist/index.d.ts +1 -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/index.d.ts +1 -1
- package/dist/stores/index.js +3 -3
- package/dist/stores/settings.d.ts +31 -1
- package/dist/styles.css +15162 -32475
- package/dist/templates/index.js +2 -4
- package/dist/{templates-CK72aKO1.js → templates-CmGfmBRA.js} +291 -85
- package/dist/templates-CmGfmBRA.js.map +1 -0
- package/dist/types/fileBrowserTypes.d.ts +77 -0
- package/dist/types/filePicker.d.ts +77 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/{useControlSchema-_OVhavKq.js → useControlSchema-DldpxNOP.js} +234 -1121
- package/dist/useControlSchema-DldpxNOP.js.map +1 -0
- package/dist/{useDropdownState-D5G4UvWd.js → useDropdownState-C7hRiVQf.js} +22 -6
- package/dist/useDropdownState-C7hRiVQf.js.map +1 -0
- package/dist/{useExperimentSelector-DSEckf_g.js → useExperimentSelector-CEcIaueJ.js} +4 -4
- package/dist/{useExperimentSelector-DSEckf_g.js.map → useExperimentSelector-CEcIaueJ.js.map} +1 -1
- package/dist/{useFormBuilder-Bd714C7y.js → useFormBuilder-z5L_dUIy.js} +2 -2
- package/dist/{useFormBuilder-Bd714C7y.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/{useProtocolTemplates-Dq5Pzwyd.js → validation-uwoJGEb6.js} +231 -158
- 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/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/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/RackEditor.test.ts +35 -0
- package/src/__tests__/components/ReagentList.test.ts +64 -2
- package/src/__tests__/components/RetiredStyles.test.ts +31 -0
- package/src/__tests__/components/SampleSelector.drag.test.ts +51 -1
- package/src/__tests__/components/ScheduleCalendar.test.ts +63 -0
- package/src/__tests__/components/SegmentedControl.test.ts +107 -0
- package/src/__tests__/components/TimePicker.test.ts +61 -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/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 +3 -0
- package/src/__tests__/composables/useRequestSyncState.test.ts +140 -0
- package/src/__tests__/stores/settings.test.ts +71 -1
- package/src/__tests__/styles/cssTokens.test.ts +131 -0
- package/src/__tests__/utils/permissions.test.ts +68 -0
- package/src/apiErrors.ts +85 -0
- package/src/components/ActionMenu.vue +45 -88
- package/src/components/AlertBox.vue +0 -4
- package/src/components/AppAvatarMenu.vue +2 -14
- package/src/components/AppLayout.vue +0 -4
- package/src/components/AppPluginSwitcher.story.vue +87 -0
- package/src/components/AppPluginSwitcher.vue +2 -5
- package/src/components/AppSidebar.vue +0 -4
- package/src/components/AppToastContainer.vue +6 -8
- package/src/components/AppTopBar.vue +0 -4
- package/src/components/ArtifactSaveDialog.vue +0 -4
- package/src/components/ArtifactSelectorModal.vue +0 -4
- package/src/components/AuditTrail.story.vue +18 -0
- package/src/components/AuditTrail.vue +27 -7
- package/src/components/AutoGroupModal.vue +0 -4
- package/src/components/Avatar.vue +0 -4
- package/src/components/BaseButton.vue +0 -4
- package/src/components/BaseCheckbox.vue +0 -4
- package/src/components/BaseInput.vue +0 -4
- package/src/components/BaseModal.vue +0 -4
- package/src/components/BasePill.vue +0 -4
- package/src/components/BaseRadioGroup.vue +0 -4
- package/src/components/BaseSelect.vue +0 -4
- package/src/components/BaseSlider.vue +0 -4
- package/src/components/BaseTabs.vue +0 -4
- package/src/components/BaseTextarea.vue +0 -4
- package/src/components/BaseToggle.vue +0 -4
- package/src/components/BatchProgressList.story.vue +18 -0
- package/src/components/BatchProgressList.vue +32 -4
- package/src/components/Breadcrumb.vue +0 -4
- package/src/components/Calendar.vue +0 -4
- package/src/components/ChartContainer.story.vue +1 -1
- package/src/components/ChartContainer.vue +0 -4
- package/src/components/ChemicalFormula.vue +0 -4
- package/src/components/CollapsibleCard.vue +0 -4
- package/src/components/ColorSlider.vue +0 -4
- package/src/components/ConcentrationInput.vue +0 -4
- package/src/components/ConfirmDialog.vue +0 -4
- package/src/components/DataFrame.story.vue +14 -0
- package/src/components/DataFrame.vue +97 -23
- package/src/components/DatePicker.vue +73 -27
- package/src/components/DateTimePicker.vue +45 -17
- package/src/components/Divider.vue +0 -4
- package/src/components/DoseCalculator.vue +0 -4
- package/src/components/DropdownButton.vue +2 -14
- package/src/components/EmptyState.vue +0 -4
- package/src/components/ExperimentCodeBadge.vue +0 -4
- package/src/components/ExperimentDataViewer.story.vue +3 -0
- package/src/components/ExperimentDataViewer.vue +33 -13
- package/src/components/ExperimentPopover.vue +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 +0 -4
- package/src/components/LcmsSequenceTable.vue +81 -50
- package/src/components/LoadingSpinner.vue +0 -4
- package/src/components/MobileSupportGate.vue +0 -4
- package/src/components/MoleculeInput.vue +0 -4
- package/src/components/MultiSelect.vue +0 -4
- package/src/components/NumberInput.vue +0 -4
- package/src/components/PlateMapEditor.vue +3 -14
- package/src/components/PlotlyChart.story.vue +61 -0
- package/src/components/PlotlyChart.vue +233 -0
- package/src/components/PluginIcon.vue +0 -4
- package/src/components/ProgressBar.vue +0 -4
- package/src/components/ProtocolStepEditor.vue +2 -4
- package/src/components/RackEditor.vue +51 -70
- package/src/components/ReagentEditor.vue +0 -4
- package/src/components/ReagentList.story.vue +18 -0
- package/src/components/ReagentList.vue +127 -82
- package/src/components/ResourceCard.vue +0 -4
- package/src/components/RuntimeUpdateNotice.story.vue +49 -0
- package/src/components/RuntimeUpdateNotice.vue +4 -4
- package/src/components/SampleHierarchyTree.story.vue +18 -0
- package/src/components/SampleHierarchyTree.vue +32 -9
- package/src/components/SampleLegend.vue +2 -8
- package/src/components/SampleSelector.drag.ts +51 -0
- package/src/components/SampleSelector.story.vue +6 -0
- package/src/components/SampleSelector.tree.ts +2 -0
- package/src/components/SampleSelector.vue +49 -10
- package/src/components/SampleSelectorGroupNode.vue +19 -0
- package/src/components/ScheduleCalendar.story.vue +6 -0
- package/src/components/ScheduleCalendar.vue +82 -7
- package/src/components/ScientificNumber.vue +0 -4
- package/src/components/SectionCard.vue +0 -4
- package/src/components/SegmentedControl.vue +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 +5 -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 +0 -4
- package/src/composables/controlSchemaTypes.ts +34 -0
- 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/index.ts +17 -0
- package/src/composables/useApi.ts +57 -13
- 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/useGeneratedAnalysis.ts +18 -26
- package/src/composables/useListReorder.story.vue +143 -0
- package/src/composables/useListReorder.ts +242 -0
- package/src/composables/useMenuKeyboard.ts +2 -2
- package/src/composables/useOptimisticMutation.ts +1 -15
- package/src/composables/usePlatformFilePickerAdapter.ts +104 -0
- package/src/composables/usePluginClient.ts +3 -0
- package/src/composables/usePluginConfig.ts +3 -1
- package/src/composables/useRackEditor.ts +2 -11
- package/src/composables/useRequestSyncState.ts +98 -28
- package/src/composables/useResourceCrud.ts +1 -15
- package/src/composables/useScheduleDrag.ts +37 -0
- package/src/composables/useWellPainting.ts +5 -29
- package/src/composables/useWellPlateEditor.ts +1 -11
- package/src/index.ts +3 -0
- package/src/permissions.ts +48 -9
- package/src/stores/experiment.ts +2 -7
- package/src/stores/index.ts +9 -1
- package/src/stores/settings.ts +93 -1
- package/src/styles/components/audit-trail.css +5 -5
- package/src/styles/components/batch-progress-list.css +7 -0
- package/src/styles/components/color-slider.css +2 -2
- package/src/styles/components/data-state.css +30 -0
- package/src/styles/components/dataframe.css +29 -35
- package/src/styles/components/experiment-data-viewer.css +4 -4
- package/src/styles/components/experiment-timeline.css +3 -30
- package/src/styles/components/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/well-plate.css +8 -0
- package/src/styles/index.css +11 -0
- package/src/styles/variables.css +29 -0
- package/src/templates/builderPresetControls.ts +1 -1
- package/src/templates/componentPlateHelpers.ts +3 -1
- package/src/templates/doseResponseAdapters.ts +2 -13
- package/src/templates/elisaAssayCollectionBuilder.ts +2 -2
- package/src/templates/plateMapBuilder.ts +2 -2
- package/src/templates/qpcrPlateBuilder.ts +1 -1
- package/src/templates/templateValidators.ts +1 -1
- package/src/templates/timeCourseBuilder.ts +2 -2
- package/src/templates/wellPlateScreenCollectionBuilder.ts +2 -2
- package/src/types/fileBrowserTypes.ts +85 -0
- package/src/types/filePicker.ts +72 -0
- package/src/types/index.ts +14 -0
- package/src/utils/color.ts +51 -0
- package/src/utils/errors.ts +29 -0
- package/dist/EmptyState-NBqf2_8z.js.map +0 -1
- package/dist/ExperimentPopover-CfOdh5Rm.js.map +0 -1
- package/dist/ExperimentSelectorModal-DvIagrxY.js.map +0 -1
- package/dist/SettingsModal-W3TYfnP9.js.map +0 -1
- package/dist/components-B80gV0P6.js.map +0 -1
- package/dist/composables/autoGroupConstants.d.ts +0 -1
- package/dist/composables/autoGroupGrouping.d.ts +0 -3
- package/dist/composables/useAutoGroupInputSources.d.ts +0 -32
- package/dist/composables-C8mmD5ta.js.map +0 -1
- package/dist/experiment-Y9Iz7KYi.js.map +0 -1
- package/dist/experiment-utils-DV3DrvE_.js.map +0 -1
- package/dist/templates/builderDefaults.d.ts +0 -1
- package/dist/templates/plateGeometry.d.ts +0 -4
- package/dist/templates-CK72aKO1.js.map +0 -1
- package/dist/useApi-CDBq6z23.js.map +0 -1
- package/dist/useControlSchema-_OVhavKq.js.map +0 -1
- package/dist/useDropdownState-D5G4UvWd.js.map +0 -1
- package/dist/useProtocolTemplates-Dq5Pzwyd.js.map +0 -1
- package/src/__tests__/composables/useAutoGroupInputSources.test.ts +0 -127
- package/src/composables/autoGroupConstants.ts +0 -4
- package/src/composables/autoGroupGrouping.ts +0 -148
- package/src/composables/useAutoGroupInputSources.ts +0 -147
- package/src/templates/builderDefaults.ts +0 -11
- package/src/templates/plateGeometry.ts +0 -62
- /package/dist/__tests__/{composables/useAutoGroupInputSources.test.d.ts → 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
|
+
}
|