@morscherlab/mint-sdk 1.3.0-beta.3 → 1.3.0-beta.4
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/dist/components/FilePicker.vue.d.ts +4 -1
- package/dist/components/index.js +1 -1
- package/dist/components/internal/FilePickerListInternal.vue.d.ts +2 -0
- package/dist/{components-CvY105kJ.js → components-CF0XMy8W.js} +326 -234
- package/dist/components-CF0XMy8W.js.map +1 -0
- package/dist/composables/createFilePickerAdapter.d.ts +5 -1
- package/dist/composables/filePicker/usePickerNavigation.d.ts +3 -2
- package/dist/composables/index.js +1 -1
- package/dist/composables/useFilePicker.d.ts +4 -1
- package/dist/{composables-DL4Vca7L.js → composables-Dlyz_Aiv.js} +92 -29
- package/dist/{composables-DL4Vca7L.js.map → composables-Dlyz_Aiv.js.map} +1 -1
- package/dist/index.js +2 -2
- package/dist/install.js +1 -1
- package/dist/{plotly.min-D6Ge6qgX.js → plotly.min-DDhZCNEM.js} +2 -2
- package/dist/{plotly.min-D6Ge6qgX.js.map → plotly.min-DDhZCNEM.js.map} +1 -1
- package/dist/styles.css +32 -1
- package/dist/types/auth.d.ts +4 -0
- package/dist/types/components.d.ts +4 -0
- package/dist/types/fileBrowserTypes.d.ts +13 -0
- package/dist/types/filePicker.d.ts +8 -1
- package/dist/types/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/components/ActionMenu.test.ts +10 -0
- package/src/__tests__/components/FilePicker.test.ts +65 -1
- package/src/__tests__/composables/createFilePickerAdapter.test.ts +92 -1
- package/src/__tests__/composables/usePickerNavigation.test.ts +61 -1
- package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +18 -6
- package/src/components/ActionMenu.vue +10 -2
- package/src/components/FilePicker.vue +42 -0
- package/src/components/internal/FilePickerListInternal.vue +11 -2
- package/src/composables/createFilePickerAdapter.ts +82 -22
- package/src/composables/filePicker/usePickerNavigation.ts +29 -2
- package/src/composables/useFilePicker.ts +8 -1
- package/src/composables/usePlatformFilePickerAdapter.ts +26 -4
- package/src/styles/components/action-menu.css +10 -0
- package/src/styles/components/file-picker.css +23 -1
- package/src/types/auth.ts +4 -0
- package/src/types/components.ts +4 -0
- package/src/types/fileBrowserTypes.ts +15 -0
- package/src/types/filePicker.ts +8 -1
- package/src/types/index.ts +2 -0
- package/dist/components-CvY105kJ.js.map +0 -1
|
@@ -22,6 +22,8 @@ const props = defineProps<{
|
|
|
22
22
|
loading: boolean
|
|
23
23
|
error: string
|
|
24
24
|
query: string
|
|
25
|
+
/** Search results stopped at a limit; the rows are the first matches only. */
|
|
26
|
+
truncated?: boolean
|
|
25
27
|
mode: PickerSelectionMode
|
|
26
28
|
selected: (node: PickerNode) => boolean
|
|
27
29
|
selectable: (node: PickerNode) => boolean
|
|
@@ -171,6 +173,9 @@ function parts(name: string) {
|
|
|
171
173
|
<div class="mint-picker__list-head" aria-hidden="true">
|
|
172
174
|
<span /><span>Name</span><span>Size</span><span>Modified</span>
|
|
173
175
|
</div>
|
|
176
|
+
<p v-if="truncated && !loading && !error && rows.length" class="mint-picker__truncated" role="status">
|
|
177
|
+
Showing the first {{ rows.length }} matches. Search a smaller folder or use a more specific search to see the rest.
|
|
178
|
+
</p>
|
|
174
179
|
<div v-if="loading" class="mint-picker__skeleton">
|
|
175
180
|
<Skeleton v-for="n in 6" :key="n" width="100%" height="1.25rem" />
|
|
176
181
|
</div>
|
|
@@ -180,10 +185,14 @@ function parts(name: string) {
|
|
|
180
185
|
</div>
|
|
181
186
|
<div v-else-if="!rows.length" class="mint-picker__empty" role="status">
|
|
182
187
|
<strong>{{
|
|
183
|
-
|
|
188
|
+
truncated
|
|
189
|
+
? 'Search stopped before finding a match'
|
|
190
|
+
: query ? 'No matching folders or files' : emptyTitle || 'No readable files'
|
|
184
191
|
}}</strong>
|
|
185
192
|
<span>{{
|
|
186
|
-
|
|
193
|
+
truncated
|
|
194
|
+
? 'Search a smaller folder or use a more specific search.'
|
|
195
|
+
: query ? 'Try a different search or include subfolders.' : 'Choose another location.'
|
|
187
196
|
}}</span>
|
|
188
197
|
</div>
|
|
189
198
|
<template v-for="row in rows" v-else :key="row.node.path">
|
|
@@ -1,10 +1,26 @@
|
|
|
1
|
-
import type { PickerAdapter, PickerNode, PickerRequest } from '../types/filePicker'
|
|
2
|
-
import type {
|
|
1
|
+
import type { PickerAdapter, PickerNode, PickerRequest, PickerSearchResult } from '../types/filePicker'
|
|
2
|
+
import type {
|
|
3
|
+
FileBrowserLocation,
|
|
4
|
+
FileDirectoryListing,
|
|
5
|
+
FileDirectoryTree,
|
|
6
|
+
FileEntry,
|
|
7
|
+
FileSearchResult,
|
|
8
|
+
ServerMount,
|
|
9
|
+
} from '../types/fileBrowserTypes'
|
|
3
10
|
import { pickerFormat } from './filePicker/validation'
|
|
4
11
|
|
|
5
12
|
export interface FilePickerTransport {
|
|
6
13
|
listMounts(request?: PickerRequest): Promise<ServerMount[]>
|
|
7
14
|
browse(location: FileBrowserLocation, request?: PickerRequest): Promise<FileDirectoryListing>
|
|
15
|
+
/** `GET …/filesystem/tree`: one request fills the cache for the subfolders too. */
|
|
16
|
+
tree?(location: FileBrowserLocation, request?: PickerRequest): Promise<FileDirectoryTree>
|
|
17
|
+
/** `GET …/filesystem/search`: replaces the folder-by-folder search walk. */
|
|
18
|
+
search?(
|
|
19
|
+
location: FileBrowserLocation,
|
|
20
|
+
query: string,
|
|
21
|
+
recursive: boolean,
|
|
22
|
+
request?: PickerRequest,
|
|
23
|
+
): Promise<FileSearchResult>
|
|
8
24
|
}
|
|
9
25
|
export interface FilePickerAdapterOptions {
|
|
10
26
|
/** Show this directory's contents directly and prevent navigation outside it. */
|
|
@@ -82,31 +98,75 @@ export function createFilePickerAdapter(
|
|
|
82
98
|
unavailableReason: mount.available === false ? 'This mount is offline.' : undefined,
|
|
83
99
|
}
|
|
84
100
|
})
|
|
101
|
+
} else if (transport.tree) {
|
|
102
|
+
const tree = await transport.tree(location, request)
|
|
103
|
+
check(request, token)
|
|
104
|
+
for (const listing of tree.listings ?? []) {
|
|
105
|
+
const at = { mount_id: listing.mount_id, path: listing.path }
|
|
106
|
+
validateLocation(at)
|
|
107
|
+
if (at.mount_id !== location.mount_id) throw new Error('Invalid file picker directory entry.')
|
|
108
|
+
// A capped subfolder is read on its own when opened, and reports the cap then.
|
|
109
|
+
if (!listing.truncated) listings.set(encodePlatformPickerPath(at.mount_id, at.path), toNodes(at, listing))
|
|
110
|
+
}
|
|
111
|
+
const cached = listings.get(key)
|
|
112
|
+
if (!cached)
|
|
113
|
+
throw new Error('This directory exceeds the server listing limit. Choose a smaller data directory.')
|
|
114
|
+
return cached
|
|
85
115
|
} else {
|
|
86
116
|
const listing = await transport.browse(location, request)
|
|
87
117
|
check(request, token)
|
|
88
118
|
if (listing.truncated)
|
|
89
119
|
throw new Error('This directory exceeds the server listing limit. Choose a smaller data directory.')
|
|
90
|
-
|
|
91
|
-
nodes = (listing.entries ?? []).map((entry): PickerNode => {
|
|
92
|
-
if (!isRelativePath(entry.path) || !entry.name ||
|
|
93
|
-
entry.path !== `${location.path ? `${location.path}/` : ''}${entry.name}` ||
|
|
94
|
-
entry.name.includes('/')) throw new Error('Invalid file picker directory entry.')
|
|
95
|
-
const common = {
|
|
96
|
-
path: encodePlatformPickerPath(location.mount_id, entry.path),
|
|
97
|
-
parentPath: atRoot ? null : key,
|
|
98
|
-
parentName: `${mountLabels.get(location.mount_id) ?? location.mount_id}${location.path ? ` / ${location.path}` : ''}`,
|
|
99
|
-
name: entry.name, bytes: entry.size_bytes, modifiedAt: entry.modified_at,
|
|
100
|
-
}
|
|
101
|
-
return entry.kind === 'directory'
|
|
102
|
-
// Counts may include only matching formats, not nested directories.
|
|
103
|
-
? { ...common, kind: 'folder' }
|
|
104
|
-
: { ...common, kind: 'file', format: pickerFormat(entry.name) }
|
|
105
|
-
})
|
|
120
|
+
nodes = toNodes(location, listing)
|
|
106
121
|
}
|
|
107
122
|
listings.set(key, nodes)
|
|
108
123
|
return nodes
|
|
109
124
|
}
|
|
125
|
+
function toNodes(location: FileBrowserLocation, listing: FileDirectoryListing): PickerNode[] {
|
|
126
|
+
return (listing.entries ?? []).map((entry) => {
|
|
127
|
+
if (entry.path !== `${location.path ? `${location.path}/` : ''}${entry.name}`)
|
|
128
|
+
throw new Error('Invalid file picker directory entry.')
|
|
129
|
+
return toNode(location.mount_id, entry)
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
/** One entry, parented on the folder its mount-relative path names. */
|
|
133
|
+
function toNode(mountId: string, entry: FileEntry): PickerNode {
|
|
134
|
+
if (!isRelativePath(entry.path) || !entry.name || entry.name.includes('/') ||
|
|
135
|
+
(entry.path !== entry.name && !entry.path.endsWith(`/${entry.name}`)))
|
|
136
|
+
throw new Error('Invalid file picker directory entry.')
|
|
137
|
+
const parent = entry.path.slice(0, Math.max(0, entry.path.length - entry.name.length - 1))
|
|
138
|
+
const atRoot = root && mountId === root.mount_id && parent === root.path
|
|
139
|
+
const common = {
|
|
140
|
+
path: encodePlatformPickerPath(mountId, entry.path),
|
|
141
|
+
parentPath: atRoot ? null : encodePlatformPickerPath(mountId, parent),
|
|
142
|
+
parentName: `${mountLabels.get(mountId) ?? mountId}${parent ? ` / ${parent}` : ''}`,
|
|
143
|
+
name: entry.name, bytes: entry.size_bytes, modifiedAt: entry.modified_at,
|
|
144
|
+
}
|
|
145
|
+
return entry.kind === 'directory'
|
|
146
|
+
// Counts may include only matching formats, not nested directories.
|
|
147
|
+
? { ...common, kind: 'folder' }
|
|
148
|
+
: { ...common, kind: 'file', format: pickerFormat(entry.name) }
|
|
149
|
+
}
|
|
150
|
+
async function serverSearch(
|
|
151
|
+
query: string,
|
|
152
|
+
scope: { path: string; recursive: boolean },
|
|
153
|
+
request: PickerRequest | undefined,
|
|
154
|
+
): Promise<PickerNode[] | PickerSearchResult> {
|
|
155
|
+
const token = begin(request)
|
|
156
|
+
const decoded = scope.path ? decodePlatformPickerPath(scope.path) : undefined
|
|
157
|
+
const location = decoded ? { mount_id: decoded.mountId, path: decoded.path } : root!
|
|
158
|
+
validateLocation(location)
|
|
159
|
+
const result = await transport.search!(location, query, scope.recursive, request)
|
|
160
|
+
check(request, token)
|
|
161
|
+
const prefix = location.path ? `${location.path}/` : ''
|
|
162
|
+
const nodes = (result.entries ?? []).map((entry) => {
|
|
163
|
+
// "This folder" searches may return direct children only.
|
|
164
|
+
if (!entry.path.startsWith(prefix) || (!scope.recursive && entry.path.slice(prefix.length).includes('/')))
|
|
165
|
+
throw new Error('Invalid file picker directory entry.')
|
|
166
|
+
return toNode(location.mount_id, entry)
|
|
167
|
+
})
|
|
168
|
+
return result.truncated ? { nodes, truncated: true } : nodes
|
|
169
|
+
}
|
|
110
170
|
return {
|
|
111
171
|
mounts: !root,
|
|
112
172
|
mountOf(path) {
|
|
@@ -120,6 +180,8 @@ export function createFilePickerAdapter(
|
|
|
120
180
|
listRoot: async (request) => read('', request, begin(request)),
|
|
121
181
|
listChildren: async (path, request) => read(path, request, begin(request)),
|
|
122
182
|
async search(query, scope, request) {
|
|
183
|
+
// The mount list itself has no server to search, so it keeps the walk.
|
|
184
|
+
if (transport.search && (scope.path || root)) return serverSearch(query, scope, request)
|
|
123
185
|
const token = begin(request)
|
|
124
186
|
const matches: PickerNode[] = []
|
|
125
187
|
const queue = [scope.path]
|
|
@@ -131,8 +193,7 @@ export function createFilePickerAdapter(
|
|
|
131
193
|
const path = queue.shift()!
|
|
132
194
|
if (visited.has(path)) continue
|
|
133
195
|
visited.add(path)
|
|
134
|
-
if (visited.size > 500)
|
|
135
|
-
throw new Error('Search reached its folder limit. Search a smaller location or turn off Subfolders.')
|
|
196
|
+
if (visited.size > 500) return { nodes: matches, truncated: true }
|
|
136
197
|
const nodes = await read(path, currentRequest, token)
|
|
137
198
|
check(request, token)
|
|
138
199
|
if (currentRequest?.refresh) {
|
|
@@ -144,8 +205,7 @@ export function createFilePickerAdapter(
|
|
|
144
205
|
if (scope.recursive && node.kind === 'folder' && !node.unavailableReason)
|
|
145
206
|
queue.push(node.path)
|
|
146
207
|
}
|
|
147
|
-
if (matches.length > 10000)
|
|
148
|
-
throw new Error('Too many search results. Use a more specific search.')
|
|
208
|
+
if (matches.length > 10000) return { nodes: matches.slice(0, 10000), truncated: true }
|
|
149
209
|
}
|
|
150
210
|
return matches
|
|
151
211
|
},
|
|
@@ -3,13 +3,33 @@ import type {
|
|
|
3
3
|
PickerAdapter,
|
|
4
4
|
PickerNode,
|
|
5
5
|
PickerRequest,
|
|
6
|
+
PickerSort,
|
|
6
7
|
PickerTreeRow,
|
|
7
8
|
} from '../../types/filePicker'
|
|
8
9
|
|
|
10
|
+
const byName = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' }).compare
|
|
11
|
+
/** Folders first; undated rows go last in either time order. */
|
|
12
|
+
function ordered(items: PickerNode[], sort: PickerSort | undefined): PickerNode[] {
|
|
13
|
+
if (!sort) return items
|
|
14
|
+
// Parse each timestamp once rather than on every comparison.
|
|
15
|
+
const times = new Map(items.map((node) => [node, node.modifiedAt ? Date.parse(node.modifiedAt) : NaN]))
|
|
16
|
+
const time = (node: PickerNode) => times.get(node)!
|
|
17
|
+
return [...items].sort((a, b) => {
|
|
18
|
+
if (a.kind !== b.kind) return a.kind === 'folder' ? -1 : 1
|
|
19
|
+
if (sort === 'name') return byName(a.name, b.name)
|
|
20
|
+
if (sort === 'name-desc') return byName(b.name, a.name)
|
|
21
|
+
const [ta, tb] = [time(a), time(b)]
|
|
22
|
+
if (Number.isNaN(ta) || Number.isNaN(tb))
|
|
23
|
+
return Number.isNaN(ta) === Number.isNaN(tb) ? byName(a.name, b.name) : Number.isNaN(ta) ? 1 : -1
|
|
24
|
+
return (sort === 'newest' ? tb - ta : ta - tb) || byName(a.name, b.name)
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
9
28
|
export function usePickerNavigation(
|
|
10
29
|
adapter: () => PickerAdapter,
|
|
11
30
|
request: () => PickerRequest,
|
|
12
31
|
systemFilter: (node: PickerNode) => boolean = () => true,
|
|
32
|
+
sort: () => PickerSort | undefined = () => undefined,
|
|
13
33
|
) {
|
|
14
34
|
const path = shallowRef('')
|
|
15
35
|
const trail = ref<{ path: string; label: string }[]>([])
|
|
@@ -21,6 +41,8 @@ export function usePickerNavigation(
|
|
|
21
41
|
const expansionErrors = ref(new Map<string, string>())
|
|
22
42
|
const loading = shallowRef(false)
|
|
23
43
|
const error = shallowRef('')
|
|
44
|
+
/** True when the current search results stopped at an adapter limit. */
|
|
45
|
+
const truncated = shallowRef(false)
|
|
24
46
|
let epoch = 0
|
|
25
47
|
let navigation = 0
|
|
26
48
|
let controller: AbortController | undefined
|
|
@@ -154,14 +176,17 @@ export function usePickerNavigation(
|
|
|
154
176
|
const options = { ...request(), ...(refresh && { refresh }), signal: controller.signal }
|
|
155
177
|
loading.value = true
|
|
156
178
|
error.value = ''
|
|
179
|
+
truncated.value = false
|
|
157
180
|
entries.value = []
|
|
158
181
|
try {
|
|
159
|
-
const
|
|
182
|
+
const found = query.trim()
|
|
160
183
|
? await adapter().search(query.trim(), { path: path.value, recursive }, options)
|
|
161
184
|
: path.value
|
|
162
185
|
? await loadChildren(path.value, [], refresh)
|
|
163
186
|
: await adapter().listRoot(options)
|
|
164
187
|
if (token !== navigation) return
|
|
188
|
+
const items = Array.isArray(found) ? found : found.nodes
|
|
189
|
+
truncated.value = !Array.isArray(found) && found.truncated
|
|
165
190
|
register(items, query.trim() ? undefined : path.value)
|
|
166
191
|
entries.value = items
|
|
167
192
|
if (!query.trim()) prefetch(items)
|
|
@@ -215,8 +240,9 @@ export function usePickerNavigation(
|
|
|
215
240
|
}
|
|
216
241
|
const rows = computed(() => {
|
|
217
242
|
const result: PickerTreeRow[] = []
|
|
243
|
+
const order = sort()
|
|
218
244
|
function visit(items: PickerNode[], depth: number, ancestors: Set<string>) {
|
|
219
|
-
for (const item of items) {
|
|
245
|
+
for (const item of ordered(items, order)) {
|
|
220
246
|
if (ancestors.has(item.path) || depth > 30) continue
|
|
221
247
|
const node = nodes.value.get(item.path) ?? item
|
|
222
248
|
if (!isAllowed(node)) continue
|
|
@@ -246,6 +272,7 @@ export function usePickerNavigation(
|
|
|
246
272
|
expansionErrors,
|
|
247
273
|
loading,
|
|
248
274
|
error,
|
|
275
|
+
truncated,
|
|
249
276
|
rows,
|
|
250
277
|
isAllowed,
|
|
251
278
|
clear,
|
|
@@ -5,6 +5,7 @@ import type {
|
|
|
5
5
|
PickerNode,
|
|
6
6
|
PickerSelection,
|
|
7
7
|
PickerSelectionMode,
|
|
8
|
+
PickerSort,
|
|
8
9
|
PickerSource,
|
|
9
10
|
} from '../types/filePicker'
|
|
10
11
|
import type { SegmentedOption } from '../types'
|
|
@@ -20,6 +21,7 @@ interface PickerStateProps {
|
|
|
20
21
|
capabilities: PickerCapabilities
|
|
21
22
|
initialPath: string
|
|
22
23
|
initialSelection: PickerSelection | null
|
|
24
|
+
defaultSort: PickerSort
|
|
23
25
|
systemFilter?: (node: PickerNode, source: PickerSource) => boolean
|
|
24
26
|
}
|
|
25
27
|
type PickerEmit = {
|
|
@@ -50,6 +52,7 @@ export function useFilePicker(
|
|
|
50
52
|
)
|
|
51
53
|
const query = shallowRef('')
|
|
52
54
|
const recursive = shallowRef(false)
|
|
55
|
+
const sort = shallowRef<PickerSort>(props.defaultSort)
|
|
53
56
|
const skipBlanks = shallowRef(false)
|
|
54
57
|
const reviewing = shallowRef(false)
|
|
55
58
|
const localFiles = shallowRef<File[]>([])
|
|
@@ -61,6 +64,7 @@ export function useFilePicker(
|
|
|
61
64
|
() => (source.value === 'localFile' ? localAdapter.value : props.adapter),
|
|
62
65
|
() => ({ source: source.value }),
|
|
63
66
|
(node) => props.systemFilter?.(node, source.value) !== false,
|
|
67
|
+
() => sort.value,
|
|
64
68
|
)
|
|
65
69
|
const capability = computed(() => props.capabilities[source.value])
|
|
66
70
|
const selection = usePickerSelection(
|
|
@@ -182,6 +186,7 @@ export function useFilePicker(
|
|
|
182
186
|
(open) => {
|
|
183
187
|
clearTimeout(timer)
|
|
184
188
|
if (open) {
|
|
189
|
+
sort.value = props.defaultSort
|
|
185
190
|
if (!initialized) {
|
|
186
191
|
path.value = props.initialPath
|
|
187
192
|
if (path.value) trail.value = [{ path: path.value, label: 'Current folder' }]
|
|
@@ -348,9 +353,10 @@ export function useFilePicker(
|
|
|
348
353
|
}
|
|
349
354
|
async function selectLocalFiles() {
|
|
350
355
|
const token = openGeneration
|
|
351
|
-
const
|
|
356
|
+
const found = props.selectionMode === 'single-file' || props.selectionMode === 'multi-file'
|
|
352
357
|
? await localAdapter.value.search('', { path: '', recursive: true })
|
|
353
358
|
: nav.entries.value
|
|
359
|
+
const items = Array.isArray(found) ? found : found.nodes
|
|
354
360
|
if (token !== openGeneration || !props.open || source.value !== 'localFile') return
|
|
355
361
|
nav.register(items)
|
|
356
362
|
const candidates = items.filter((node) => selection.selectable(node))
|
|
@@ -411,6 +417,7 @@ export function useFilePicker(
|
|
|
411
417
|
source,
|
|
412
418
|
query,
|
|
413
419
|
recursive,
|
|
420
|
+
sort,
|
|
414
421
|
skipBlanks,
|
|
415
422
|
reviewing,
|
|
416
423
|
nav,
|
|
@@ -1,11 +1,24 @@
|
|
|
1
1
|
import type { PickerAdapter } from '../types/filePicker'
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
FileBrowserLocation,
|
|
4
|
+
FileDirectoryListing,
|
|
5
|
+
FileDirectoryTree,
|
|
6
|
+
FileSearchResult,
|
|
7
|
+
ServerMount,
|
|
8
|
+
} from '../types/fileBrowserTypes'
|
|
9
|
+
import type { PickerRequest } from '../types/filePicker'
|
|
3
10
|
import { useApi } from './useApi'
|
|
4
11
|
import { getInjectedPlatformContext } from './platformContextHelpers'
|
|
5
12
|
import { createFilePickerAdapter, type FilePickerAdapterOptions } from './createFilePickerAdapter'
|
|
6
13
|
|
|
7
14
|
export { encodePlatformPickerPath, decodePlatformPickerPath } from './createFilePickerAdapter'
|
|
8
15
|
|
|
16
|
+
function query(location: FileBrowserLocation, request?: PickerRequest): URLSearchParams {
|
|
17
|
+
const params = new URLSearchParams({ mount_id: location.mount_id, path: location.path })
|
|
18
|
+
if (request?.refresh) params.set('refresh', 'true')
|
|
19
|
+
return params
|
|
20
|
+
}
|
|
21
|
+
|
|
9
22
|
/** Adapt the authenticated MINT mount API without exposing absolute host paths. */
|
|
10
23
|
export function usePlatformFilePickerAdapter(
|
|
11
24
|
options: FilePickerAdapterOptions & { apiBaseUrl?: string } = {},
|
|
@@ -27,11 +40,20 @@ export function usePlatformFilePickerAdapter(
|
|
|
27
40
|
}
|
|
28
41
|
},
|
|
29
42
|
browse(location, request) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
return api.get<FileDirectoryListing>(`/filesystem/browse?${query(location, request)}`, {
|
|
44
|
+
signal: request?.signal,
|
|
45
|
+
})
|
|
46
|
+
},
|
|
47
|
+
tree(location, request) {
|
|
48
|
+
return api.get<FileDirectoryTree>(`/filesystem/tree?${query(location, request)}`, {
|
|
33
49
|
signal: request?.signal,
|
|
34
50
|
})
|
|
35
51
|
},
|
|
52
|
+
search(location, text, recursive, request) {
|
|
53
|
+
const params = query(location, request)
|
|
54
|
+
params.set('q', text)
|
|
55
|
+
params.set('recursive', String(recursive))
|
|
56
|
+
return api.get<FileSearchResult>(`/filesystem/search?${params}`, { signal: request?.signal })
|
|
57
|
+
},
|
|
36
58
|
}, options)
|
|
37
59
|
}
|
|
@@ -18,3 +18,13 @@
|
|
|
18
18
|
min-width: 10rem;
|
|
19
19
|
max-width: min(18rem, calc(100vw - 1rem));
|
|
20
20
|
}
|
|
21
|
+
|
|
22
|
+
.mint-action-menu__check {
|
|
23
|
+
flex-shrink: 0;
|
|
24
|
+
color: var(--color-primary);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* Unchecked radio rows keep the check's width so labels stay aligned. */
|
|
28
|
+
.mint-action-menu__check--off {
|
|
29
|
+
visibility: hidden;
|
|
30
|
+
}
|
|
@@ -106,6 +106,7 @@
|
|
|
106
106
|
white-space: nowrap;
|
|
107
107
|
}
|
|
108
108
|
.mint-picker__scope,
|
|
109
|
+
.mint-picker__sort,
|
|
109
110
|
.mint-picker__link {
|
|
110
111
|
padding: 2px 6px;
|
|
111
112
|
border: 0;
|
|
@@ -116,10 +117,20 @@
|
|
|
116
117
|
cursor: pointer;
|
|
117
118
|
transition: background-color 150ms ease;
|
|
118
119
|
}
|
|
119
|
-
.mint-picker__scope
|
|
120
|
+
.mint-picker__scope,
|
|
121
|
+
.mint-picker__sort {
|
|
120
122
|
flex-shrink: 0;
|
|
121
123
|
color: var(--text-secondary-strong);
|
|
122
124
|
}
|
|
125
|
+
.mint-picker__sort {
|
|
126
|
+
display: inline-flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
gap: 2px;
|
|
129
|
+
}
|
|
130
|
+
.mint-picker__sort svg {
|
|
131
|
+
width: 12px;
|
|
132
|
+
height: 12px;
|
|
133
|
+
}
|
|
123
134
|
.mint-picker__link {
|
|
124
135
|
color: var(--_ink-primary);
|
|
125
136
|
}
|
|
@@ -138,10 +149,13 @@
|
|
|
138
149
|
cursor: not-allowed;
|
|
139
150
|
}
|
|
140
151
|
.mint-picker__scope:hover,
|
|
152
|
+
.mint-picker__sort:hover,
|
|
153
|
+
.mint-picker__sort[aria-expanded='true'],
|
|
141
154
|
.mint-picker__link:hover {
|
|
142
155
|
background: var(--bg-hover);
|
|
143
156
|
}
|
|
144
157
|
.mint-picker__scope:focus-visible,
|
|
158
|
+
.mint-picker__sort:focus-visible,
|
|
145
159
|
.mint-picker__link:focus-visible {
|
|
146
160
|
outline: none;
|
|
147
161
|
box-shadow: var(--focus-ring-offset);
|
|
@@ -185,6 +199,14 @@
|
|
|
185
199
|
.mint-picker__list-head > span:nth-child(n + 3) {
|
|
186
200
|
text-align: right;
|
|
187
201
|
}
|
|
202
|
+
.mint-picker__truncated {
|
|
203
|
+
margin: 0;
|
|
204
|
+
padding: 6px 12px;
|
|
205
|
+
border-bottom: 1px solid var(--border-light);
|
|
206
|
+
background: var(--bg-secondary);
|
|
207
|
+
font-size: 12px;
|
|
208
|
+
color: var(--text-secondary);
|
|
209
|
+
}
|
|
188
210
|
.mint-picker__row {
|
|
189
211
|
box-sizing: border-box;
|
|
190
212
|
height: 34px;
|
package/src/types/auth.ts
CHANGED
|
@@ -33,6 +33,10 @@ export interface TokenVerifyResponse {
|
|
|
33
33
|
valid: boolean
|
|
34
34
|
username: string | null
|
|
35
35
|
expires_at: string | null
|
|
36
|
+
/** Platform 1.3+: `access_token` for a personal access token. */
|
|
37
|
+
token_type?: 'session' | 'access_token'
|
|
38
|
+
/** Platform 1.3+: true for a read-only personal access token. */
|
|
39
|
+
read_only?: boolean
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
// Registration request
|
package/src/types/components.ts
CHANGED
|
@@ -13,6 +13,8 @@ interface ActionMenuItemBase {
|
|
|
13
13
|
label: string
|
|
14
14
|
disabled?: boolean
|
|
15
15
|
danger?: boolean
|
|
16
|
+
/** Set on every item to make the menu a single choice (`menuitemradio`); true marks the current one. */
|
|
17
|
+
checked?: boolean
|
|
16
18
|
}
|
|
17
19
|
|
|
18
20
|
export type ActionMenuItem =
|
|
@@ -23,6 +25,8 @@ export type ActionMenuItem =
|
|
|
23
25
|
| (ActionMenuItemBase & {
|
|
24
26
|
href: string
|
|
25
27
|
target?: '_self' | '_blank' | '_parent' | '_top'
|
|
28
|
+
/** Links navigate; only buttons can be single-choice items. */
|
|
29
|
+
checked?: never
|
|
26
30
|
})
|
|
27
31
|
|
|
28
32
|
// Input types
|
|
@@ -83,3 +83,18 @@ export interface FileBrowserLocation {
|
|
|
83
83
|
mount_id: string
|
|
84
84
|
path: string
|
|
85
85
|
}
|
|
86
|
+
|
|
87
|
+
/** Several directory levels in one response, parents before children. */
|
|
88
|
+
export interface FileDirectoryTree {
|
|
89
|
+
listings?: FileDirectoryListing[]
|
|
90
|
+
/** True when the server's entry budget stopped before the requested depth. */
|
|
91
|
+
truncated?: boolean
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Entries whose name contains a query, below one directory. */
|
|
95
|
+
export interface FileSearchResult {
|
|
96
|
+
entries?: FileEntry[]
|
|
97
|
+
searched_directories?: number
|
|
98
|
+
/** True when a directory, result or time budget ended the search early. */
|
|
99
|
+
truncated?: boolean
|
|
100
|
+
}
|
package/src/types/filePicker.ts
CHANGED
|
@@ -3,6 +3,8 @@ export type PickerSource = 'server' | 'localFile'
|
|
|
3
3
|
export type PickerSelectionMode = 'single-file' | 'multi-file' | 'folder' | 'folder+files'
|
|
4
4
|
/** `list` shows one folder at a time (the default); `tree` adds disclosure triangles. */
|
|
5
5
|
export type PickerView = 'list' | 'tree'
|
|
6
|
+
/** Row order inside each folder; folders always come before files. */
|
|
7
|
+
export type PickerSort = 'newest' | 'oldest' | 'name' | 'name-desc'
|
|
6
8
|
|
|
7
9
|
interface PickerNodeBase {
|
|
8
10
|
path: string
|
|
@@ -39,6 +41,11 @@ export interface PickerRequest {
|
|
|
39
41
|
*/
|
|
40
42
|
refresh?: true
|
|
41
43
|
}
|
|
44
|
+
/** What a search found before a server or walk limit stopped it. */
|
|
45
|
+
export interface PickerSearchResult {
|
|
46
|
+
nodes: PickerNode[]
|
|
47
|
+
truncated: boolean
|
|
48
|
+
}
|
|
42
49
|
export interface PickerAdapter {
|
|
43
50
|
/**
|
|
44
51
|
* `listRoot()` returns mount points. FilePicker then shows them as header
|
|
@@ -55,7 +62,7 @@ export interface PickerAdapter {
|
|
|
55
62
|
query: string,
|
|
56
63
|
scope: { path: string; recursive: boolean },
|
|
57
64
|
request?: PickerRequest,
|
|
58
|
-
): Promise<PickerNode[]>
|
|
65
|
+
): Promise<PickerNode[] | PickerSearchResult>
|
|
59
66
|
}
|
|
60
67
|
export interface PickerCapability {
|
|
61
68
|
/** Case-insensitive suffixes, including compound extensions such as mzML.gz. */
|
package/src/types/index.ts
CHANGED
|
@@ -218,9 +218,11 @@ export type {
|
|
|
218
218
|
FileBrowserLocation,
|
|
219
219
|
FileBrowserSortKey,
|
|
220
220
|
FileDirectoryListing,
|
|
221
|
+
FileDirectoryTree,
|
|
221
222
|
FileEntry,
|
|
222
223
|
FileEntryKind,
|
|
223
224
|
FilePathCrumb,
|
|
225
|
+
FileSearchResult,
|
|
224
226
|
FileSelection,
|
|
225
227
|
ServerMount,
|
|
226
228
|
} from './fileBrowserTypes'
|