@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.
Files changed (42) hide show
  1. package/dist/components/FilePicker.vue.d.ts +4 -1
  2. package/dist/components/index.js +1 -1
  3. package/dist/components/internal/FilePickerListInternal.vue.d.ts +2 -0
  4. package/dist/{components-CvY105kJ.js → components-CF0XMy8W.js} +326 -234
  5. package/dist/components-CF0XMy8W.js.map +1 -0
  6. package/dist/composables/createFilePickerAdapter.d.ts +5 -1
  7. package/dist/composables/filePicker/usePickerNavigation.d.ts +3 -2
  8. package/dist/composables/index.js +1 -1
  9. package/dist/composables/useFilePicker.d.ts +4 -1
  10. package/dist/{composables-DL4Vca7L.js → composables-Dlyz_Aiv.js} +92 -29
  11. package/dist/{composables-DL4Vca7L.js.map → composables-Dlyz_Aiv.js.map} +1 -1
  12. package/dist/index.js +2 -2
  13. package/dist/install.js +1 -1
  14. package/dist/{plotly.min-D6Ge6qgX.js → plotly.min-DDhZCNEM.js} +2 -2
  15. package/dist/{plotly.min-D6Ge6qgX.js.map → plotly.min-DDhZCNEM.js.map} +1 -1
  16. package/dist/styles.css +32 -1
  17. package/dist/types/auth.d.ts +4 -0
  18. package/dist/types/components.d.ts +4 -0
  19. package/dist/types/fileBrowserTypes.d.ts +13 -0
  20. package/dist/types/filePicker.d.ts +8 -1
  21. package/dist/types/index.d.ts +1 -1
  22. package/package.json +1 -1
  23. package/src/__tests__/components/ActionMenu.test.ts +10 -0
  24. package/src/__tests__/components/FilePicker.test.ts +65 -1
  25. package/src/__tests__/composables/createFilePickerAdapter.test.ts +92 -1
  26. package/src/__tests__/composables/usePickerNavigation.test.ts +61 -1
  27. package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +18 -6
  28. package/src/components/ActionMenu.vue +10 -2
  29. package/src/components/FilePicker.vue +42 -0
  30. package/src/components/internal/FilePickerListInternal.vue +11 -2
  31. package/src/composables/createFilePickerAdapter.ts +82 -22
  32. package/src/composables/filePicker/usePickerNavigation.ts +29 -2
  33. package/src/composables/useFilePicker.ts +8 -1
  34. package/src/composables/usePlatformFilePickerAdapter.ts +26 -4
  35. package/src/styles/components/action-menu.css +10 -0
  36. package/src/styles/components/file-picker.css +23 -1
  37. package/src/types/auth.ts +4 -0
  38. package/src/types/components.ts +4 -0
  39. package/src/types/fileBrowserTypes.ts +15 -0
  40. package/src/types/filePicker.ts +8 -1
  41. package/src/types/index.ts +2 -0
  42. 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
- query ? 'No matching folders or files' : emptyTitle || 'No readable files'
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
- query ? 'Try a different search or include subfolders.' : 'Choose another location.'
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 { FileBrowserLocation, FileDirectoryListing, ServerMount } from '../types/fileBrowserTypes'
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
- const atRoot = root && location.mount_id === root.mount_id && location.path === root.path
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 items = query.trim()
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 items = props.selectionMode === 'single-file' || props.selectionMode === 'multi-file'
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 { FileDirectoryListing, ServerMount } from '../types/fileBrowserTypes'
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
- const query = new URLSearchParams({ mount_id: location.mount_id, path: location.path })
31
- if (request?.refresh) query.set('refresh', 'true')
32
- return api.get<FileDirectoryListing>(`/filesystem/browse?${query}`, {
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
@@ -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
+ }
@@ -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. */
@@ -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'