@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.
Files changed (215) hide show
  1. package/README.md +30 -0
  2. package/dist/{ExperimentPopover-oL0fg_A0.js → ExperimentPopover-CJdNlCZb.js} +21 -14
  3. package/dist/ExperimentPopover-CJdNlCZb.js.map +1 -0
  4. package/dist/{ExperimentSelectorModal-BeEOomw_.js → ExperimentSelectorModal-DHE9k8TP.js} +5 -5
  5. package/dist/{ExperimentSelectorModal-BeEOomw_.js.map → ExperimentSelectorModal-DHE9k8TP.js.map} +1 -1
  6. package/dist/{SettingsModal-Dphh3RrI.js → SettingsModal-DOcCf0Vo.js} +41 -18
  7. package/dist/SettingsModal-DOcCf0Vo.js.map +1 -0
  8. package/dist/{Skeleton-DxtwJ5gt.js → Skeleton-jhF9wUkU.js} +19 -9
  9. package/dist/Skeleton-jhF9wUkU.js.map +1 -0
  10. package/dist/__tests__/apiErrors.test.d.ts +1 -0
  11. package/dist/__tests__/components/FileBrowserModal.test.d.ts +1 -0
  12. package/dist/__tests__/components/FilePicker.state.test.d.ts +1 -0
  13. package/dist/__tests__/components/FilePicker.test.d.ts +1 -0
  14. package/dist/__tests__/components/FormField.test.d.ts +1 -0
  15. package/dist/__tests__/components/NumberInput.test.d.ts +1 -0
  16. package/dist/__tests__/components/PlotlyChart.test.d.ts +1 -0
  17. package/dist/__tests__/components/iconButtonIconSizing.test.d.ts +1 -0
  18. package/dist/__tests__/composables/localFiles.test.d.ts +1 -0
  19. package/dist/__tests__/composables/useFileBrowser.test.d.ts +1 -0
  20. package/dist/__tests__/composables/usePickerSelection.test.d.ts +1 -0
  21. package/dist/__tests__/composables/usePlatformFilePickerAdapter.test.d.ts +1 -0
  22. package/dist/__tests__/styles/tokenOnlyColors.test.d.ts +1 -0
  23. package/dist/apiErrors.d.ts +26 -0
  24. package/dist/components/AppSidebar.vue.d.ts +7 -7
  25. package/dist/components/BaseInput.vue.d.ts +1 -1
  26. package/dist/components/BasePill.vue.d.ts +3 -0
  27. package/dist/components/BaseSelect.vue.d.ts +1 -1
  28. package/dist/components/BaseTabs.vue.d.ts +1 -4
  29. package/dist/components/BaseTextarea.vue.d.ts +1 -1
  30. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  31. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +2 -2
  32. package/dist/components/ConcentrationInput.vue.d.ts +1 -1
  33. package/dist/components/DatePicker.vue.d.ts +1 -1
  34. package/dist/components/DateTimePicker.vue.d.ts +1 -1
  35. package/dist/components/FileBrowserModal.vue.d.ts +99 -0
  36. package/dist/components/FilePicker.vue.d.ts +32 -0
  37. package/dist/components/FileUploader.vue.d.ts +8 -2
  38. package/dist/components/FormField.vue.d.ts +8 -1
  39. package/dist/components/FormulaInput.vue.d.ts +1 -1
  40. package/dist/components/GeneratedPlotlyResult.vue.d.ts +1 -3
  41. package/dist/components/MobileSupportGate.vue.d.ts +1 -1
  42. package/dist/components/MoleculeInput.vue.d.ts +1 -1
  43. package/dist/components/NumberInput.vue.d.ts +1 -1
  44. package/dist/components/PlotlyChart.vue.d.ts +59 -0
  45. package/dist/components/PluginWorkspaceView.props.d.ts +3 -3
  46. package/dist/components/ResourceCard.vue.d.ts +1 -1
  47. package/dist/components/SecretInput.vue.d.ts +1 -1
  48. package/dist/components/SequenceInput.vue.d.ts +1 -1
  49. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  50. package/dist/components/TagsInput.vue.d.ts +1 -1
  51. package/dist/components/TimePicker.vue.d.ts +1 -1
  52. package/dist/components/TimeRangeInput.vue.d.ts +1 -1
  53. package/dist/components/UnitInput.vue.d.ts +1 -1
  54. package/dist/components/WellPlate.vue.d.ts +1 -1
  55. package/dist/components/fileBrowserPresentation.d.ts +10 -0
  56. package/dist/components/index.d.ts +4 -0
  57. package/dist/components/index.js +8 -8
  58. package/dist/components/internal/FileBrowserDialogInternal.vue.d.ts +34 -0
  59. package/dist/components/internal/FileBrowserListingInternal.vue.d.ts +25 -0
  60. package/dist/components/internal/FileBrowserToolbarInternal.vue.d.ts +26 -0
  61. package/dist/components/internal/FilePickerPreviewInternal.vue.d.ts +18 -0
  62. package/dist/components/internal/FilePickerTreeInternal.vue.d.ts +31 -0
  63. package/dist/{components-CgAUzF8F.js → components-Cp8DcNrU.js} +4363 -1730
  64. package/dist/components-Cp8DcNrU.js.map +1 -0
  65. package/dist/composables/filePicker/localFiles.d.ts +3 -0
  66. package/dist/composables/filePicker/usePickerNavigation.d.ts +96 -0
  67. package/dist/composables/filePicker/usePickerSelection.d.ts +31 -0
  68. package/dist/composables/filePicker/validation.d.ts +5 -0
  69. package/dist/composables/index.d.ts +2 -0
  70. package/dist/composables/index.js +10 -10
  71. package/dist/composables/pluginWorkspaceModel.d.ts +0 -3
  72. package/dist/composables/useApi.d.ts +14 -7
  73. package/dist/composables/useFileBrowser.d.ts +55 -0
  74. package/dist/composables/useFilePicker.d.ts +189 -0
  75. package/dist/composables/usePlatformFilePickerAdapter.d.ts +11 -0
  76. package/dist/composables/useToast.d.ts +22 -1
  77. package/dist/{composables-B_pia3hO.js → composables-D_4qqOlp.js} +300 -11
  78. package/dist/composables-D_4qqOlp.js.map +1 -0
  79. package/dist/{errors-_W0-LbA6.js → errors-BLuqMee5.js} +78 -15
  80. package/dist/errors-BLuqMee5.js.map +1 -0
  81. package/dist/{experiment-Cwl9AYxw.js → experiment-CVDRAnqp.js} +2 -2
  82. package/dist/{experiment-Cwl9AYxw.js.map → experiment-CVDRAnqp.js.map} +1 -1
  83. package/dist/{experiment-utils-DV3DrvE_.js → experiment-utils-CV2Wza3z.js} +1 -1
  84. package/dist/{experiment-utils-DV3DrvE_.js.map → experiment-utils-CV2Wza3z.js.map} +1 -1
  85. package/dist/index.d.ts +1 -0
  86. package/dist/index.js +15 -15
  87. package/dist/install.js +2 -2
  88. package/dist/stores/index.js +2 -2
  89. package/dist/styles.css +2957 -1708
  90. package/dist/templates/index.js +2 -2
  91. package/dist/{templates-Qq113pyZ.js → templates-Dd9bBSs6.js} +2 -2
  92. package/dist/{templates-Qq113pyZ.js.map → templates-Dd9bBSs6.js.map} +1 -1
  93. package/dist/types/components.d.ts +21 -0
  94. package/dist/types/fileBrowserTypes.d.ts +77 -0
  95. package/dist/types/filePicker.d.ts +77 -0
  96. package/dist/types/form-builder.d.ts +2 -0
  97. package/dist/types/generated-ui.d.ts +2 -0
  98. package/dist/types/index.d.ts +3 -1
  99. package/dist/{useControlSchema-aMZlb4Sc.js → useControlSchema-BB2GRPhW.js} +190 -105
  100. package/dist/useControlSchema-BB2GRPhW.js.map +1 -0
  101. package/dist/{useDropdownState-DitCtrTT.js → useDropdownState-C7hRiVQf.js} +2 -2
  102. package/dist/{useDropdownState-DitCtrTT.js.map → useDropdownState-C7hRiVQf.js.map} +1 -1
  103. package/dist/{useExperimentSelector-I8HC8hUu.js → useExperimentSelector-G_CAOFTX.js} +4 -4
  104. package/dist/{useExperimentSelector-I8HC8hUu.js.map → useExperimentSelector-G_CAOFTX.js.map} +1 -1
  105. package/dist/{useFormBuilder-TCxNnqeb.js → useFormBuilder-BnGSdSRG.js} +2 -2
  106. package/dist/{useFormBuilder-TCxNnqeb.js.map → useFormBuilder-BnGSdSRG.js.map} +1 -1
  107. package/dist/{useProtocolTemplates-BmX_iVBR.js → validation-CTxuzLIX.js} +70 -13
  108. package/dist/validation-CTxuzLIX.js.map +1 -0
  109. package/package.json +1 -1
  110. package/src/__tests__/apiErrors.test.ts +81 -0
  111. package/src/__tests__/components/AppSidebar.test.ts +84 -94
  112. package/src/__tests__/components/AppToastContainer.test.ts +55 -1
  113. package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +0 -21
  114. package/src/__tests__/components/ControlWorkspaceView.test.ts +0 -2
  115. package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +0 -2
  116. package/src/__tests__/components/ExperimentPopover.test.ts +30 -0
  117. package/src/__tests__/components/FileBrowserModal.test.ts +460 -0
  118. package/src/__tests__/components/FilePicker.state.test.ts +287 -0
  119. package/src/__tests__/components/FilePicker.test.ts +487 -0
  120. package/src/__tests__/components/FormBuilder.test.ts +26 -0
  121. package/src/__tests__/components/FormField.test.ts +26 -0
  122. package/src/__tests__/components/GeneratedPathInput.test.ts +18 -0
  123. package/src/__tests__/components/GeneratedPlotlyResult.test.ts +52 -5
  124. package/src/__tests__/components/NumberInput.test.ts +105 -0
  125. package/src/__tests__/components/PlotlyChart.test.ts +305 -0
  126. package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +1 -2
  127. package/src/__tests__/components/PluginWorkspaceView.test.ts +1 -2
  128. package/src/__tests__/components/SegmentedControl.test.ts +19 -0
  129. package/src/__tests__/components/iconButtonIconSizing.test.ts +18 -0
  130. package/src/__tests__/composables/localFiles.test.ts +57 -0
  131. package/src/__tests__/composables/useApi.test.ts +92 -9
  132. package/src/__tests__/composables/useFileBrowser.test.ts +305 -0
  133. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +7 -2
  134. package/src/__tests__/composables/usePickerSelection.test.ts +98 -0
  135. package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +67 -0
  136. package/src/__tests__/composables/usePluginClient.test.ts +3 -0
  137. package/src/__tests__/generatedUi.test.ts +111 -0
  138. package/src/__tests__/styles/tokenOnlyColors.test.ts +16 -0
  139. package/src/apiErrors.ts +85 -0
  140. package/src/components/AppLayout.story.vue +1 -1
  141. package/src/components/AppSidebar.story.vue +7 -10
  142. package/src/components/AppSidebar.vue +178 -58
  143. package/src/components/AppToastContainer.vue +65 -23
  144. package/src/components/BasePill.story.vue +2 -0
  145. package/src/components/BasePill.vue +4 -0
  146. package/src/components/BaseTabs.story.vue +0 -42
  147. package/src/components/BaseTabs.vue +3 -6
  148. package/src/components/ExperimentPopover.vue +10 -2
  149. package/src/components/FileBrowserModal.story.vue +203 -0
  150. package/src/components/FileBrowserModal.vue +384 -0
  151. package/src/components/FilePicker.story.vue +44 -0
  152. package/src/components/FilePicker.vue +284 -0
  153. package/src/components/FileUploader.vue +42 -15
  154. package/src/components/FormField.vue +10 -3
  155. package/src/components/GeneratedPathInput.vue +11 -7
  156. package/src/components/GeneratedPlotlyResult.vue +34 -69
  157. package/src/components/GeneratedResultRenderer.vue +0 -5
  158. package/src/components/NumberInput.story.vue +32 -13
  159. package/src/components/NumberInput.vue +116 -55
  160. package/src/components/PlotlyChart.story.vue +61 -0
  161. package/src/components/PlotlyChart.vue +233 -0
  162. package/src/components/PluginWorkspaceView.props.ts +3 -3
  163. package/src/components/SegmentedControl.vue +2 -2
  164. package/src/components/fileBrowserPresentation.ts +39 -0
  165. package/src/components/index.ts +5 -0
  166. package/src/components/internal/FileBrowserDialogInternal.vue +75 -0
  167. package/src/components/internal/FileBrowserListingInternal.vue +197 -0
  168. package/src/components/internal/FileBrowserToolbarInternal.vue +136 -0
  169. package/src/components/internal/FilePickerPreviewInternal.vue +100 -0
  170. package/src/components/internal/FilePickerTreeInternal.vue +238 -0
  171. package/src/components/internal/FormFieldRendererInternal.vue +17 -0
  172. package/src/components/internal/WorkspaceSidebarInternal.vue +1 -2
  173. package/src/composables/filePicker/localFiles.ts +65 -0
  174. package/src/composables/filePicker/usePickerNavigation.ts +220 -0
  175. package/src/composables/filePicker/usePickerSelection.ts +270 -0
  176. package/src/composables/filePicker/validation.ts +41 -0
  177. package/src/composables/index.ts +7 -0
  178. package/src/composables/pluginWorkspaceModel.ts +0 -3
  179. package/src/composables/useApi.ts +57 -13
  180. package/src/composables/useFileBrowser.ts +314 -0
  181. package/src/composables/useFilePicker.ts +366 -0
  182. package/src/composables/useGeneratedAnalysis.ts +10 -17
  183. package/src/composables/usePlatformFilePickerAdapter.ts +104 -0
  184. package/src/composables/usePluginClient.ts +3 -0
  185. package/src/composables/usePluginWorkspace.ts +1 -2
  186. package/src/composables/useToast.ts +21 -5
  187. package/src/generated/schema.ts +58 -18
  188. package/src/index.ts +3 -0
  189. package/src/styles/components/app-sidebar.css +282 -121
  190. package/src/styles/components/file-browser-modal.css +489 -0
  191. package/src/styles/components/file-picker.css +502 -0
  192. package/src/styles/components/form-builder.css +1 -1
  193. package/src/styles/components/form-field.css +28 -0
  194. package/src/styles/components/icon-button.css +9 -0
  195. package/src/styles/components/number-input.css +73 -91
  196. package/src/styles/components/pill.css +49 -51
  197. package/src/styles/components/segmented-control.css +16 -11
  198. package/src/styles/components/tabs.css +18 -34
  199. package/src/styles/components/toast.css +136 -27
  200. package/src/styles/index.css +3 -0
  201. package/src/styles/variables.css +25 -5
  202. package/src/types/components.ts +23 -0
  203. package/src/types/fileBrowserTypes.ts +85 -0
  204. package/src/types/filePicker.ts +72 -0
  205. package/src/types/form-builder.ts +2 -0
  206. package/src/types/generated-ui.ts +2 -0
  207. package/src/types/index.ts +16 -0
  208. package/dist/ExperimentPopover-oL0fg_A0.js.map +0 -1
  209. package/dist/SettingsModal-Dphh3RrI.js.map +0 -1
  210. package/dist/Skeleton-DxtwJ5gt.js.map +0 -1
  211. package/dist/components-CgAUzF8F.js.map +0 -1
  212. package/dist/composables-B_pia3hO.js.map +0 -1
  213. package/dist/errors-_W0-LbA6.js.map +0 -1
  214. package/dist/useControlSchema-aMZlb4Sc.js.map +0 -1
  215. 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
+ }
@@ -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
- /** Raised when an authenticated SDK request is rejected because the login is no longer valid. */
107
- export class AuthenticationRequiredError extends Error {
108
- readonly code = 'AUTHENTICATION_REQUIRED'
109
- readonly status = 401
110
-
111
- constructor(readonly cause?: unknown) {
112
- super('Authentication required. Please sign in again.')
113
- this.name = 'AuthenticationRequiredError'
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?.status === 401) {
209
+ async (error: unknown) => {
210
+ if (axios.isAxiosError(error) && error.response) {
192
211
  const request = error.config as MintInternalAxiosRequestConfig | undefined
193
- if (request?._mintSkipAuth !== true) {
194
- return Promise.reject(authenticationRequiredError(error, request?._mintAuthToken))
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