@morscherlab/mint-sdk 1.3.0-beta.1 → 1.3.0-beta.11
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 +23 -140
- package/dist/{ResourcePickerListInternal-c3IT8PIu.js → ResourcePickerListInternal-CFAw4vTZ.js} +26 -19
- package/dist/ResourcePickerListInternal-CFAw4vTZ.js.map +1 -0
- package/dist/components/FilePicker.vue.d.ts +4 -1
- package/dist/components/SettingsModal.schema.d.ts +1 -1
- package/dist/components/SettingsModal.vue.d.ts +2 -0
- package/dist/components/WellPlate.colors.d.ts +1 -5
- package/dist/components/WellPlate.rendering.d.ts +5 -2
- package/dist/components/index.d.ts +32 -17
- package/dist/components/index.js +3 -3
- package/dist/components/internal/FilePickerListInternal.vue.d.ts +2 -0
- package/dist/components/internal/ResourcePickerListInternal.vue.d.ts +9 -3
- package/dist/{components-CvY105kJ.js → components-vHfxV2PB.js} +425 -392
- package/dist/components-vHfxV2PB.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.d.ts +0 -3
- package/dist/composables/index.js +4 -4
- package/dist/composables/useAutoGroup.d.ts +1 -1
- package/dist/composables/useBioTemplateComponents.d.ts +15 -3
- package/dist/composables/useBioTemplateControls.d.ts +5 -1
- package/dist/composables/useBioTemplatePackWorkspace.d.ts +2 -0
- package/dist/composables/useBioTemplatePresetWorkspace.d.ts +5 -1
- package/dist/composables/useBioTemplateWorkspace.d.ts +5 -1
- package/dist/composables/useExperimentSave.d.ts +5 -1
- package/dist/composables/useFilePicker.d.ts +4 -1
- package/dist/composables/useGroupAssignment.d.ts +5 -1
- package/dist/composables/usePlatformContext.d.ts +3 -0
- package/dist/composables/useTemplateCollection.d.ts +5 -1
- package/dist/composables-D6uqp0UF.js +591 -0
- package/dist/composables-D6uqp0UF.js.map +1 -0
- package/dist/index.js +24 -24
- package/dist/index.js.map +1 -1
- package/dist/install.js +1 -1
- package/dist/lcms.d.ts +11 -11
- package/dist/styles.css +228 -214
- package/dist/templates/index.js +1 -1
- package/dist/{templates-Bj9hZ7iV.js → templates-BU1pet28.js} +31 -44
- package/dist/templates-BU1pet28.js.map +1 -0
- package/dist/types/auth.d.ts +0 -27
- package/dist/types/components.d.ts +8 -0
- package/dist/types/fileBrowserTypes.d.ts +13 -0
- package/dist/types/filePicker.d.ts +8 -1
- package/dist/types/index.d.ts +2 -2
- package/dist/types/platform.d.ts +2 -0
- package/dist/utils/rack.d.ts +16 -16
- package/dist/{validation-B4Y8_eEe.js → validation-Dmp9Q5W0.js} +50 -12
- package/dist/validation-Dmp9Q5W0.js.map +1 -0
- package/package.json +1 -6
- package/src/__tests__/components/ActionMenu.test.ts +10 -0
- package/src/__tests__/components/AppTopBar.test.ts +24 -8
- package/src/__tests__/components/AppTopBarPageSelector.test.ts +4 -4
- package/src/__tests__/components/EmptyState.test.ts +15 -0
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +12 -5
- package/src/__tests__/components/FilePicker.test.ts +65 -1
- package/src/__tests__/components/FileUploader.test.ts +6 -9
- package/src/__tests__/components/ResourcePickerListInternal.test.ts +23 -4
- package/src/__tests__/components/SettingsModal.test.ts +22 -0
- package/src/__tests__/components/WellPlate.decoration.test.ts +4 -28
- package/src/__tests__/components/WellPlate.rendering.test.ts +20 -33
- package/src/__tests__/components/WellPlate.sampleTypeIndicator.test.ts +7 -7
- 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/__tests__/docs/frontendDocsCatalog.test.ts +2 -2
- package/src/__tests__/styles/controlRefreshRules.test.ts +21 -8
- package/src/__tests__/styles/menusShellRefresh.test.ts +20 -5
- package/src/__tests__/templates/templatesDeprecation.test.ts +58 -11
- package/src/components/ActionMenu.vue +10 -2
- package/src/components/AppTopBar.vue +6 -4
- package/src/components/EmptyState.vue +3 -1
- package/src/components/ExperimentSelectorModal.vue +4 -1
- package/src/components/FilePicker.vue +42 -0
- package/src/components/FileUploader.vue +30 -46
- package/src/components/SettingsModal.schema.ts +4 -2
- package/src/components/SettingsModal.vue +4 -2
- package/src/components/WellPlate.colors.ts +6 -10
- package/src/components/WellPlate.legend.ts +4 -4
- package/src/components/WellPlate.rendering.ts +9 -8
- package/src/components/WellPlate.vue +5 -22
- package/src/components/index.ts +32 -17
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +3 -6
- package/src/components/internal/FilePickerListInternal.vue +11 -2
- package/src/components/internal/ResourcePickerListInternal.vue +21 -27
- package/src/composables/autoGroup/index.ts +1 -1
- package/src/composables/createFilePickerAdapter.ts +82 -22
- package/src/composables/filePicker/usePickerNavigation.ts +29 -2
- package/src/composables/index.ts +0 -7
- package/src/composables/useAutoGroup.ts +1 -1
- package/src/composables/useBioTemplateComponents.ts +15 -3
- package/src/composables/useBioTemplateControls.ts +5 -1
- package/src/composables/useBioTemplatePackWorkspace.ts +2 -0
- package/src/composables/useBioTemplatePresetWorkspace.ts +5 -1
- package/src/composables/useBioTemplateWorkspace.ts +5 -1
- package/src/composables/useExperimentSave.ts +5 -1
- package/src/composables/useFilePicker.ts +8 -1
- package/src/composables/useGroupAssignment.ts +5 -1
- package/src/composables/usePlatformFilePickerAdapter.ts +26 -4
- package/src/composables/useTemplateCollection.ts +5 -1
- package/src/index.ts +1 -1
- package/src/lcms.ts +11 -11
- package/src/styles/components/action-menu.css +10 -0
- package/src/styles/components/app-page-selector.css +61 -17
- package/src/styles/components/app-pill-nav.css +1 -1
- package/src/styles/components/app-top-bar.css +18 -38
- package/src/styles/components/empty-state.css +3 -2
- package/src/styles/components/experiment-selector-modal.css +3 -9
- package/src/styles/components/file-picker.css +23 -1
- package/src/styles/components/file-uploader.css +22 -55
- package/src/styles/components/resource-picker-list.css +70 -36
- package/src/styles/components/well-plate.css +18 -55
- package/src/types/auth.ts +0 -37
- package/src/types/components.ts +8 -0
- package/src/types/fileBrowserTypes.ts +15 -0
- package/src/types/filePicker.ts +8 -1
- package/src/types/index.ts +2 -5
- package/src/types/platform.ts +2 -0
- package/src/utils/rack.ts +16 -16
- package/dist/ResourcePickerListInternal-c3IT8PIu.js.map +0 -1
- package/dist/__tests__/composables/useAuth.test.d.ts +0 -1
- package/dist/__tests__/composables/usePasskey.test.d.ts +0 -1
- package/dist/components-CvY105kJ.js.map +0 -1
- package/dist/composables/useAuth.d.ts +0 -47
- package/dist/composables/usePasskey.d.ts +0 -10
- package/dist/composables/useWellPlateEditor.d.ts +0 -33
- package/dist/composables-DL4Vca7L.js +0 -1193
- package/dist/composables-DL4Vca7L.js.map +0 -1
- package/dist/plotly.min-D6Ge6qgX.js +0 -168582
- package/dist/plotly.min-D6Ge6qgX.js.map +0 -1
- package/dist/templates-Bj9hZ7iV.js.map +0 -1
- package/dist/validation-B4Y8_eEe.js.map +0 -1
- package/src/__tests__/composables/useAuth.test.ts +0 -290
- package/src/__tests__/composables/usePasskey.test.ts +0 -38
- package/src/composables/useAuth.ts +0 -470
- package/src/composables/usePasskey.ts +0 -216
- package/src/composables/useWellPlateEditor.ts +0 -415
|
@@ -110,7 +110,7 @@ it('bounds recursive traversal and rejects truncated listings', async () => {
|
|
|
110
110
|
mount_id, path,
|
|
111
111
|
entries: [{ kind: 'directory', name: 'next', path: path ? `${path}/next` : 'next' }],
|
|
112
112
|
}))
|
|
113
|
-
await
|
|
113
|
+
expect(await adapter.search('missing', { path: '', recursive: true })).toEqual({ nodes: [], truncated: true })
|
|
114
114
|
expect(transport.browse).toHaveBeenCalledTimes(500)
|
|
115
115
|
vi.mocked(transport.browse).mockResolvedValue({ mount_id: 'raw', path: '', entries: [], truncated: true })
|
|
116
116
|
await expect(adapter.listRoot({ refresh: true })).rejects.toThrow('listing limit')
|
|
@@ -162,3 +162,94 @@ it('names the mount holding a path and nothing for a path it cannot decode', ()
|
|
|
162
162
|
const adapter = createFilePickerAdapter(setup().transport)
|
|
163
163
|
expect([adapter.mountOf!(identity('study/run')), adapter.mountOf!('not-json')]).toEqual([identity(), undefined])
|
|
164
164
|
})
|
|
165
|
+
|
|
166
|
+
function treeSetup() {
|
|
167
|
+
const study = { kind: 'directory' as const, name: 'study', path: 'study' }
|
|
168
|
+
const run = { kind: 'file' as const, name: 'run.raw', path: 'study/run.raw' }
|
|
169
|
+
const transport: FilePickerTransport = {
|
|
170
|
+
listMounts: vi.fn(async () => [{ id: 'raw', label: 'Raw files' }]),
|
|
171
|
+
browse: vi.fn(async ({ mount_id, path }) => ({ mount_id, path, entries: [] })),
|
|
172
|
+
tree: vi.fn(async ({ mount_id }) => ({
|
|
173
|
+
listings: [
|
|
174
|
+
{ mount_id, path: '', entries: [study] },
|
|
175
|
+
{ mount_id, path: 'study', entries: [run] },
|
|
176
|
+
],
|
|
177
|
+
truncated: false,
|
|
178
|
+
})),
|
|
179
|
+
search: vi.fn(async ({ path }) => ({
|
|
180
|
+
entries: [study, run].filter((entry) => !path || entry.path.startsWith(`${path}/`)),
|
|
181
|
+
searched_directories: 2,
|
|
182
|
+
truncated: false,
|
|
183
|
+
})),
|
|
184
|
+
}
|
|
185
|
+
return { transport, adapter: createFilePickerAdapter(transport, { rootLocation: { mount_id: 'raw', path: '' } }) }
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
it('should serve a subfolder from the tree response when the transport has tree', async () => {
|
|
189
|
+
const { adapter, transport } = treeSetup()
|
|
190
|
+
await adapter.listRoot()
|
|
191
|
+
await adapter.listChildren(identity('study'))
|
|
192
|
+
expect(transport.tree).toHaveBeenCalledTimes(1)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('should not call browse when the transport has tree', async () => {
|
|
196
|
+
const { adapter, transport } = treeSetup()
|
|
197
|
+
await adapter.listRoot()
|
|
198
|
+
expect(transport.browse).not.toHaveBeenCalled()
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
it('should map tree listings to their own parent folders', async () => {
|
|
202
|
+
const { adapter } = treeSetup()
|
|
203
|
+
await adapter.listRoot()
|
|
204
|
+
expect(await adapter.listChildren(identity('study'))).toMatchObject([
|
|
205
|
+
{ path: identity('study/run.raw'), parentPath: identity('study'), kind: 'file' },
|
|
206
|
+
])
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('should search on the server when the transport has search', async () => {
|
|
210
|
+
const { adapter, transport } = treeSetup()
|
|
211
|
+
await adapter.search('ru', { path: identity('study'), recursive: true })
|
|
212
|
+
expect(transport.search).toHaveBeenCalledWith({ mount_id: 'raw', path: 'study' }, 'ru', true, undefined)
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
it('should give server search results their parent folder', async () => {
|
|
216
|
+
const { adapter } = treeSetup()
|
|
217
|
+
expect(await adapter.search('ru', { path: '', recursive: true })).toMatchObject([
|
|
218
|
+
{ path: identity('study'), parentPath: null },
|
|
219
|
+
{ path: identity('study/run.raw'), parentPath: identity('study') },
|
|
220
|
+
])
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('should return partial server results marked truncated when the server stops early', async () => {
|
|
224
|
+
const { adapter, transport } = treeSetup()
|
|
225
|
+
vi.mocked(transport.search!).mockResolvedValueOnce({
|
|
226
|
+
entries: [{ kind: 'file', name: 'run.raw', path: 'study/run.raw' }], searched_directories: 5000, truncated: true,
|
|
227
|
+
})
|
|
228
|
+
expect(await adapter.search('ru', { path: '', recursive: true })).toMatchObject({
|
|
229
|
+
nodes: [{ path: identity('study/run.raw') }], truncated: true,
|
|
230
|
+
})
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('should reject a search entry whose name is only a suffix of its last path segment', async () => {
|
|
234
|
+
const { adapter, transport } = treeSetup()
|
|
235
|
+
vi.mocked(transport.search!).mockResolvedValueOnce({
|
|
236
|
+
entries: [{ kind: 'file', name: 'b', path: 'study/ab' }], searched_directories: 1, truncated: false,
|
|
237
|
+
})
|
|
238
|
+
await expect(adapter.search('b', { path: '', recursive: true })).rejects.toThrow(/Invalid/)
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
it('should reject a server search entry outside the searched folder', async () => {
|
|
242
|
+
const { adapter, transport } = treeSetup()
|
|
243
|
+
vi.mocked(transport.search!).mockResolvedValueOnce({
|
|
244
|
+
entries: [{ kind: 'file', name: 'x.raw', path: 'other/x.raw' }], searched_directories: 1, truncated: false,
|
|
245
|
+
})
|
|
246
|
+
await expect(adapter.search('x', { path: identity('study'), recursive: true })).rejects.toThrow(/Invalid/)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('should reject a nested entry from a search limited to this folder', async () => {
|
|
250
|
+
const { adapter, transport } = treeSetup()
|
|
251
|
+
vi.mocked(transport.search!).mockResolvedValueOnce({
|
|
252
|
+
entries: [{ kind: 'file', name: 'x.raw', path: 'study/private/x.raw' }], searched_directories: 1, truncated: false,
|
|
253
|
+
})
|
|
254
|
+
await expect(adapter.search('x', { path: identity('study'), recursive: false })).rejects.toThrow(/Invalid/)
|
|
255
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterEach, expect, it, vi } from 'vitest'
|
|
2
2
|
import { effectScope, type EffectScope } from 'vue'
|
|
3
3
|
import { usePickerNavigation } from '../../composables/filePicker/usePickerNavigation'
|
|
4
|
-
import type { PickerAdapter, PickerNode } from '../../types/filePicker'
|
|
4
|
+
import type { PickerAdapter, PickerNode, PickerSort } from '../../types/filePicker'
|
|
5
5
|
|
|
6
6
|
const scopes: EffectScope[] = []
|
|
7
7
|
afterEach(() => {
|
|
@@ -147,3 +147,63 @@ it('restarts prefetch after reopening even if old requests ignore cancellation',
|
|
|
147
147
|
finish[4]!()
|
|
148
148
|
await vi.advanceTimersByTimeAsync(100)
|
|
149
149
|
})
|
|
150
|
+
|
|
151
|
+
const dated = (kind: 'folder' | 'file', name: string, modifiedAt?: string): PickerNode =>
|
|
152
|
+
kind === 'folder'
|
|
153
|
+
? { kind, path: name, name, modifiedAt }
|
|
154
|
+
: { kind, path: name, name, modifiedAt, format: 'raw' }
|
|
155
|
+
const mixed = () => [
|
|
156
|
+
dated('file', 'b.raw', '2026-09-01T00:00:00Z'),
|
|
157
|
+
dated('folder', 'old', '2026-01-01T00:00:00Z'),
|
|
158
|
+
dated('file', 'a.raw', '2026-09-29T00:00:00Z'),
|
|
159
|
+
dated('folder', 'new', '2026-09-28T00:00:00Z'),
|
|
160
|
+
dated('file', 'undated.raw'),
|
|
161
|
+
]
|
|
162
|
+
async function sortedNames(sort: PickerSort | undefined) {
|
|
163
|
+
vi.useFakeTimers()
|
|
164
|
+
const adapter: PickerAdapter = {
|
|
165
|
+
listRoot: vi.fn(async () => mixed()),
|
|
166
|
+
listChildren: vi.fn(async () => []),
|
|
167
|
+
search: vi.fn(async () => []),
|
|
168
|
+
}
|
|
169
|
+
const scope = effectScope()
|
|
170
|
+
scopes.push(scope)
|
|
171
|
+
const nav = scope.run(() => usePickerNavigation(() => adapter, () => ({}), () => true, () => sort))!
|
|
172
|
+
await nav.load()
|
|
173
|
+
return nav.rows.value.map((row) => row.node.name)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
it('should list folders first, newest first, when sorted by newest', async () => {
|
|
177
|
+
expect(await sortedNames('newest')).toEqual(['new', 'old', 'a.raw', 'b.raw', 'undated.raw'])
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('should list folders first, oldest first, when sorted by oldest', async () => {
|
|
181
|
+
expect(await sortedNames('oldest')).toEqual(['old', 'new', 'b.raw', 'a.raw', 'undated.raw'])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should list folders first by name when sorted by name', async () => {
|
|
185
|
+
expect(await sortedNames('name')).toEqual(['new', 'old', 'a.raw', 'b.raw', 'undated.raw'])
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
it('should list folders first by reverse name when sorted by name-desc', async () => {
|
|
189
|
+
expect(await sortedNames('name-desc')).toEqual(['old', 'new', 'undated.raw', 'b.raw', 'a.raw'])
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('should keep the adapter order when no sort is given', async () => {
|
|
193
|
+
expect(await sortedNames(undefined)).toEqual(['b.raw', 'old', 'a.raw', 'new', 'undated.raw'])
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('should show partial results and flag them when the adapter search is truncated', async () => {
|
|
197
|
+
const { nav, adapter } = setup([folder('a')])
|
|
198
|
+
vi.mocked(adapter.search).mockResolvedValueOnce({ nodes: [folder('a')], truncated: true })
|
|
199
|
+
await nav.load('a', true)
|
|
200
|
+
expect([nav.entries.value.length, nav.truncated.value]).toEqual([1, true])
|
|
201
|
+
})
|
|
202
|
+
|
|
203
|
+
it('should clear the truncated flag on the next plain listing', async () => {
|
|
204
|
+
const { nav, adapter } = setup([folder('a')])
|
|
205
|
+
vi.mocked(adapter.search).mockResolvedValueOnce({ nodes: [], truncated: true })
|
|
206
|
+
await nav.load('a', true)
|
|
207
|
+
await nav.load()
|
|
208
|
+
expect(nav.truncated.value).toBe(false)
|
|
209
|
+
})
|
|
@@ -26,7 +26,10 @@ it('passes cancellation, marks offline mounts, and maps compound extensions', as
|
|
|
26
26
|
],
|
|
27
27
|
})
|
|
28
28
|
.mockResolvedValueOnce({
|
|
29
|
-
|
|
29
|
+
listings: [{
|
|
30
|
+
mount_id: 'online', path: '',
|
|
31
|
+
entries: [{ kind: 'file', path: 'run.mzML.gz', name: 'run.mzML.gz', size_bytes: 7 }],
|
|
32
|
+
}],
|
|
30
33
|
})
|
|
31
34
|
const adapter = usePlatformFilePickerAdapter({ apiBaseUrl: '/api' })
|
|
32
35
|
const signal = new AbortController().signal
|
|
@@ -42,7 +45,7 @@ it('passes cancellation, marks offline mounts, and maps compound extensions', as
|
|
|
42
45
|
expect(get.mock.calls.every((call) => call[1].signal === signal)).toBe(true)
|
|
43
46
|
})
|
|
44
47
|
it('refuses truncated listings rather than returning a partial folder selection', async () => {
|
|
45
|
-
get.mockResolvedValue({ truncated: true, entries: [] })
|
|
48
|
+
get.mockResolvedValue({ listings: [{ mount_id: 'a', path: '', truncated: true, entries: [] }] })
|
|
46
49
|
await expect(
|
|
47
50
|
usePlatformFilePickerAdapter({ apiBaseUrl: '/api' }).listChildren(
|
|
48
51
|
encodePlatformPickerPath('a'),
|
|
@@ -58,22 +61,22 @@ it('scopes shallow search to one level and recursive search skips offline mounts
|
|
|
58
61
|
{ id: 'b', label: 'NAS', available: false },
|
|
59
62
|
],
|
|
60
63
|
}
|
|
61
|
-
: { entries: [{ kind: 'file', path: 'run.raw', name: 'run.raw', size_bytes: 1 }] },
|
|
64
|
+
: { listings: [{ mount_id: 'a', path: '', entries: [{ kind: 'file', path: 'run.raw', name: 'run.raw', size_bytes: 1 }] }] },
|
|
62
65
|
)
|
|
63
66
|
const adapter = usePlatformFilePickerAdapter({ apiBaseUrl: '/api' })
|
|
64
67
|
expect(await adapter.search('run', { path: '', recursive: false })).toEqual([])
|
|
65
68
|
expect(await adapter.search('run', { path: '', recursive: true })).toHaveLength(1)
|
|
66
|
-
expect(get.mock.calls.filter((call) => call[0].includes('/
|
|
69
|
+
expect(get.mock.calls.filter((call) => call[0].includes('/tree'))).toHaveLength(1)
|
|
67
70
|
})
|
|
68
71
|
it('forwards explicit refresh to mount browsing and supports a scoped root', async () => {
|
|
69
|
-
get.mockResolvedValue({ entries: [] })
|
|
72
|
+
get.mockResolvedValue({ listings: [{ mount_id: 'raw', path: 'study', entries: [] }] })
|
|
70
73
|
const adapter = usePlatformFilePickerAdapter({
|
|
71
74
|
apiBaseUrl: '/api', rootLocation: { mount_id: 'raw', path: 'study' },
|
|
72
75
|
})
|
|
73
76
|
const signal = new AbortController().signal
|
|
74
77
|
await adapter.listRoot({ refresh: true, signal })
|
|
75
78
|
expect(get).toHaveBeenCalledExactlyOnceWith(
|
|
76
|
-
'/filesystem/
|
|
79
|
+
'/filesystem/tree?mount_id=raw&path=study&refresh=true', { signal },
|
|
77
80
|
)
|
|
78
81
|
})
|
|
79
82
|
it('forwards a mount-list refresh so server directory snapshots are invalidated too', async () => {
|
|
@@ -83,3 +86,12 @@ it('forwards a mount-list refresh so server directory snapshots are invalidated
|
|
|
83
86
|
await adapter.listRoot({ refresh: true, signal })
|
|
84
87
|
expect(get).toHaveBeenCalledExactlyOnceWith('/filesystem/mounts?refresh=true', { signal })
|
|
85
88
|
})
|
|
89
|
+
|
|
90
|
+
it('should search a folder on the server with the query and scope', async () => {
|
|
91
|
+
get.mockResolvedValue({ entries: [], truncated: false })
|
|
92
|
+
await usePlatformFilePickerAdapter({ apiBaseUrl: '/api' })
|
|
93
|
+
.search('run', { path: encodePlatformPickerPath('raw', 'study'), recursive: false })
|
|
94
|
+
expect(get).toHaveBeenCalledExactlyOnceWith(
|
|
95
|
+
'/filesystem/search?mount_id=raw&path=study&q=run&recursive=false', { signal: undefined },
|
|
96
|
+
)
|
|
97
|
+
})
|
|
@@ -60,7 +60,7 @@ function parseComponentExports(): string[] {
|
|
|
60
60
|
const componentsDir = join(SDK_ROOT, 'src', 'components')
|
|
61
61
|
const content = readFileSync(join(componentsDir, 'index.ts'), 'utf8')
|
|
62
62
|
return [...content.matchAll(
|
|
63
|
-
/export\s+\{\s*default\s+as\s+(\w+)(?:\s*,\s*[^}]*)?\s*\}\s+from\s+['"]\.\/([\w-]+)\.vue['"]/g,
|
|
63
|
+
/export\s+\{\s*(?:\/\*\*.*?\*\/\s*)?default\s+as\s+(\w+)(?:\s*,\s*[^}]*)?\s*\}\s+from\s+['"]\.\/([\w-]+)\.vue['"]/g,
|
|
64
64
|
)]
|
|
65
65
|
.filter((match) => existsSync(join(componentsDir, `${match[2]}.vue`)))
|
|
66
66
|
.map(match => match[1])
|
|
@@ -121,7 +121,7 @@ function parseDeprecatedComponentExports(): Record<string, string> {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
const exportMatch = line.match(
|
|
124
|
-
/export\s+\{\s*default\s+as\s+(\w+)(?:\s*,\s*[^}]*)?\s*\}\s+from\s+['"]\.\/([\w-]+)\.vue['"]/,
|
|
124
|
+
/export\s+\{\s*(?:\/\*\*.*?\*\/\s*)?default\s+as\s+(\w+)(?:\s*,\s*[^}]*)?\s*\}\s+from\s+['"]\.\/([\w-]+)\.vue['"]/,
|
|
125
125
|
)
|
|
126
126
|
if (!exportMatch) continue
|
|
127
127
|
|
|
@@ -184,17 +184,30 @@ describe('SDK control refresh style rules', () => {
|
|
|
184
184
|
expect(rule).toMatch(/flex-wrap:\s*wrap/)
|
|
185
185
|
})
|
|
186
186
|
|
|
187
|
-
it('should
|
|
188
|
-
const css = stripComments(readFileSync(component('
|
|
189
|
-
const rule = /\.mint-
|
|
190
|
-
expect(rule).toMatch(/
|
|
187
|
+
it('should pad resource picker rows to the roomier two-column layout when scanning resource-picker-list.css', () => {
|
|
188
|
+
const css = stripComments(readFileSync(component('resource-picker-list.css'), 'utf-8'))
|
|
189
|
+
const rule = /\.mint-resource-picker__row\s*\{([^}]*)\}/.exec(css)?.[1] ?? ''
|
|
190
|
+
expect(rule).toMatch(/padding:\s*10px 12px/)
|
|
191
191
|
})
|
|
192
192
|
|
|
193
|
-
|
|
194
|
-
|
|
193
|
+
// Group bars and the list frame were inset by the modal body padding; the list now runs edge to edge.
|
|
194
|
+
it('should drop the modal body padding around the resource picker when scanning resource-picker-list.css', () => {
|
|
195
195
|
const css = stripComments(readFileSync(component('resource-picker-list.css'), 'utf-8'))
|
|
196
|
-
const rule = /\.mint-resource-
|
|
197
|
-
expect(rule).toMatch(/padding:\s*
|
|
196
|
+
const rule = /\.mint-modal__body:has\(> \.mint-resource-picker\)\s*\{([^}]*)\}/.exec(css)?.[1] ?? ''
|
|
197
|
+
expect(rule).toMatch(/padding:\s*0/)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('should pad resource picker group bars to the modal gutter when scanning resource-picker-list.css', () => {
|
|
201
|
+
const css = stripComments(readFileSync(component('resource-picker-list.css'), 'utf-8'))
|
|
202
|
+
const rule = /\.mint-resource-picker__group\s*\{([^}]*)\}/.exec(css)?.[1] ?? ''
|
|
203
|
+
expect(rule).toMatch(/padding:\s*0 1\.25rem/)
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
// The list sat in a bordered card inside the modal card.
|
|
207
|
+
it('should draw no frame around the resource picker list when scanning resource-picker-list.css', () => {
|
|
208
|
+
const css = stripComments(readFileSync(component('resource-picker-list.css'), 'utf-8'))
|
|
209
|
+
const rule = /\.mint-resource-picker__frame\s*\{([^}]*)\}/.exec(css)?.[1] ?? ''
|
|
210
|
+
expect(rule).not.toMatch(/(^|;)\s*border:/)
|
|
198
211
|
})
|
|
199
212
|
|
|
200
213
|
it('should paint the clear-selection link with primary ink when scanning experiment-selector-modal.css (8b)', () => {
|
|
@@ -21,16 +21,31 @@ describe('menus and app shell styles', () => {
|
|
|
21
21
|
expect(css('step-wizard.css')).not.toMatch(/\.mint-wizard__nav-btn/)
|
|
22
22
|
})
|
|
23
23
|
|
|
24
|
-
it('should pad the top bar to
|
|
25
|
-
expect(css('app-top-bar.css')).toMatch(/\.mint-topbar\s*\{[^}]*padding:\s*0\.
|
|
24
|
+
it('should pad the top bar to the 1.2 height (62px)', () => {
|
|
25
|
+
expect(css('app-top-bar.css')).toMatch(/\.mint-topbar\s*\{[^}]*padding:\s*0\.75rem 1rem;/)
|
|
26
26
|
})
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
// 28px items + 3px track padding + 1px border on each side = the 36px top-bar control height.
|
|
29
|
+
it('should size pill nav items so the track matches the 36px top-bar controls', () => {
|
|
30
|
+
expect(css('app-pill-nav.css')).toMatch(/\.mint-pill-nav__item\s*\{[^}]*height:\s*1\.75rem;/)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('should size the page selector trigger to the 36px identity tile', () => {
|
|
34
|
+
expect(css('app-page-selector.css')).toMatch(/\.mint-page-selector__trigger\s*\{[^}]*height:\s*2\.25rem;/)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Shrinking the tile beside the identity also shrank structured icons inside their solid chip, drawing a ring.
|
|
38
|
+
it('should keep the plugin tile beside the static plugin identity (5a)', () => {
|
|
39
|
+
expect(css('app-top-bar.css')).not.toMatch(/:has\(\+ \.mint-topbar__plugin-identity\)/)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
// A slotted 36px logo shrank to 32px wide but kept its height, drawing a 32x36 tile.
|
|
43
|
+
it('should fit slotted identity content inside the identity tile (5a)', () => {
|
|
44
|
+
expect(css('app-top-bar.css')).toMatch(/\.mint-topbar__identity > \*\s*\{[^}]*max-width:\s*100%;[^}]*max-height:\s*100%;/)
|
|
30
45
|
})
|
|
31
46
|
|
|
32
47
|
// 15px x 1.15 + 12px x 1.2 = 31.7px, so the title group fits the 32px control row.
|
|
33
|
-
it('should fit the top-bar title and subtitle inside the
|
|
48
|
+
it('should fit the top-bar title and subtitle inside the control row (5a)', () => {
|
|
34
49
|
expect(css('app-top-bar.css')).toMatch(/\.mint-topbar-title\s*\{[^}]*line-height:\s*1\.15;/)
|
|
35
50
|
})
|
|
36
51
|
|
|
@@ -1,31 +1,78 @@
|
|
|
1
1
|
import { dirname, resolve } from 'node:path'
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import { beforeAll, describe, expect, it } from 'vitest'
|
|
4
4
|
import ts from 'typescript'
|
|
5
5
|
|
|
6
6
|
const TEMPLATES_INDEX = resolve(dirname(fileURLToPath(import.meta.url)), '../../templates/index.ts')
|
|
7
|
+
const COMPONENTS_INDEX = resolve(dirname(fileURLToPath(import.meta.url)), '../../components/index.ts')
|
|
8
|
+
const COMPOSABLES_DIR = resolve(dirname(fileURLToPath(import.meta.url)), '../../composables')
|
|
7
9
|
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// Composables removed in MINT 1.4 with the templates and sample grouping.
|
|
11
|
+
const REMOVED_COMPOSABLE_FILES = [
|
|
12
|
+
'useBioTemplateComponents.ts',
|
|
13
|
+
'useBioTemplateControls.ts',
|
|
14
|
+
'useBioTemplatePackWorkspace.ts',
|
|
15
|
+
'useBioTemplatePresetWorkspace.ts',
|
|
16
|
+
'useBioTemplateWorkspace.ts',
|
|
17
|
+
'useTemplateCollection.ts',
|
|
18
|
+
'useExperimentSave.ts',
|
|
19
|
+
'useGroupAssignment.ts',
|
|
20
|
+
].map(file => resolve(COMPOSABLES_DIR, file))
|
|
21
|
+
|
|
22
|
+
// One program for every file: building one per file took longer than the test timeout on CI.
|
|
23
|
+
let program: ts.Program
|
|
24
|
+
let checker: ts.TypeChecker
|
|
25
|
+
|
|
26
|
+
beforeAll(() => {
|
|
27
|
+
program = ts.createProgram([TEMPLATES_INDEX, COMPONENTS_INDEX, ...REMOVED_COMPOSABLE_FILES], {
|
|
12
28
|
target: ts.ScriptTarget.ES2022,
|
|
13
29
|
module: ts.ModuleKind.ESNext,
|
|
14
30
|
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
15
31
|
noEmit: true,
|
|
16
32
|
})
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
33
|
+
checker = program.getTypeChecker()
|
|
34
|
+
}, 60_000)
|
|
35
|
+
|
|
36
|
+
function exportsOf(file: string): ts.Symbol[] {
|
|
37
|
+
return checker.getExportsOfModule(checker.getSymbolAtLocation(program.getSourceFile(file)!)!)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isDeprecated(symbol: ts.Symbol): boolean {
|
|
41
|
+
return symbol.getJsDocTags(checker).some(tag => tag.name === 'deprecated')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// TypeScript ignores @deprecated on `export { ... } from` blocks; an import
|
|
45
|
+
// site only shows the deprecation when the original declaration carries it.
|
|
46
|
+
function undeprecatedValueExports(file: string): string[] {
|
|
47
|
+
return exportsOf(file)
|
|
21
48
|
.map(symbol => (symbol.flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol) : symbol))
|
|
22
49
|
.filter(symbol => symbol.flags & ts.SymbolFlags.Value)
|
|
23
|
-
.filter(symbol => !symbol
|
|
50
|
+
.filter(symbol => !isDeprecated(symbol))
|
|
24
51
|
.map(symbol => symbol.getName())
|
|
25
52
|
}
|
|
26
53
|
|
|
27
54
|
describe('templates deprecation', () => {
|
|
55
|
+
it('should expose deprecated component tags to TypeScript consumers', () => {
|
|
56
|
+
const names = ['BioTemplateRenderer', 'SmartGroupModal', 'DoseDesignWorkspaceView']
|
|
57
|
+
const tagged = exportsOf(COMPONENTS_INDEX).filter(isDeprecated).map(symbol => symbol.getName())
|
|
58
|
+
|
|
59
|
+
expect(names.filter(name => !tagged.includes(name))).toEqual([])
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should not tag components that stay public in MINT 1.4', () => {
|
|
63
|
+
const names = ['SampleSelector', 'SequenceProgressBar', 'InstrumentStateBadge']
|
|
64
|
+
const tagged = exportsOf(COMPONENTS_INDEX).filter(isDeprecated).map(symbol => symbol.getName())
|
|
65
|
+
|
|
66
|
+
expect(names.filter(name => tagged.includes(name))).toEqual([])
|
|
67
|
+
})
|
|
68
|
+
|
|
28
69
|
it('should tag every template value export when the templates move out in MINT 1.4', () => {
|
|
29
|
-
expect(
|
|
70
|
+
expect(undeprecatedValueExports(TEMPLATES_INDEX)).toEqual([])
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it('should tag every value export of the template and grouping composables when they are removed in MINT 1.4', () => {
|
|
74
|
+
const untagged = REMOVED_COMPOSABLE_FILES.flatMap(undeprecatedValueExports)
|
|
75
|
+
|
|
76
|
+
expect(untagged).toEqual([])
|
|
30
77
|
})
|
|
31
78
|
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/** Compact overflow actions with an IconButton trigger and a viewport-safe teleported menu. */
|
|
3
3
|
import { computePosition, flip, offset, shift } from '@floating-ui/dom'
|
|
4
|
-
import { Ellipsis } from 'lucide-vue-next'
|
|
4
|
+
import { Check, Ellipsis } from 'lucide-vue-next'
|
|
5
5
|
import { nextTick, onMounted, ref, watch } from 'vue'
|
|
6
6
|
import { useDropdownState } from '../composables/useDropdownState'
|
|
7
7
|
import { useMenuKeyboard } from '../composables/useMenuKeyboard'
|
|
@@ -167,10 +167,18 @@ watch(isOpen, async (opened) => {
|
|
|
167
167
|
v-else
|
|
168
168
|
type="button"
|
|
169
169
|
:class="['mint-menu-item mint-action-menu__item', { 'mint-menu-item--danger mint-action-menu__item--danger': item.danger }]"
|
|
170
|
-
role="menuitem"
|
|
170
|
+
:role="item.checked === undefined ? 'menuitem' : 'menuitemradio'"
|
|
171
|
+
:aria-checked="item.checked"
|
|
171
172
|
:disabled="item.disabled"
|
|
172
173
|
@click="handleSelect(item)"
|
|
173
174
|
>
|
|
175
|
+
<Check
|
|
176
|
+
v-if="item.checked !== undefined"
|
|
177
|
+
class="mint-action-menu__check"
|
|
178
|
+
:class="{ 'mint-action-menu__check--off': !item.checked }"
|
|
179
|
+
:size="14"
|
|
180
|
+
aria-hidden="true"
|
|
181
|
+
/>
|
|
174
182
|
{{ item.label }}
|
|
175
183
|
</button>
|
|
176
184
|
</template>
|
|
@@ -144,8 +144,11 @@ const hasPlatformPageSelector = computed(() =>
|
|
|
144
144
|
props.pageSelector === undefined && (plugin.value?.nav_items?.length ?? 0) > 1,
|
|
145
145
|
)
|
|
146
146
|
const hasPageSelector = computed(() => (props.pageSelector?.length ?? 0) > 1 || hasPlatformPageSelector.value)
|
|
147
|
-
/** Integrated plugins show their name
|
|
148
|
-
|
|
147
|
+
/** Integrated plugins show their name as a static identity in place of the title; the page selector replaces it. */
|
|
148
|
+
// Platforms before 1.3 send no display_name.
|
|
149
|
+
const identityName = computed(() =>
|
|
150
|
+
isIntegrated.value && !hasPageSelector.value ? plugin.value?.display_name || plugin.value?.name : undefined,
|
|
151
|
+
)
|
|
149
152
|
const hasPillNav = computed(() => !!props.pillNav?.length)
|
|
150
153
|
const hasAccountMenu = computed(() => !!props.accountMenu?.length)
|
|
151
154
|
const hasTitleGroup = computed(
|
|
@@ -304,9 +307,7 @@ const effectiveCurrentPageSelectorId = computed(() =>
|
|
|
304
307
|
<!-- Left: static plugin identity (not a control) -->
|
|
305
308
|
<span v-if="identityName" class="mint-topbar__plugin-identity">
|
|
306
309
|
<span class="mint-topbar__plugin-name">{{ identityName }}</span>
|
|
307
|
-
<span v-if="plugin?.version" class="mint-topbar__plugin-version">v{{ plugin.version }}</span>
|
|
308
310
|
</span>
|
|
309
|
-
<span v-if="identityName && hasPageSelector" class="mint-topbar__divider" aria-hidden="true" />
|
|
310
311
|
|
|
311
312
|
<!-- Left: page selector -->
|
|
312
313
|
<AppTopBarPageSelectorInternal
|
|
@@ -443,6 +444,7 @@ const effectiveCurrentPageSelectorId = computed(() =>
|
|
|
443
444
|
:title="settingsConfig?.title"
|
|
444
445
|
:tabs="normalizedSettingsTabs"
|
|
445
446
|
:show-appearance="settingsConfig?.showAppearance ?? true"
|
|
447
|
+
:appearance-first="settingsConfig?.appearanceFirst"
|
|
446
448
|
:size="settingsConfig?.size"
|
|
447
449
|
:layout="settingsConfig?.layout"
|
|
448
450
|
:schema="settingsConfig?.schema"
|
|
@@ -56,7 +56,9 @@ const defaultIconPaths = [
|
|
|
56
56
|
<div class="mint-empty-state__body">
|
|
57
57
|
<h3 v-if="title" class="mint-empty-state__title">{{ title }}</h3>
|
|
58
58
|
<p v-if="description" class="mint-empty-state__description">{{ description }}</p>
|
|
59
|
-
<
|
|
59
|
+
<div v-if="$slots.default" class="mint-empty-state__content">
|
|
60
|
+
<slot />
|
|
61
|
+
</div>
|
|
60
62
|
</div>
|
|
61
63
|
<div v-if="actionLabel" class="mint-empty-state__action">
|
|
62
64
|
<BaseButton variant="secondary" @click="emit('action')">
|
|
@@ -14,6 +14,7 @@ import { useExperimentStore } from '../stores/experiment'
|
|
|
14
14
|
import {
|
|
15
15
|
formatExperimentDate,
|
|
16
16
|
formatExperimentStatus,
|
|
17
|
+
getExperimentStatusVariant,
|
|
17
18
|
EXPERIMENT_STATUS_OPTIONS,
|
|
18
19
|
DATE_PRESET_OPTIONS,
|
|
19
20
|
SORT_OPTIONS,
|
|
@@ -105,8 +106,10 @@ function describe(experiment: ExperimentSummary) {
|
|
|
105
106
|
meta: [
|
|
106
107
|
...(groupToggle.value ? [] : [experiment.project_name || experiment.project || '']),
|
|
107
108
|
owner ? [owner.first_name, owner.last_name].filter(Boolean).join(' ') || owner.username : '',
|
|
108
|
-
formatExperimentStatus(experiment.status),
|
|
109
109
|
].filter(Boolean),
|
|
110
|
+
status: experiment.status
|
|
111
|
+
? { label: formatExperimentStatus(experiment.status), variant: getExperimentStatusVariant(experiment.status) }
|
|
112
|
+
: undefined,
|
|
110
113
|
date: formatExperimentDate(experiment.created_at),
|
|
111
114
|
}
|
|
112
115
|
}
|
|
@@ -12,6 +12,7 @@ import { computed, ref, useTemplateRef } from 'vue'
|
|
|
12
12
|
import BaseModal from './BaseModal.vue'
|
|
13
13
|
import BaseButton from './BaseButton.vue'
|
|
14
14
|
import BaseInput from './BaseInput.vue'
|
|
15
|
+
import ActionMenu from './ActionMenu.vue'
|
|
15
16
|
import SegmentedControl from './SegmentedControl.vue'
|
|
16
17
|
import FileUploader from './FileUploader.vue'
|
|
17
18
|
import FilePickerListInternal from './internal/FilePickerListInternal.vue'
|
|
@@ -24,6 +25,7 @@ import type {
|
|
|
24
25
|
PickerNode,
|
|
25
26
|
PickerSelection,
|
|
26
27
|
PickerSelectionMode,
|
|
28
|
+
PickerSort,
|
|
27
29
|
PickerSource,
|
|
28
30
|
PickerView,
|
|
29
31
|
} from '../types/filePicker'
|
|
@@ -39,6 +41,8 @@ interface Props {
|
|
|
39
41
|
title?: string
|
|
40
42
|
/** `list` shows one folder at a time; `tree` adds disclosure triangles. */
|
|
41
43
|
view?: PickerView
|
|
44
|
+
/** Row order on each open; the user can change it from the path bar until closing. */
|
|
45
|
+
defaultSort?: PickerSort
|
|
42
46
|
/** Host-owned visibility/selection filter; backend must still enforce access. */
|
|
43
47
|
systemFilter?: (node: PickerNode, source: PickerSource) => boolean
|
|
44
48
|
}
|
|
@@ -50,6 +54,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
50
54
|
initialSelection: null,
|
|
51
55
|
title: 'Choose data',
|
|
52
56
|
view: 'list',
|
|
57
|
+
defaultSort: 'newest',
|
|
53
58
|
})
|
|
54
59
|
const emit = defineEmits<{
|
|
55
60
|
'update:open': [open: boolean]
|
|
@@ -68,6 +73,7 @@ const {
|
|
|
68
73
|
source,
|
|
69
74
|
query,
|
|
70
75
|
recursive,
|
|
76
|
+
sort,
|
|
71
77
|
skipBlanks,
|
|
72
78
|
reviewing,
|
|
73
79
|
nav,
|
|
@@ -109,6 +115,19 @@ const sourceLabel = computed(() => {
|
|
|
109
115
|
}
|
|
110
116
|
return mounts.value.find((item) => item.path === props.adapter.mountOf?.(node.path))?.name ?? sourceNames.server
|
|
111
117
|
})
|
|
118
|
+
const SORT_LABELS: Record<PickerSort, string> = {
|
|
119
|
+
newest: 'Newest first',
|
|
120
|
+
oldest: 'Oldest first',
|
|
121
|
+
name: 'Name A–Z',
|
|
122
|
+
'name-desc': 'Name Z–A',
|
|
123
|
+
}
|
|
124
|
+
const sortItems = computed(() =>
|
|
125
|
+
(Object.keys(SORT_LABELS) as PickerSort[]).map((id) => ({
|
|
126
|
+
id,
|
|
127
|
+
label: SORT_LABELS[id],
|
|
128
|
+
checked: sort.value === id,
|
|
129
|
+
})),
|
|
130
|
+
)
|
|
112
131
|
const accepts = computed(() =>
|
|
113
132
|
(capability.value?.formats ?? []).map((format) => `.${format.replace(/^\./, '')}`).join(' '),
|
|
114
133
|
)
|
|
@@ -198,6 +217,28 @@ const summary = computed(() => {
|
|
|
198
217
|
</template>
|
|
199
218
|
</nav>
|
|
200
219
|
<span v-if="accepts" class="mint-picker__accepts">Accepts {{ accepts }}</span>
|
|
220
|
+
<ActionMenu
|
|
221
|
+
label="Sort"
|
|
222
|
+
:items="sortItems"
|
|
223
|
+
@select="sort = $event.id as PickerSort"
|
|
224
|
+
>
|
|
225
|
+
<template #trigger="{ open: menuOpen, toggle, keydown }">
|
|
226
|
+
<button
|
|
227
|
+
type="button"
|
|
228
|
+
class="mint-picker__sort"
|
|
229
|
+
aria-haspopup="menu"
|
|
230
|
+
:aria-expanded="menuOpen"
|
|
231
|
+
:aria-label="`Sort: ${SORT_LABELS[sort]}`"
|
|
232
|
+
@click="toggle"
|
|
233
|
+
@keydown="keydown"
|
|
234
|
+
>
|
|
235
|
+
{{ SORT_LABELS[sort] }}
|
|
236
|
+
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
|
237
|
+
<path d="M4 6l4 4 4-4" />
|
|
238
|
+
</svg>
|
|
239
|
+
</button>
|
|
240
|
+
</template>
|
|
241
|
+
</ActionMenu>
|
|
201
242
|
<button
|
|
202
243
|
type="button"
|
|
203
244
|
class="mint-picker__scope"
|
|
@@ -255,6 +296,7 @@ const summary = computed(() => {
|
|
|
255
296
|
:loading="loading"
|
|
256
297
|
:error="error"
|
|
257
298
|
:query="query"
|
|
299
|
+
:truncated="nav.truncated.value"
|
|
258
300
|
:mode="selectionMode"
|
|
259
301
|
:selected="selection.selected"
|
|
260
302
|
:selectable="selection.selectable"
|