@morscherlab/mint-sdk 1.2.0-beta.9 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/dist/{ExperimentPopover-oL0fg_A0.js → ExperimentPopover-CJdNlCZb.js} +21 -14
- package/dist/ExperimentPopover-CJdNlCZb.js.map +1 -0
- package/dist/{ExperimentSelectorModal-BeEOomw_.js → ExperimentSelectorModal-DHE9k8TP.js} +5 -5
- package/dist/{ExperimentSelectorModal-BeEOomw_.js.map → ExperimentSelectorModal-DHE9k8TP.js.map} +1 -1
- package/dist/{SettingsModal-Dphh3RrI.js → SettingsModal-DOcCf0Vo.js} +41 -18
- package/dist/SettingsModal-DOcCf0Vo.js.map +1 -0
- package/dist/{Skeleton-DxtwJ5gt.js → Skeleton-jhF9wUkU.js} +19 -9
- package/dist/Skeleton-jhF9wUkU.js.map +1 -0
- package/dist/__tests__/apiErrors.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/FormField.test.d.ts +1 -0
- package/dist/__tests__/components/NumberInput.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/localFiles.test.d.ts +1 -0
- package/dist/__tests__/composables/useFileBrowser.test.d.ts +1 -0
- package/dist/__tests__/composables/usePickerSelection.test.d.ts +1 -0
- package/dist/__tests__/composables/usePlatformFilePickerAdapter.test.d.ts +1 -0
- package/dist/__tests__/styles/tokenOnlyColors.test.d.ts +1 -0
- package/dist/apiErrors.d.ts +26 -0
- package/dist/components/AppSidebar.vue.d.ts +7 -7
- package/dist/components/BaseInput.vue.d.ts +1 -1
- package/dist/components/BasePill.vue.d.ts +3 -0
- package/dist/components/BaseSelect.vue.d.ts +1 -1
- package/dist/components/BaseTabs.vue.d.ts +1 -4
- package/dist/components/BaseTextarea.vue.d.ts +1 -1
- 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/DatePicker.vue.d.ts +1 -1
- package/dist/components/DateTimePicker.vue.d.ts +1 -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 +8 -2
- package/dist/components/FormField.vue.d.ts +8 -1
- package/dist/components/FormulaInput.vue.d.ts +1 -1
- package/dist/components/GeneratedPlotlyResult.vue.d.ts +1 -3
- 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 +3 -3
- package/dist/components/ResourceCard.vue.d.ts +1 -1
- package/dist/components/SecretInput.vue.d.ts +1 -1
- package/dist/components/SequenceInput.vue.d.ts +1 -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 +1 -1
- package/dist/components/TimeRangeInput.vue.d.ts +1 -1
- package/dist/components/UnitInput.vue.d.ts +1 -1
- package/dist/components/WellPlate.vue.d.ts +1 -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/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-CgAUzF8F.js → components-Cp8DcNrU.js} +4363 -1730
- package/dist/components-Cp8DcNrU.js.map +1 -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 +2 -0
- package/dist/composables/index.js +10 -10
- package/dist/composables/pluginWorkspaceModel.d.ts +0 -3
- package/dist/composables/useApi.d.ts +14 -7
- package/dist/composables/useFileBrowser.d.ts +55 -0
- package/dist/composables/useFilePicker.d.ts +189 -0
- package/dist/composables/usePlatformFilePickerAdapter.d.ts +11 -0
- package/dist/composables/useToast.d.ts +22 -1
- package/dist/{composables-B_pia3hO.js → composables-D_4qqOlp.js} +300 -11
- package/dist/composables-D_4qqOlp.js.map +1 -0
- package/dist/{errors-_W0-LbA6.js → errors-BLuqMee5.js} +78 -15
- package/dist/errors-BLuqMee5.js.map +1 -0
- package/dist/{experiment-Cwl9AYxw.js → experiment-CVDRAnqp.js} +2 -2
- package/dist/{experiment-Cwl9AYxw.js.map → experiment-CVDRAnqp.js.map} +1 -1
- package/dist/{experiment-utils-DV3DrvE_.js → experiment-utils-CV2Wza3z.js} +1 -1
- package/dist/{experiment-utils-DV3DrvE_.js.map → experiment-utils-CV2Wza3z.js.map} +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -15
- package/dist/install.js +2 -2
- package/dist/stores/index.js +2 -2
- package/dist/styles.css +2957 -1708
- package/dist/templates/index.js +2 -2
- package/dist/{templates-Qq113pyZ.js → templates-Dd9bBSs6.js} +2 -2
- package/dist/{templates-Qq113pyZ.js.map → templates-Dd9bBSs6.js.map} +1 -1
- package/dist/types/components.d.ts +21 -0
- package/dist/types/fileBrowserTypes.d.ts +77 -0
- package/dist/types/filePicker.d.ts +77 -0
- package/dist/types/form-builder.d.ts +2 -0
- package/dist/types/generated-ui.d.ts +2 -0
- package/dist/types/index.d.ts +3 -1
- package/dist/{useControlSchema-aMZlb4Sc.js → useControlSchema-BB2GRPhW.js} +190 -105
- package/dist/useControlSchema-BB2GRPhW.js.map +1 -0
- package/dist/{useDropdownState-DitCtrTT.js → useDropdownState-C7hRiVQf.js} +2 -2
- package/dist/{useDropdownState-DitCtrTT.js.map → useDropdownState-C7hRiVQf.js.map} +1 -1
- package/dist/{useExperimentSelector-I8HC8hUu.js → useExperimentSelector-G_CAOFTX.js} +4 -4
- package/dist/{useExperimentSelector-I8HC8hUu.js.map → useExperimentSelector-G_CAOFTX.js.map} +1 -1
- package/dist/{useFormBuilder-TCxNnqeb.js → useFormBuilder-BnGSdSRG.js} +2 -2
- package/dist/{useFormBuilder-TCxNnqeb.js.map → useFormBuilder-BnGSdSRG.js.map} +1 -1
- package/dist/{useProtocolTemplates-BmX_iVBR.js → validation-CTxuzLIX.js} +70 -13
- package/dist/validation-CTxuzLIX.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/apiErrors.test.ts +81 -0
- package/src/__tests__/components/AppSidebar.test.ts +84 -94
- package/src/__tests__/components/AppToastContainer.test.ts +55 -1
- package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +0 -21
- package/src/__tests__/components/ControlWorkspaceView.test.ts +0 -2
- package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +0 -2
- package/src/__tests__/components/ExperimentPopover.test.ts +30 -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/FormBuilder.test.ts +26 -0
- package/src/__tests__/components/FormField.test.ts +26 -0
- package/src/__tests__/components/GeneratedPathInput.test.ts +18 -0
- package/src/__tests__/components/GeneratedPlotlyResult.test.ts +52 -5
- package/src/__tests__/components/NumberInput.test.ts +105 -0
- package/src/__tests__/components/PlotlyChart.test.ts +305 -0
- package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +1 -2
- package/src/__tests__/components/PluginWorkspaceView.test.ts +1 -2
- package/src/__tests__/components/SegmentedControl.test.ts +19 -0
- package/src/__tests__/components/iconButtonIconSizing.test.ts +18 -0
- package/src/__tests__/composables/localFiles.test.ts +57 -0
- package/src/__tests__/composables/useApi.test.ts +92 -9
- package/src/__tests__/composables/useFileBrowser.test.ts +305 -0
- package/src/__tests__/composables/useGeneratedAnalysis.test.ts +7 -2
- package/src/__tests__/composables/usePickerSelection.test.ts +98 -0
- package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +67 -0
- package/src/__tests__/composables/usePluginClient.test.ts +3 -0
- package/src/__tests__/generatedUi.test.ts +111 -0
- package/src/__tests__/styles/tokenOnlyColors.test.ts +16 -0
- package/src/apiErrors.ts +85 -0
- package/src/components/AppLayout.story.vue +1 -1
- package/src/components/AppSidebar.story.vue +7 -10
- package/src/components/AppSidebar.vue +178 -58
- package/src/components/AppToastContainer.vue +65 -23
- package/src/components/BasePill.story.vue +2 -0
- package/src/components/BasePill.vue +4 -0
- package/src/components/BaseTabs.story.vue +0 -42
- package/src/components/BaseTabs.vue +3 -6
- package/src/components/ExperimentPopover.vue +10 -2
- 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 +42 -15
- package/src/components/FormField.vue +10 -3
- package/src/components/GeneratedPathInput.vue +11 -7
- package/src/components/GeneratedPlotlyResult.vue +34 -69
- package/src/components/GeneratedResultRenderer.vue +0 -5
- package/src/components/NumberInput.story.vue +32 -13
- package/src/components/NumberInput.vue +116 -55
- package/src/components/PlotlyChart.story.vue +61 -0
- package/src/components/PlotlyChart.vue +233 -0
- package/src/components/PluginWorkspaceView.props.ts +3 -3
- package/src/components/SegmentedControl.vue +2 -2
- package/src/components/fileBrowserPresentation.ts +39 -0
- package/src/components/index.ts +5 -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/FormFieldRendererInternal.vue +17 -0
- package/src/components/internal/WorkspaceSidebarInternal.vue +1 -2
- package/src/composables/filePicker/localFiles.ts +65 -0
- package/src/composables/filePicker/usePickerNavigation.ts +220 -0
- package/src/composables/filePicker/usePickerSelection.ts +270 -0
- package/src/composables/filePicker/validation.ts +41 -0
- package/src/composables/index.ts +7 -0
- package/src/composables/pluginWorkspaceModel.ts +0 -3
- package/src/composables/useApi.ts +57 -13
- package/src/composables/useFileBrowser.ts +314 -0
- package/src/composables/useFilePicker.ts +366 -0
- package/src/composables/useGeneratedAnalysis.ts +10 -17
- package/src/composables/usePlatformFilePickerAdapter.ts +104 -0
- package/src/composables/usePluginClient.ts +3 -0
- package/src/composables/usePluginWorkspace.ts +1 -2
- package/src/composables/useToast.ts +21 -5
- package/src/generated/schema.ts +58 -18
- package/src/index.ts +3 -0
- package/src/styles/components/app-sidebar.css +282 -121
- 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 +1 -1
- package/src/styles/components/form-field.css +28 -0
- package/src/styles/components/icon-button.css +9 -0
- package/src/styles/components/number-input.css +73 -91
- package/src/styles/components/pill.css +49 -51
- package/src/styles/components/segmented-control.css +16 -11
- package/src/styles/components/tabs.css +18 -34
- package/src/styles/components/toast.css +136 -27
- package/src/styles/index.css +3 -0
- package/src/styles/variables.css +25 -5
- package/src/types/components.ts +23 -0
- package/src/types/fileBrowserTypes.ts +85 -0
- package/src/types/filePicker.ts +72 -0
- package/src/types/form-builder.ts +2 -0
- package/src/types/generated-ui.ts +2 -0
- package/src/types/index.ts +16 -0
- package/dist/ExperimentPopover-oL0fg_A0.js.map +0 -1
- package/dist/SettingsModal-Dphh3RrI.js.map +0 -1
- package/dist/Skeleton-DxtwJ5gt.js.map +0 -1
- package/dist/components-CgAUzF8F.js.map +0 -1
- package/dist/composables-B_pia3hO.js.map +0 -1
- package/dist/errors-_W0-LbA6.js.map +0 -1
- package/dist/useControlSchema-aMZlb4Sc.js.map +0 -1
- package/dist/useProtocolTemplates-BmX_iVBR.js.map +0 -1
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { computed, ref, shallowRef, watch } from 'vue'
|
|
2
|
+
import type {
|
|
3
|
+
PickerCapability,
|
|
4
|
+
PickerExcluded,
|
|
5
|
+
PickerFile,
|
|
6
|
+
PickerNode,
|
|
7
|
+
PickerSelection,
|
|
8
|
+
PickerSelectionMode,
|
|
9
|
+
} from '../../types/filePicker'
|
|
10
|
+
import type { usePickerNavigation } from './usePickerNavigation'
|
|
11
|
+
import { pickerExclusion } from './validation'
|
|
12
|
+
|
|
13
|
+
type Navigation = ReturnType<typeof usePickerNavigation>
|
|
14
|
+
export function usePickerSelection(
|
|
15
|
+
nav: Navigation,
|
|
16
|
+
mode: () => PickerSelectionMode,
|
|
17
|
+
capability: () => PickerCapability | undefined,
|
|
18
|
+
skipBlanks: () => boolean,
|
|
19
|
+
) {
|
|
20
|
+
const keys = ref(new Set<string>())
|
|
21
|
+
const dropped = ref(new Set<string>())
|
|
22
|
+
const focusPath = shallowRef('')
|
|
23
|
+
const resolving = shallowRef(false)
|
|
24
|
+
const resolutionError = shallowRef('')
|
|
25
|
+
let anchor = ''
|
|
26
|
+
let generation = 0
|
|
27
|
+
|
|
28
|
+
function ancestorSelected(node: PickerNode, candidates = keys.value): boolean {
|
|
29
|
+
const seen = new Set<string>()
|
|
30
|
+
let parent = node.parentPath
|
|
31
|
+
while (parent && !seen.has(parent)) {
|
|
32
|
+
if (candidates.has(parent)) return true
|
|
33
|
+
seen.add(parent)
|
|
34
|
+
parent = nav.nodes.value.get(parent)?.parentPath
|
|
35
|
+
}
|
|
36
|
+
return false
|
|
37
|
+
}
|
|
38
|
+
function exclusion(node: PickerNode) {
|
|
39
|
+
if (!nav.isAllowed(node))
|
|
40
|
+
return { path: node.path, name: node.name, reason: 'forbidden' as const }
|
|
41
|
+
const own = pickerExclusion(node, capability(), skipBlanks())
|
|
42
|
+
if (own) return own
|
|
43
|
+
const seen = new Set<string>()
|
|
44
|
+
let parent = node.parentPath
|
|
45
|
+
while (parent && !seen.has(parent)) {
|
|
46
|
+
seen.add(parent)
|
|
47
|
+
const folder = nav.nodes.value.get(parent)
|
|
48
|
+
if (!folder) break
|
|
49
|
+
const inherited = pickerExclusion(folder, capability(), skipBlanks())
|
|
50
|
+
if (inherited) return { ...inherited, path: node.path, name: node.name }
|
|
51
|
+
parent = folder.parentPath
|
|
52
|
+
}
|
|
53
|
+
return null
|
|
54
|
+
}
|
|
55
|
+
function selected(node: PickerNode): boolean {
|
|
56
|
+
return (
|
|
57
|
+
!exclusion(node) &&
|
|
58
|
+
!dropped.value.has(node.path) &&
|
|
59
|
+
!ancestorSelected(node, dropped.value) &&
|
|
60
|
+
(keys.value.has(node.path) || (mode() === 'folder+files' && ancestorSelected(node)))
|
|
61
|
+
)
|
|
62
|
+
}
|
|
63
|
+
function selectable(node: PickerNode): boolean {
|
|
64
|
+
if (exclusion(node)) return false
|
|
65
|
+
return node.kind === 'folder'
|
|
66
|
+
? mode() === 'folder' || mode() === 'folder+files'
|
|
67
|
+
: mode() !== 'folder'
|
|
68
|
+
}
|
|
69
|
+
function select(
|
|
70
|
+
node: PickerNode,
|
|
71
|
+
modifiers: { metaKey?: boolean; ctrlKey?: boolean; shiftKey?: boolean; toggle?: boolean } = {},
|
|
72
|
+
) {
|
|
73
|
+
if (exclusion(node)) return
|
|
74
|
+
focusPath.value = node.path
|
|
75
|
+
if (!selectable(node)) return
|
|
76
|
+
const multi = mode() === 'multi-file' || mode() === 'folder+files'
|
|
77
|
+
// Multi modes behave like a checklist: a plain click (or Space) adds or
|
|
78
|
+
// removes one row, Shift extends a range, Ctrl/Cmd is accepted for habit.
|
|
79
|
+
const additive = multi && (modifiers.metaKey || modifiers.ctrlKey || modifiers.toggle)
|
|
80
|
+
if (multi && modifiers.shiftKey && anchor) {
|
|
81
|
+
const rows = nav.rows.value.map((row) => row.node)
|
|
82
|
+
const from = rows.findIndex((item) => item.path === anchor)
|
|
83
|
+
const to = rows.findIndex((item) => item.path === node.path)
|
|
84
|
+
if (from >= 0 && to >= 0) {
|
|
85
|
+
keys.value = new Set([
|
|
86
|
+
...(additive ? keys.value : []),
|
|
87
|
+
...rows
|
|
88
|
+
.slice(Math.min(from, to), Math.max(from, to) + 1)
|
|
89
|
+
.filter(selectable)
|
|
90
|
+
.map((item) => item.path),
|
|
91
|
+
])
|
|
92
|
+
dropped.value.clear()
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (additive || (modifiers.toggle && (selected(node) || dropped.value.has(node.path)))) {
|
|
97
|
+
if (mode() === 'folder+files' && ancestorSelected(node)) {
|
|
98
|
+
if (dropped.value.has(node.path)) dropped.value.delete(node.path)
|
|
99
|
+
else dropped.value.add(node.path)
|
|
100
|
+
} else if (keys.value.has(node.path)) keys.value.delete(node.path)
|
|
101
|
+
else {
|
|
102
|
+
keys.value.add(node.path)
|
|
103
|
+
dropped.value.delete(node.path)
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
keys.value = new Set([node.path])
|
|
107
|
+
dropped.value.clear()
|
|
108
|
+
}
|
|
109
|
+
anchor = node.path
|
|
110
|
+
}
|
|
111
|
+
function reset(selection?: PickerSelection | null) {
|
|
112
|
+
++generation
|
|
113
|
+
keys.value.clear()
|
|
114
|
+
dropped.value.clear()
|
|
115
|
+
resolving.value = false
|
|
116
|
+
resolutionError.value = ''
|
|
117
|
+
focusPath.value = ''
|
|
118
|
+
if (selection?.source === 'server') {
|
|
119
|
+
const files =
|
|
120
|
+
mode() === 'folder'
|
|
121
|
+
? []
|
|
122
|
+
: mode() === 'single-file'
|
|
123
|
+
? selection.files.slice(0, 1)
|
|
124
|
+
: selection.files
|
|
125
|
+
const folders =
|
|
126
|
+
mode() === 'folder'
|
|
127
|
+
? selection.folders.slice(0, 1)
|
|
128
|
+
: mode() === 'folder+files'
|
|
129
|
+
? selection.folders
|
|
130
|
+
: []
|
|
131
|
+
nav.register(files)
|
|
132
|
+
folders.forEach((path) => {
|
|
133
|
+
if (!nav.nodes.value.has(path))
|
|
134
|
+
nav.register([
|
|
135
|
+
{ kind: 'folder', path, name: 'Selected folder', parentName: 'Selected location' },
|
|
136
|
+
])
|
|
137
|
+
keys.value.add(path)
|
|
138
|
+
})
|
|
139
|
+
files.forEach((file) => keys.value.add(file.path))
|
|
140
|
+
selection.excluded
|
|
141
|
+
.filter((item) => item.reason === 'deselected')
|
|
142
|
+
.forEach((item) => dropped.value.add(item.path))
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const roots = computed(() =>
|
|
146
|
+
[...keys.value]
|
|
147
|
+
.map((key) => nav.nodes.value.get(key))
|
|
148
|
+
.filter((node): node is PickerNode => !!node && !ancestorSelected(node) && nav.isAllowed(node)),
|
|
149
|
+
)
|
|
150
|
+
const chosen = computed(() => {
|
|
151
|
+
const files: PickerFile[] = []
|
|
152
|
+
const excluded: PickerExcluded[] = []
|
|
153
|
+
for (const node of nav.nodes.value.values()) {
|
|
154
|
+
if (!nav.isAllowed(node)) continue
|
|
155
|
+
if (!keys.value.has(node.path) && !ancestorSelected(node)) continue
|
|
156
|
+
const reason =
|
|
157
|
+
exclusion(node) ??
|
|
158
|
+
(dropped.value.has(node.path)
|
|
159
|
+
? { path: node.path, name: node.name, reason: 'deselected' as const }
|
|
160
|
+
: null)
|
|
161
|
+
// A dropped folder excludes its descendants as well.
|
|
162
|
+
const droppedByParent = ancestorSelected(node, dropped.value)
|
|
163
|
+
if (reason || droppedByParent) {
|
|
164
|
+
if (node.kind === 'file' || reason?.reason === 'unreadable')
|
|
165
|
+
excluded.push(reason ?? { path: node.path, name: node.name, reason: 'deselected' })
|
|
166
|
+
} else if (node.kind === 'file') files.push(node)
|
|
167
|
+
}
|
|
168
|
+
return { files, excluded, bytes: files.reduce((total, node) => total + (node.bytes ?? 0), 0) }
|
|
169
|
+
})
|
|
170
|
+
async function resolve() {
|
|
171
|
+
const token = ++generation
|
|
172
|
+
resolving.value = true
|
|
173
|
+
resolutionError.value = ''
|
|
174
|
+
let count = 0
|
|
175
|
+
const seen = new Set<string>()
|
|
176
|
+
async function walk(node: PickerNode, ancestors: string[] = []): Promise<PickerFile[]> {
|
|
177
|
+
if (token !== generation) throw new DOMException('Cancelled', 'AbortError')
|
|
178
|
+
if (!nav.isAllowed(node)) return []
|
|
179
|
+
if (node.kind === 'file') return [node]
|
|
180
|
+
if (node.unavailableReason) return []
|
|
181
|
+
if (ancestors.includes(node.path))
|
|
182
|
+
throw new Error('The adapter returned a circular folder tree.')
|
|
183
|
+
if (seen.has(node.path)) return []
|
|
184
|
+
seen.add(node.path)
|
|
185
|
+
if (++count > 500 || ancestors.length > 30)
|
|
186
|
+
throw new Error('This selection is too large to validate. Choose a smaller folder.')
|
|
187
|
+
const children = await nav.loadChildren(node.path, ancestors)
|
|
188
|
+
const files: PickerFile[] = []
|
|
189
|
+
for (const child of children) files.push(...(await walk(child, [...ancestors, node.path])))
|
|
190
|
+
if (token !== generation) throw new DOMException('Cancelled', 'AbortError')
|
|
191
|
+
const formats: Record<string, number> = {}
|
|
192
|
+
files.forEach((file) => {
|
|
193
|
+
formats[file.format] = (formats[file.format] ?? 0) + 1
|
|
194
|
+
})
|
|
195
|
+
nav.register([
|
|
196
|
+
{
|
|
197
|
+
...node,
|
|
198
|
+
fileCount: files.length,
|
|
199
|
+
bytes: files.reduce((sum, file) => sum + (file.bytes ?? 0), 0),
|
|
200
|
+
formats,
|
|
201
|
+
},
|
|
202
|
+
])
|
|
203
|
+
return files
|
|
204
|
+
}
|
|
205
|
+
try {
|
|
206
|
+
for (const node of roots.value) await walk(node)
|
|
207
|
+
if (token === generation) {
|
|
208
|
+
// Resolved descendants inherit their folder, including restored selections.
|
|
209
|
+
for (const key of [...keys.value]) {
|
|
210
|
+
const node = nav.nodes.value.get(key)
|
|
211
|
+
if (node && ancestorSelected(node)) keys.value.delete(key)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch (cause) {
|
|
215
|
+
if (token === generation)
|
|
216
|
+
resolutionError.value =
|
|
217
|
+
cause instanceof Error ? cause.message : 'Could not validate the selection.'
|
|
218
|
+
} finally {
|
|
219
|
+
if (token === generation) resolving.value = false
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const focused = computed(() => {
|
|
223
|
+
const node = nav.nodes.value.get(focusPath.value)
|
|
224
|
+
return node && nav.isAllowed(node) ? node : roots.value[roots.value.length - 1] ?? null
|
|
225
|
+
})
|
|
226
|
+
watch(
|
|
227
|
+
() => new Map([...nav.nodes.value].map(([path, node]) => [path, nav.isAllowed(node)])),
|
|
228
|
+
(allowed, previous) => {
|
|
229
|
+
if (keys.value.size && [...allowed].some(([path, value]) =>
|
|
230
|
+
previous.has(path) && value !== previous.get(path),
|
|
231
|
+
)) void resolve()
|
|
232
|
+
},
|
|
233
|
+
)
|
|
234
|
+
const selectedFileCounts = computed(() => {
|
|
235
|
+
const counts = new Map<string, number>()
|
|
236
|
+
for (const file of chosen.value.files) {
|
|
237
|
+
const seen = new Set<string>()
|
|
238
|
+
let parent = file.parentPath
|
|
239
|
+
while (parent && !seen.has(parent)) {
|
|
240
|
+
seen.add(parent)
|
|
241
|
+
counts.set(parent, (counts.get(parent) ?? 0) + 1)
|
|
242
|
+
parent = nav.nodes.value.get(parent)?.parentPath
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return counts
|
|
246
|
+
})
|
|
247
|
+
function folderProgress(node: PickerNode): string {
|
|
248
|
+
if (node.kind !== 'folder' || node.fileCount == null) return ''
|
|
249
|
+
const allowed = selected(node) ? (selectedFileCounts.value.get(node.path) ?? 0) : 0
|
|
250
|
+
return `${allowed} / ${node.fileCount}`
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
keys,
|
|
254
|
+
dropped,
|
|
255
|
+
focusPath,
|
|
256
|
+
resolving,
|
|
257
|
+
resolutionError,
|
|
258
|
+
roots,
|
|
259
|
+
chosen,
|
|
260
|
+
focused,
|
|
261
|
+
select,
|
|
262
|
+
selected,
|
|
263
|
+
selectable,
|
|
264
|
+
exclusion,
|
|
265
|
+
reset,
|
|
266
|
+
resolve,
|
|
267
|
+
folderProgress,
|
|
268
|
+
ancestorSelected,
|
|
269
|
+
}
|
|
270
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { PickerCapability, PickerExcluded, PickerNode } from '../../types/filePicker'
|
|
2
|
+
|
|
3
|
+
export function normalizePickerFormat(format: string): string {
|
|
4
|
+
return format.trim().replace(/^\./, '').toLowerCase()
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function pickerExclusion(
|
|
8
|
+
node: PickerNode,
|
|
9
|
+
capability?: PickerCapability,
|
|
10
|
+
skipBlanks = false,
|
|
11
|
+
): PickerExcluded | null {
|
|
12
|
+
const base = { path: node.path, name: node.name }
|
|
13
|
+
if (node.unavailableReason) return { ...base, reason: 'unreadable' }
|
|
14
|
+
if (skipBlanks && capability?.isBlank?.(node)) return { ...base, reason: 'blank' }
|
|
15
|
+
if (node.kind === 'folder') return null
|
|
16
|
+
const formats = capability?.formats.map(normalizePickerFormat) ?? []
|
|
17
|
+
if (formats.length && !formats.some((format) => node.name.toLowerCase().endsWith(`.${format}`))) {
|
|
18
|
+
return { ...base, reason: 'unsupported' }
|
|
19
|
+
}
|
|
20
|
+
if (capability?.maxBytes != null && node.bytes != null && node.bytes > capability.maxBytes) {
|
|
21
|
+
return { ...base, reason: 'tooLarge' }
|
|
22
|
+
}
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function pickerReason(excluded: PickerExcluded | null, node?: PickerNode): string {
|
|
27
|
+
if (!excluded) return ''
|
|
28
|
+
return {
|
|
29
|
+
unsupported: 'This format is not supported by this source.',
|
|
30
|
+
tooLarge: 'This file exceeds the size limit for this source.',
|
|
31
|
+
blank: 'Skipped by the blank-file rule.',
|
|
32
|
+
unreadable: node?.unavailableReason || 'This item cannot be read.',
|
|
33
|
+
deselected: 'Removed from the selection.',
|
|
34
|
+
forbidden: 'This item is not permitted by the system filter.',
|
|
35
|
+
}[excluded.reason]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function pickerFormat(name: string): string {
|
|
39
|
+
const compound = /\.(mzml|mzxml)\.gz$/i.exec(name)
|
|
40
|
+
return compound ? `${compound[1]}.gz` : name.includes('.') ? name.split('.').pop()! : ''
|
|
41
|
+
}
|
package/src/composables/index.ts
CHANGED
|
@@ -174,6 +174,11 @@ export {
|
|
|
174
174
|
type UseAnalysisArtifactsOptions,
|
|
175
175
|
type UseAnalysisArtifactsReturn,
|
|
176
176
|
} from './useAnalysisArtifacts'
|
|
177
|
+
export {
|
|
178
|
+
useFileBrowser,
|
|
179
|
+
type UseFileBrowserOptions,
|
|
180
|
+
type UseFileBrowserReturn,
|
|
181
|
+
} from './useFileBrowser'
|
|
177
182
|
export {
|
|
178
183
|
buildAnalysisArtifactTargetQuery,
|
|
179
184
|
parseAnalysisArtifactTarget,
|
|
@@ -564,3 +569,5 @@ export {
|
|
|
564
569
|
type UseMenuKeyboardOptions,
|
|
565
570
|
type UseMenuKeyboardReturn,
|
|
566
571
|
} from './useMenuKeyboard'
|
|
572
|
+
|
|
573
|
+
export { usePlatformFilePickerAdapter, encodePlatformPickerPath, decodePlatformPickerPath } from './usePlatformFilePickerAdapter'
|
|
@@ -16,7 +16,6 @@ import type {
|
|
|
16
16
|
ControlSchema,
|
|
17
17
|
ControlWorkspaceOptions,
|
|
18
18
|
} from './useControlSchema'
|
|
19
|
-
import type { PluginWorkspaceSidebarVariant } from '../components/PluginWorkspaceView.props'
|
|
20
19
|
|
|
21
20
|
/**
|
|
22
21
|
* Scoped-slot payload shared by PluginWorkspaceView's `default`, `topbar`, and `sidebar`
|
|
@@ -125,8 +124,6 @@ export interface PluginWorkspaceSidebarModel {
|
|
|
125
124
|
subtitle?: string
|
|
126
125
|
/** Optional compact badge/count rendered in the sidebar header. */
|
|
127
126
|
badge?: string | number
|
|
128
|
-
/** AppSidebar visual preset. */
|
|
129
|
-
variant: PluginWorkspaceSidebarVariant
|
|
130
127
|
/** Map of view IDs to sidebar tool sections. */
|
|
131
128
|
panels: Record<string, SidebarToolSection[]>
|
|
132
129
|
/** Resolved active view, used to pick which panels/forms render. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import axios, { type AxiosInstance, type AxiosRequestConfig, type InternalAxiosRequestConfig } from 'axios'
|
|
2
2
|
import { useSettingsStore } from '../stores/settings'
|
|
3
3
|
import { useAuthStore } from '../stores/auth'
|
|
4
|
+
import { MintApiError, mintApiErrorFromResponse } from '../apiErrors'
|
|
4
5
|
|
|
5
6
|
let apiClientInstance: AxiosInstance | null = null
|
|
6
7
|
let interceptorAttached = false
|
|
@@ -8,11 +9,13 @@ let interceptorAttached = false
|
|
|
8
9
|
interface MintAxiosRequestConfig extends AxiosRequestConfig {
|
|
9
10
|
_mintSkipAuth?: boolean
|
|
10
11
|
_mintAuthToken?: string
|
|
12
|
+
_mintTypedErrors?: boolean
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
interface MintInternalAxiosRequestConfig extends InternalAxiosRequestConfig {
|
|
14
16
|
_mintSkipAuth?: boolean
|
|
15
17
|
_mintAuthToken?: string
|
|
18
|
+
_mintTypedErrors?: boolean
|
|
16
19
|
}
|
|
17
20
|
|
|
18
21
|
type MutableHeaders = Record<string, unknown> & {
|
|
@@ -103,14 +106,27 @@ function getAuthorizationHeader(headers: AxiosRequestConfig['headers']): unknown
|
|
|
103
106
|
return key ? bag[key] : undefined
|
|
104
107
|
}
|
|
105
108
|
|
|
106
|
-
/**
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
/**
|
|
110
|
+
* Raised when an authenticated SDK request is rejected because the login is no
|
|
111
|
+
* longer valid. It is also a `MintApiError` (status 401), so typed-error callers
|
|
112
|
+
* and `instanceof AuthenticationRequiredError` callers both recognise it.
|
|
113
|
+
*/
|
|
114
|
+
export class AuthenticationRequiredError extends MintApiError {
|
|
115
|
+
override readonly name: string = 'AuthenticationRequiredError'
|
|
116
|
+
declare readonly code: 'AUTHENTICATION_REQUIRED'
|
|
117
|
+
declare readonly status: 401
|
|
118
|
+
|
|
119
|
+
constructor(cause?: unknown, envelope?: MintApiError) {
|
|
120
|
+
super(
|
|
121
|
+
'Authentication required. Please sign in again.',
|
|
122
|
+
'AUTHENTICATION_REQUIRED',
|
|
123
|
+
401,
|
|
124
|
+
envelope?.requestId ?? null,
|
|
125
|
+
envelope?.details ?? null,
|
|
126
|
+
envelope?.detail,
|
|
127
|
+
envelope?.body ?? null,
|
|
128
|
+
cause,
|
|
129
|
+
)
|
|
114
130
|
}
|
|
115
131
|
}
|
|
116
132
|
|
|
@@ -118,12 +134,13 @@ export class AuthenticationRequiredError extends Error {
|
|
|
118
134
|
export function authenticationRequiredError(
|
|
119
135
|
cause: unknown,
|
|
120
136
|
rejectedToken?: string,
|
|
137
|
+
envelope?: MintApiError,
|
|
121
138
|
): AuthenticationRequiredError {
|
|
122
139
|
const currentAuthStore = useAuthStore()
|
|
123
140
|
if (rejectedToken && currentAuthStore.token === rejectedToken) {
|
|
124
141
|
currentAuthStore.clearToken()
|
|
125
142
|
}
|
|
126
|
-
return new AuthenticationRequiredError(cause)
|
|
143
|
+
return new AuthenticationRequiredError(cause, envelope)
|
|
127
144
|
}
|
|
128
145
|
|
|
129
146
|
function getApiClient(): AxiosInstance {
|
|
@@ -141,6 +158,8 @@ export interface ApiClientOptions {
|
|
|
141
158
|
baseUrl?: string
|
|
142
159
|
timeout?: number
|
|
143
160
|
withAuth?: boolean
|
|
161
|
+
/** Normalize failed responses into MintApiError. Generated clients enable this by default. */
|
|
162
|
+
typedErrors?: boolean
|
|
144
163
|
}
|
|
145
164
|
|
|
146
165
|
export interface UseApiReturn {
|
|
@@ -187,11 +206,35 @@ export function useApi(options: ApiClientOptions = {}): UseApiReturn {
|
|
|
187
206
|
})
|
|
188
207
|
apiClient.interceptors.response.use(
|
|
189
208
|
(response) => response,
|
|
190
|
-
(error: unknown) => {
|
|
191
|
-
if (axios.isAxiosError(error) && error.response
|
|
209
|
+
async (error: unknown) => {
|
|
210
|
+
if (axios.isAxiosError(error) && error.response) {
|
|
192
211
|
const request = error.config as MintInternalAxiosRequestConfig | undefined
|
|
193
|
-
if (request?.
|
|
194
|
-
|
|
212
|
+
if (request?._mintTypedErrors) {
|
|
213
|
+
let payload: unknown = error.response.data
|
|
214
|
+
if (payload instanceof Blob) {
|
|
215
|
+
try {
|
|
216
|
+
payload = JSON.parse(await payload.text())
|
|
217
|
+
} catch {
|
|
218
|
+
// Keep non-JSON download errors available as the original body.
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const typed = mintApiErrorFromResponse(
|
|
222
|
+
error.response.status,
|
|
223
|
+
payload,
|
|
224
|
+
{
|
|
225
|
+
requestId: String(error.response.headers?.['x-request-id'] ?? '') || null,
|
|
226
|
+
cause: error,
|
|
227
|
+
},
|
|
228
|
+
)
|
|
229
|
+
if (error.response.status === 401 && request._mintSkipAuth !== true) {
|
|
230
|
+
return Promise.reject(authenticationRequiredError(error, request._mintAuthToken, typed))
|
|
231
|
+
}
|
|
232
|
+
return Promise.reject(typed)
|
|
233
|
+
}
|
|
234
|
+
if (error.response.status === 401) {
|
|
235
|
+
if (request?._mintSkipAuth !== true) {
|
|
236
|
+
return Promise.reject(authenticationRequiredError(error, request?._mintAuthToken))
|
|
237
|
+
}
|
|
195
238
|
}
|
|
196
239
|
}
|
|
197
240
|
return Promise.reject(error)
|
|
@@ -213,6 +256,7 @@ export function useApi(options: ApiClientOptions = {}): UseApiReturn {
|
|
|
213
256
|
const base: MintAxiosRequestConfig = {
|
|
214
257
|
baseURL: getBaseUrl(),
|
|
215
258
|
timeout: options.timeout ?? settingsStore.requestTimeout,
|
|
259
|
+
_mintTypedErrors: options.typedErrors,
|
|
216
260
|
...config,
|
|
217
261
|
}
|
|
218
262
|
// Strip auth header if explicitly disabled
|