@morscherlab/mint-sdk 1.2.0-beta.9 → 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 +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-DCXmwKNS.js} +5 -5
- package/dist/{ExperimentSelectorModal-BeEOomw_.js.map → ExperimentSelectorModal-DCXmwKNS.js.map} +1 -1
- package/dist/{SettingsModal-Dphh3RrI.js → SettingsModal-BYR20I4c.js} +5 -5
- package/dist/{SettingsModal-Dphh3RrI.js.map → SettingsModal-BYR20I4c.js.map} +1 -1
- package/dist/{Skeleton-DxtwJ5gt.js → Skeleton-BAF3CKY7.js} +3 -3
- package/dist/{Skeleton-DxtwJ5gt.js.map → Skeleton-BAF3CKY7.js.map} +1 -1
- 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/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/usePlatformFilePickerAdapter.test.d.ts +1 -0
- package/dist/apiErrors.d.ts +26 -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/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 +5 -2
- 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/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-DIum5hkx.js} +4286 -1866
- package/dist/components-DIum5hkx.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/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-B_pia3hO.js → composables-mFXsTA4g.js} +300 -11
- package/dist/composables-mFXsTA4g.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 +2369 -1377
- package/dist/templates/index.js +2 -2
- package/dist/{templates-Qq113pyZ.js → templates-CmGfmBRA.js} +2 -2
- package/dist/{templates-Qq113pyZ.js.map → templates-CmGfmBRA.js.map} +1 -1
- 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-aMZlb4Sc.js → useControlSchema-DldpxNOP.js} +42 -37
- package/dist/useControlSchema-DldpxNOP.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-CEcIaueJ.js} +4 -4
- package/dist/{useExperimentSelector-I8HC8hUu.js.map → useExperimentSelector-CEcIaueJ.js.map} +1 -1
- package/dist/{useFormBuilder-TCxNnqeb.js → useFormBuilder-z5L_dUIy.js} +2 -2
- package/dist/{useFormBuilder-TCxNnqeb.js.map → useFormBuilder-z5L_dUIy.js.map} +1 -1
- package/dist/{useProtocolTemplates-BmX_iVBR.js → validation-uwoJGEb6.js} +53 -8
- 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/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/GeneratedPlotlyResult.test.ts +52 -5
- package/src/__tests__/components/PlotlyChart.test.ts +305 -0
- package/src/__tests__/components/SegmentedControl.test.ts +19 -0
- package/src/__tests__/components/iconButtonIconSizing.test.ts +18 -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/usePlatformFilePickerAdapter.test.ts +67 -0
- package/src/__tests__/composables/usePluginClient.test.ts +3 -0
- package/src/apiErrors.ts +85 -0
- 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 +32 -14
- package/src/components/GeneratedPlotlyResult.vue +34 -69
- package/src/components/GeneratedResultRenderer.vue +0 -5
- package/src/components/PlotlyChart.story.vue +61 -0
- package/src/components/PlotlyChart.vue +233 -0
- 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/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 +7 -0
- 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/index.ts +3 -0
- package/src/styles/components/file-browser-modal.css +489 -0
- package/src/styles/components/file-picker.css +502 -0
- package/src/styles/components/icon-button.css +9 -0
- package/src/styles/index.css +3 -0
- package/src/types/fileBrowserTypes.ts +85 -0
- package/src/types/filePicker.ts +72 -0
- package/src/types/index.ts +14 -0
- package/dist/ExperimentPopover-oL0fg_A0.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,284 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** Adapter-driven file picker with Finder-style selection, disclosure and metadata preview. */
|
|
3
|
+
import { ref } from 'vue'
|
|
4
|
+
import BaseButton from './BaseButton.vue'
|
|
5
|
+
import BaseInput from './BaseInput.vue'
|
|
6
|
+
import IconButton from './IconButton.vue'
|
|
7
|
+
import FileUploader from './FileUploader.vue'
|
|
8
|
+
import FileBrowserDialogInternal from './internal/FileBrowserDialogInternal.vue'
|
|
9
|
+
import FilePickerTreeInternal from './internal/FilePickerTreeInternal.vue'
|
|
10
|
+
import FilePickerPreviewInternal from './internal/FilePickerPreviewInternal.vue'
|
|
11
|
+
import { formatBytes } from './fileBrowserPresentation'
|
|
12
|
+
import { useFilePicker } from '../composables/useFilePicker'
|
|
13
|
+
import { pickerReason } from '../composables/filePicker/validation'
|
|
14
|
+
import type {
|
|
15
|
+
PickerAdapter,
|
|
16
|
+
PickerCapabilities,
|
|
17
|
+
PickerNode,
|
|
18
|
+
PickerSelection,
|
|
19
|
+
PickerSelectionMode,
|
|
20
|
+
PickerSource,
|
|
21
|
+
} from '../types/filePicker'
|
|
22
|
+
|
|
23
|
+
interface Props {
|
|
24
|
+
open: boolean
|
|
25
|
+
adapter: PickerAdapter
|
|
26
|
+
selectionMode?: PickerSelectionMode
|
|
27
|
+
sources?: PickerSource[]
|
|
28
|
+
capabilities?: PickerCapabilities
|
|
29
|
+
initialPath?: string
|
|
30
|
+
initialSelection?: PickerSelection | null
|
|
31
|
+
title?: string
|
|
32
|
+
/** Host-owned visibility/selection filter; backend must still enforce access. */
|
|
33
|
+
systemFilter?: (node: PickerNode, source: PickerSource) => boolean
|
|
34
|
+
}
|
|
35
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
36
|
+
selectionMode: 'folder+files',
|
|
37
|
+
sources: () => ['server'],
|
|
38
|
+
capabilities: () => ({}),
|
|
39
|
+
initialPath: '',
|
|
40
|
+
initialSelection: null,
|
|
41
|
+
title: 'Choose data',
|
|
42
|
+
})
|
|
43
|
+
const emit = defineEmits<{
|
|
44
|
+
'update:open': [open: boolean]
|
|
45
|
+
select: [selection: PickerSelection]
|
|
46
|
+
cancel: []
|
|
47
|
+
}>()
|
|
48
|
+
const uploadError = ref('')
|
|
49
|
+
const {
|
|
50
|
+
previewNode,
|
|
51
|
+
sourceNames,
|
|
52
|
+
sourceIcons,
|
|
53
|
+
allowedSources,
|
|
54
|
+
source,
|
|
55
|
+
query,
|
|
56
|
+
recursive,
|
|
57
|
+
skipBlanks,
|
|
58
|
+
reviewing,
|
|
59
|
+
nav,
|
|
60
|
+
selection,
|
|
61
|
+
capability,
|
|
62
|
+
path,
|
|
63
|
+
trail,
|
|
64
|
+
loading,
|
|
65
|
+
error,
|
|
66
|
+
rows,
|
|
67
|
+
roots,
|
|
68
|
+
chosen,
|
|
69
|
+
focused,
|
|
70
|
+
resolving,
|
|
71
|
+
resolutionError,
|
|
72
|
+
searchArea,
|
|
73
|
+
confirmLabel,
|
|
74
|
+
folderCount,
|
|
75
|
+
canConfirm,
|
|
76
|
+
switchSource,
|
|
77
|
+
selectNode,
|
|
78
|
+
enter,
|
|
79
|
+
up,
|
|
80
|
+
refresh,
|
|
81
|
+
selectParent,
|
|
82
|
+
chooseLocal,
|
|
83
|
+
cancel,
|
|
84
|
+
confirm,
|
|
85
|
+
keyboard,
|
|
86
|
+
} = useFilePicker(props, emit)
|
|
87
|
+
</script>
|
|
88
|
+
|
|
89
|
+
<template>
|
|
90
|
+
<FileBrowserDialogInternal
|
|
91
|
+
:open="open"
|
|
92
|
+
:label="title"
|
|
93
|
+
size="full"
|
|
94
|
+
variant="picker"
|
|
95
|
+
@close="cancel"
|
|
96
|
+
>
|
|
97
|
+
<template #header>
|
|
98
|
+
<div v-if="source === 'server'" ref="searchArea" class="mint-picker__search">
|
|
99
|
+
<svg
|
|
100
|
+
viewBox="0 0 24 24"
|
|
101
|
+
fill="none"
|
|
102
|
+
stroke="currentColor"
|
|
103
|
+
stroke-width="1.8"
|
|
104
|
+
aria-hidden="true"
|
|
105
|
+
>
|
|
106
|
+
<circle cx="10.5" cy="10.5" r="7" />
|
|
107
|
+
<path d="m16 16 5 5" />
|
|
108
|
+
</svg>
|
|
109
|
+
<BaseInput
|
|
110
|
+
v-model="query"
|
|
111
|
+
aria-label="Search folders and files"
|
|
112
|
+
placeholder="Search folders and files…"
|
|
113
|
+
/>
|
|
114
|
+
<kbd>/</kbd>
|
|
115
|
+
</div>
|
|
116
|
+
<h2 v-else class="mint-file-browser__title mint-picker__local-title">{{ title }}</h2>
|
|
117
|
+
<div
|
|
118
|
+
v-if="allowedSources.length > 1"
|
|
119
|
+
class="mint-picker__sources"
|
|
120
|
+
role="group"
|
|
121
|
+
aria-label="Data source"
|
|
122
|
+
:style="{ '--picker-source-index': allowedSources.indexOf(source) }"
|
|
123
|
+
>
|
|
124
|
+
<span class="mint-picker__source-indicator" aria-hidden="true" />
|
|
125
|
+
<IconButton
|
|
126
|
+
v-for="item in allowedSources"
|
|
127
|
+
:key="item"
|
|
128
|
+
size="sm"
|
|
129
|
+
variant="ghost"
|
|
130
|
+
:label="sourceNames[item]"
|
|
131
|
+
:aria-pressed="item === source"
|
|
132
|
+
@click="switchSource(item)"
|
|
133
|
+
>
|
|
134
|
+
<svg
|
|
135
|
+
viewBox="0 0 24 24"
|
|
136
|
+
fill="none"
|
|
137
|
+
stroke="currentColor"
|
|
138
|
+
stroke-width="1.8"
|
|
139
|
+
aria-hidden="true"
|
|
140
|
+
>
|
|
141
|
+
<path :d="sourceIcons[item]" />
|
|
142
|
+
</svg>
|
|
143
|
+
</IconButton>
|
|
144
|
+
</div>
|
|
145
|
+
</template>
|
|
146
|
+
<div :key="source" class="mint-picker mint-picker__source-panel" @keydown="keyboard">
|
|
147
|
+
<div v-if="source === 'server'" class="mint-picker__pathbar">
|
|
148
|
+
<IconButton
|
|
149
|
+
label="Up one level"
|
|
150
|
+
size="sm"
|
|
151
|
+
variant="secondary"
|
|
152
|
+
:disabled="!path"
|
|
153
|
+
@click="up()"
|
|
154
|
+
><svg
|
|
155
|
+
viewBox="0 0 24 24"
|
|
156
|
+
fill="none"
|
|
157
|
+
stroke="currentColor"
|
|
158
|
+
stroke-width="2"
|
|
159
|
+
aria-hidden="true"
|
|
160
|
+
>
|
|
161
|
+
<path d="M11 17l-5-5 5-5M6 12h13" /></svg
|
|
162
|
+
></IconButton>
|
|
163
|
+
<nav aria-label="Breadcrumb" class="mint-picker__breadcrumbs">
|
|
164
|
+
<BaseButton variant="ghost" size="sm" @click="up('')">{{
|
|
165
|
+
sourceNames[source]
|
|
166
|
+
}}</BaseButton>
|
|
167
|
+
<template v-for="crumb in trail" :key="crumb.path"
|
|
168
|
+
><span>/</span
|
|
169
|
+
><BaseButton variant="ghost" size="sm" @click="up(crumb.path)">{{
|
|
170
|
+
crumb.label
|
|
171
|
+
}}</BaseButton></template
|
|
172
|
+
>
|
|
173
|
+
</nav>
|
|
174
|
+
<BaseButton
|
|
175
|
+
variant="ghost"
|
|
176
|
+
size="sm"
|
|
177
|
+
class="mint-picker__scope"
|
|
178
|
+
:aria-pressed="recursive"
|
|
179
|
+
@click="recursive = !recursive"
|
|
180
|
+
>{{ recursive ? 'Subfolders' : 'This folder' }}</BaseButton
|
|
181
|
+
>
|
|
182
|
+
<IconButton size="sm" label="Refresh location" :disabled="loading" @click="refresh()"
|
|
183
|
+
><svg
|
|
184
|
+
viewBox="0 0 24 24"
|
|
185
|
+
fill="none"
|
|
186
|
+
stroke="currentColor"
|
|
187
|
+
stroke-width="1.8"
|
|
188
|
+
aria-hidden="true"
|
|
189
|
+
>
|
|
190
|
+
<path d="M20 7v5h-5M4 17v-5h5M6 8a7 7 0 0 1 12-2l2 3M4 15l2 3a7 7 0 0 0 12-2" /></svg
|
|
191
|
+
></IconButton>
|
|
192
|
+
</div>
|
|
193
|
+
<div v-if="!allowedSources.length" class="mint-picker__empty">
|
|
194
|
+
No data sources are enabled.
|
|
195
|
+
</div>
|
|
196
|
+
<template v-else>
|
|
197
|
+
<div v-if="source === 'localFile'" class="mint-picker__local-upload">
|
|
198
|
+
<FileUploader
|
|
199
|
+
size="lg"
|
|
200
|
+
:mode="selectionMode === 'folder' ? 'folder' : selectionMode === 'single-file' ? 'file' : 'file+folder'"
|
|
201
|
+
:multiple="selectionMode !== 'single-file'"
|
|
202
|
+
:show-files="false"
|
|
203
|
+
@upload="(...args) => { uploadError = ''; chooseLocal(...args) }"
|
|
204
|
+
@error="uploadError = $event"
|
|
205
|
+
/>
|
|
206
|
+
<p v-if="uploadError || error || resolutionError" role="alert">
|
|
207
|
+
{{ uploadError || error || resolutionError }}
|
|
208
|
+
</p>
|
|
209
|
+
</div>
|
|
210
|
+
<div v-else class="mint-picker__workspace">
|
|
211
|
+
<FilePickerTreeInternal
|
|
212
|
+
:empty-title="
|
|
213
|
+
path
|
|
214
|
+
? 'No readable files'
|
|
215
|
+
: 'No server locations configured'
|
|
216
|
+
"
|
|
217
|
+
:rows="rows"
|
|
218
|
+
:loading="loading"
|
|
219
|
+
:error="error"
|
|
220
|
+
:query="query"
|
|
221
|
+
:mode="selectionMode"
|
|
222
|
+
:selected="selection.selected"
|
|
223
|
+
:reason="(node) => pickerReason(selection.exclusion(node), node)"
|
|
224
|
+
:progress="selection.folderProgress"
|
|
225
|
+
:focus-path="selection.focusPath.value"
|
|
226
|
+
@select="selectNode"
|
|
227
|
+
@enter="enter"
|
|
228
|
+
@expand="nav.expand"
|
|
229
|
+
@up="up"
|
|
230
|
+
@retry="refresh"
|
|
231
|
+
@parent="selectParent"
|
|
232
|
+
/>
|
|
233
|
+
<FilePickerPreviewInternal
|
|
234
|
+
:node="previewNode"
|
|
235
|
+
:total-count="focused?.kind === 'folder' ? focused.fileCount : undefined"
|
|
236
|
+
:source="source"
|
|
237
|
+
:exclusions="chosen.excluded"
|
|
238
|
+
:busy="resolving"
|
|
239
|
+
:error="resolutionError"
|
|
240
|
+
:reviewing="reviewing"
|
|
241
|
+
@review="reviewing = !reviewing"
|
|
242
|
+
@retry="selection.resolve"
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
</template>
|
|
246
|
+
</div>
|
|
247
|
+
<template #footer>
|
|
248
|
+
<div class="mint-picker__footer">
|
|
249
|
+
<div class="mint-picker__receipt" role="status">
|
|
250
|
+
<span v-if="folderCount" class="mint-picker__badge"
|
|
251
|
+
>{{ folderCount }} folder{{ folderCount === 1 ? '' : 's' }}</span
|
|
252
|
+
>
|
|
253
|
+
<span v-if="chosen.files.length" class="mint-picker__badge"
|
|
254
|
+
>{{ chosen.files.length }} {{ chosen.files.length === 1 ? 'file' : 'files' }}</span
|
|
255
|
+
>
|
|
256
|
+
<span v-if="roots.length">{{ resolving ? 'Checking…' : formatBytes(chosen.bytes) }}</span>
|
|
257
|
+
<span v-else>No selection</span>
|
|
258
|
+
<BaseButton
|
|
259
|
+
v-if="chosen.excluded.length && source === 'server'"
|
|
260
|
+
variant="ghost"
|
|
261
|
+
size="sm"
|
|
262
|
+
@click="reviewing = !reviewing"
|
|
263
|
+
>{{ chosen.excluded.length }} skipped</BaseButton
|
|
264
|
+
>
|
|
265
|
+
<span v-else-if="chosen.excluded.length">{{ chosen.excluded.length }} skipped</span>
|
|
266
|
+
<BaseButton
|
|
267
|
+
v-if="capability?.isBlank"
|
|
268
|
+
size="sm"
|
|
269
|
+
variant="ghost"
|
|
270
|
+
:aria-pressed="skipBlanks"
|
|
271
|
+
@click="skipBlanks = !skipBlanks"
|
|
272
|
+
>Skip blanks {{ skipBlanks ? 'on' : 'off' }}</BaseButton
|
|
273
|
+
>
|
|
274
|
+
</div>
|
|
275
|
+
<div class="mint-picker__actions">
|
|
276
|
+
<BaseButton variant="ghost" size="sm" @click="cancel">Cancel</BaseButton
|
|
277
|
+
><BaseButton size="sm" :disabled="!canConfirm" @click="confirm">{{
|
|
278
|
+
confirmLabel
|
|
279
|
+
}}</BaseButton>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</template>
|
|
283
|
+
</FileBrowserDialogInternal>
|
|
284
|
+
</template>
|
|
@@ -8,7 +8,9 @@ interface Props {
|
|
|
8
8
|
maxSize?: number
|
|
9
9
|
disabled?: boolean
|
|
10
10
|
size?: 'sm' | 'md' | 'lg'
|
|
11
|
-
mode?: 'file' | 'folder'
|
|
11
|
+
mode?: 'file' | 'folder' | 'file+folder'
|
|
12
|
+
/** Hide the selection list when a parent presents the files itself. */
|
|
13
|
+
showFiles?: boolean
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
const props = withDefaults(defineProps<Props>(), {
|
|
@@ -16,6 +18,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
16
18
|
disabled: false,
|
|
17
19
|
size: 'md',
|
|
18
20
|
mode: 'file',
|
|
21
|
+
showFiles: true,
|
|
19
22
|
})
|
|
20
23
|
|
|
21
24
|
const emit = defineEmits<{
|
|
@@ -30,9 +33,10 @@ const selectedFiles = ref<File[]>([])
|
|
|
30
33
|
const folderName = ref<string | null>(null)
|
|
31
34
|
|
|
32
35
|
const isFolder = computed(() => props.mode === 'folder')
|
|
36
|
+
const isMixed = computed(() => props.mode === 'file+folder')
|
|
33
37
|
|
|
34
38
|
const acceptLabel = computed(() => {
|
|
35
|
-
if (!props.accept) return 'any file'
|
|
39
|
+
if (!props.accept) return isFolder.value ? 'a folder' : 'any file'
|
|
36
40
|
return props.accept.split(',').map(a => a.trim().replace('.', '').toUpperCase()).join(', ')
|
|
37
41
|
})
|
|
38
42
|
|
|
@@ -154,15 +158,19 @@ function extractFolderName(files: File[]): string | null {
|
|
|
154
158
|
return null
|
|
155
159
|
}
|
|
156
160
|
|
|
157
|
-
function handleDrop(event: DragEvent) {
|
|
161
|
+
async function handleDrop(event: DragEvent) {
|
|
158
162
|
event.preventDefault()
|
|
159
163
|
isDragOver.value = false
|
|
160
164
|
if (props.disabled) return
|
|
161
165
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
+
try {
|
|
167
|
+
if (props.mode !== 'file' && event.dataTransfer?.items?.length) {
|
|
168
|
+
await handleFolderDrop(event.dataTransfer.items)
|
|
169
|
+
} else if (event.dataTransfer?.files) {
|
|
170
|
+
handleFiles(event.dataTransfer.files)
|
|
171
|
+
}
|
|
172
|
+
} catch (cause) {
|
|
173
|
+
emit('error', cause instanceof Error ? cause.message : 'Could not read the dropped folder.')
|
|
166
174
|
}
|
|
167
175
|
}
|
|
168
176
|
|
|
@@ -171,8 +179,12 @@ async function handleFolderDrop(items: DataTransferItemList) {
|
|
|
171
179
|
const relativePaths: string[] = []
|
|
172
180
|
let folderNameFromDrop: string | undefined
|
|
173
181
|
|
|
174
|
-
|
|
182
|
+
// Capture live drag data before awaiting: the browser protects it after the drop handler.
|
|
183
|
+
const dropped = Array.from(items, (item) => {
|
|
175
184
|
const entry = item.webkitGetAsEntry?.()
|
|
185
|
+
return { entry, file: entry ? null : item.getAsFile?.() }
|
|
186
|
+
})
|
|
187
|
+
for (const { entry, file: droppedFile } of dropped) {
|
|
176
188
|
if (entry?.isDirectory) {
|
|
177
189
|
folderNameFromDrop = entry.name
|
|
178
190
|
await readDirectory(
|
|
@@ -181,8 +193,10 @@ async function handleFolderDrop(items: DataTransferItemList) {
|
|
|
181
193
|
relativePaths,
|
|
182
194
|
entry.name,
|
|
183
195
|
)
|
|
184
|
-
} else
|
|
185
|
-
const file =
|
|
196
|
+
} else {
|
|
197
|
+
const file = entry?.isFile
|
|
198
|
+
? await getFile(entry as FileSystemFileEntry)
|
|
199
|
+
: droppedFile
|
|
186
200
|
if (file) {
|
|
187
201
|
files.push(file)
|
|
188
202
|
relativePaths.push(file.name)
|
|
@@ -291,7 +305,11 @@ function formatFileSize(bytes: number): string {
|
|
|
291
305
|
<div
|
|
292
306
|
role="button"
|
|
293
307
|
tabindex="0"
|
|
294
|
-
:aria-label="disabled
|
|
308
|
+
:aria-label="disabled
|
|
309
|
+
? 'File upload disabled'
|
|
310
|
+
: isMixed
|
|
311
|
+
? 'Drop files or a folder, or click to choose files'
|
|
312
|
+
: isFolder ? 'Click or drag a folder to upload' : 'Click or drag files to upload'"
|
|
295
313
|
:aria-disabled="disabled"
|
|
296
314
|
:class="[
|
|
297
315
|
'mint-file-uploader__dropzone',
|
|
@@ -345,7 +363,7 @@ function formatFileSize(bytes: number): string {
|
|
|
345
363
|
</svg>
|
|
346
364
|
|
|
347
365
|
<p class="mint-file-uploader__text">
|
|
348
|
-
Drop {
|
|
366
|
+
{{ isMixed ? 'Drop files or a folder, or click to choose files' : `Drop ${acceptLabel} or click` }}
|
|
349
367
|
</p>
|
|
350
368
|
</div>
|
|
351
369
|
</div>
|
|
@@ -356,7 +374,7 @@ function formatFileSize(bytes: number): string {
|
|
|
356
374
|
</div>
|
|
357
375
|
|
|
358
376
|
<!-- Folder summary display -->
|
|
359
|
-
<div v-if="isFolder && folderName && selectedFiles.length > 0" class="mint-file-uploader__folder-summary">
|
|
377
|
+
<div v-if="showFiles && isFolder && folderName && selectedFiles.length > 0" class="mint-file-uploader__folder-summary">
|
|
360
378
|
<div class="mint-file-uploader__folder-info">
|
|
361
379
|
<div class="mint-file-uploader__folder-icon-wrapper">
|
|
362
380
|
<svg class="mint-file-uploader__folder-icon" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
|
@@ -382,7 +400,7 @@ function formatFileSize(bytes: number): string {
|
|
|
382
400
|
|
|
383
401
|
<!-- File list display -->
|
|
384
402
|
<TransitionGroup
|
|
385
|
-
v-if="!isFolder && selectedFiles.length > 0"
|
|
403
|
+
v-if="showFiles && !isFolder && selectedFiles.length > 0"
|
|
386
404
|
tag="ul"
|
|
387
405
|
name="mint-file-uploader-item"
|
|
388
406
|
class="mint-file-uploader__list"
|
|
@@ -1,87 +1,52 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
3
2
|
import type * as PlotlyType from 'plotly.js'
|
|
3
|
+
import { computed, toRaw } from 'vue'
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import PlotlyChart from './PlotlyChart.vue'
|
|
6
6
|
|
|
7
7
|
const props = defineProps<{
|
|
8
8
|
spec: Record<string, unknown>
|
|
9
9
|
fallback?: { media_type: string; data: string }
|
|
10
10
|
}>()
|
|
11
11
|
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
async function loadPlotly(): Promise<typeof PlotlyType> {
|
|
17
|
-
const module = await import('plotly.js-dist-min')
|
|
18
|
-
return ('default' in module ? module.default : module) as unknown as typeof PlotlyType
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
async function renderChart(): Promise<void> {
|
|
22
|
-
if (!chartElement.value) return
|
|
23
|
-
failed.value = false
|
|
24
|
-
await nextTick()
|
|
25
|
-
try {
|
|
26
|
-
const renderer = plotly ?? await loadPlotly()
|
|
27
|
-
plotly = renderer
|
|
28
|
-
const data = Array.isArray(props.spec.data) ? props.spec.data : []
|
|
29
|
-
const layout = props.spec.layout && typeof props.spec.layout === 'object'
|
|
30
|
-
? props.spec.layout
|
|
31
|
-
: {}
|
|
32
|
-
await renderer.react(
|
|
33
|
-
chartElement.value,
|
|
34
|
-
data as PlotlyType.Data[],
|
|
35
|
-
layout as Partial<PlotlyType.Layout>,
|
|
36
|
-
{ responsive: true, displaylogo: false },
|
|
37
|
-
)
|
|
38
|
-
failed.value = false
|
|
39
|
-
} catch {
|
|
40
|
-
failed.value = true
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
onMounted(renderChart)
|
|
45
|
-
watch(() => props.spec, async () => {
|
|
46
|
-
await nextTick()
|
|
47
|
-
await renderChart()
|
|
48
|
-
})
|
|
49
|
-
onBeforeUnmount(() => {
|
|
50
|
-
if (plotly && chartElement.value) plotly.purge(chartElement.value)
|
|
12
|
+
const data = computed<PlotlyType.Data[]>(() => {
|
|
13
|
+
const traces = toRaw(props.spec).data
|
|
14
|
+
return Array.isArray(traces) ? [...toRaw(traces)] as PlotlyType.Data[] : []
|
|
51
15
|
})
|
|
16
|
+
const layout = computed<Partial<PlotlyType.Layout>>(() => (
|
|
17
|
+
props.spec.layout && typeof props.spec.layout === 'object'
|
|
18
|
+
? props.spec.layout as Partial<PlotlyType.Layout>
|
|
19
|
+
: {}
|
|
20
|
+
))
|
|
21
|
+
const config = computed<Partial<PlotlyType.Config>>(() => (
|
|
22
|
+
props.spec.config && typeof props.spec.config === 'object'
|
|
23
|
+
? props.spec.config as Partial<PlotlyType.Config>
|
|
24
|
+
: {}
|
|
25
|
+
))
|
|
52
26
|
</script>
|
|
53
27
|
|
|
54
28
|
<template>
|
|
55
|
-
<
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
>
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
<div
|
|
70
|
-
ref="chartElement"
|
|
71
|
-
v-show="!failed"
|
|
72
|
-
class="mint-generated-result__plotly"
|
|
73
|
-
aria-label="Interactive Plotly chart"
|
|
74
|
-
/>
|
|
75
|
-
</ChartContainer>
|
|
29
|
+
<PlotlyChart
|
|
30
|
+
title="Interactive chart"
|
|
31
|
+
:data="data"
|
|
32
|
+
:layout="layout"
|
|
33
|
+
:config="config"
|
|
34
|
+
>
|
|
35
|
+
<template v-if="fallback" #error>
|
|
36
|
+
<img
|
|
37
|
+
class="mint-generated-result__image"
|
|
38
|
+
:src="`data:${fallback.media_type};base64,${fallback.data}`"
|
|
39
|
+
alt="Static Plotly chart fallback"
|
|
40
|
+
>
|
|
41
|
+
</template>
|
|
42
|
+
</PlotlyChart>
|
|
76
43
|
</template>
|
|
77
44
|
|
|
78
45
|
<style scoped>
|
|
79
|
-
.mint-generated-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
background: var(--mint-error-bg);
|
|
85
|
-
color: var(--mint-error);
|
|
46
|
+
.mint-generated-result__image {
|
|
47
|
+
display: block;
|
|
48
|
+
max-width: 100%;
|
|
49
|
+
height: auto;
|
|
50
|
+
margin: 0 auto;
|
|
86
51
|
}
|
|
87
52
|
</style>
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type * as Plotly from 'plotly.js'
|
|
3
|
+
|
|
4
|
+
import PlotlyChart from './PlotlyChart.vue'
|
|
5
|
+
|
|
6
|
+
const data: Plotly.Data[] = [
|
|
7
|
+
{
|
|
8
|
+
type: 'scatter',
|
|
9
|
+
mode: 'lines+markers',
|
|
10
|
+
name: 'Control',
|
|
11
|
+
x: ['0 h', '6 h', '12 h', '24 h'],
|
|
12
|
+
y: [1, 1.3, 1.8, 2.1],
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
type: 'scatter',
|
|
16
|
+
mode: 'lines+markers',
|
|
17
|
+
name: 'Treatment',
|
|
18
|
+
x: ['0 h', '6 h', '12 h', '24 h'],
|
|
19
|
+
y: [1, 1.8, 3.1, 4.4],
|
|
20
|
+
},
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
const layout: Partial<Plotly.Layout> = {
|
|
24
|
+
margin: { t: 24, r: 24, b: 56, l: 56 },
|
|
25
|
+
xaxis: { title: { text: 'Time' } },
|
|
26
|
+
yaxis: { title: { text: 'Relative abundance' } },
|
|
27
|
+
}
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<template>
|
|
31
|
+
<Story title="Data Display/PlotlyChart">
|
|
32
|
+
<Variant title="Native Plotly">
|
|
33
|
+
<div style="max-width: 760px; margin: 0 auto; padding: 2rem;">
|
|
34
|
+
<PlotlyChart
|
|
35
|
+
title="Metabolite response"
|
|
36
|
+
description="Native Plotly traces with MINT theme and resize handling"
|
|
37
|
+
:data="data"
|
|
38
|
+
:layout="layout"
|
|
39
|
+
:config="{ toImageButtonOptions: { filename: 'metabolite-response' } }"
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
</Variant>
|
|
43
|
+
|
|
44
|
+
<Variant title="Empty">
|
|
45
|
+
<div style="max-width: 760px; margin: 0 auto; padding: 2rem;">
|
|
46
|
+
<PlotlyChart
|
|
47
|
+
title="Metabolite response"
|
|
48
|
+
:data="[]"
|
|
49
|
+
empty
|
|
50
|
+
empty-message="No measurements are available."
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
</Variant>
|
|
54
|
+
|
|
55
|
+
<Variant title="Loading">
|
|
56
|
+
<div style="max-width: 760px; margin: 0 auto; padding: 2rem;">
|
|
57
|
+
<PlotlyChart title="Loading measurements" :data="[]" loading />
|
|
58
|
+
</div>
|
|
59
|
+
</Variant>
|
|
60
|
+
</Story>
|
|
61
|
+
</template>
|