@morscherlab/mint-sdk 1.0.61 → 1.1.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +101 -2
- package/dist/EmptyState-D1c4RdC7.js +638 -0
- package/dist/EmptyState-D1c4RdC7.js.map +1 -0
- package/dist/{ExperimentPopover-BbPkIFsI.js → ExperimentPopover-BFw3QX8L.js} +10 -9
- package/dist/{ExperimentPopover-BbPkIFsI.js.map → ExperimentPopover-BFw3QX8L.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-DZeSd-yr.js → ExperimentSelectorModal-B7w6k3Y8.js} +12 -216
- package/dist/ExperimentSelectorModal-B7w6k3Y8.js.map +1 -0
- package/dist/{SettingsModal-CJgLadsN.js → SettingsModal-DPDwCbXK.js} +12 -9
- package/dist/{SettingsModal-CJgLadsN.js.map → SettingsModal-DPDwCbXK.js.map} +1 -1
- package/dist/__tests__/components/GeneratedPathInput.test.d.ts +1 -0
- package/dist/__tests__/components/GeneratedPlotlyResult.test.d.ts +1 -0
- package/dist/__tests__/components/GeneratedPluginView.test.d.ts +1 -0
- package/dist/__tests__/components/GeneratedResultRenderer.test.d.ts +1 -0
- package/dist/__tests__/components/InstrumentAlertLog.test.d.ts +1 -0
- package/dist/__tests__/composables/useGeneratedAnalysis.test.d.ts +1 -0
- package/dist/__tests__/generatedUi.test.d.ts +1 -0
- package/dist/{analysisArtifactTypes-Cu40dzSG.js → analysisArtifactTypes-ejqR8MX3.js} +1 -1
- package/dist/{analysisArtifactTypes-Cu40dzSG.js.map → analysisArtifactTypes-ejqR8MX3.js.map} +1 -1
- package/dist/{auth-DZAwEeis.js → auth-B87xM--s.js} +1 -1
- package/dist/{auth-DZAwEeis.js.map → auth-B87xM--s.js.map} +1 -1
- package/dist/chunk-BoAXSpZd.js +33 -0
- package/dist/components/AppSidebar.vue.d.ts +1 -1
- package/dist/components/AutoGroupModal.vue.d.ts +2 -2
- package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
- package/dist/components/CollapsibleCard.vue.d.ts +1 -1
- package/dist/components/ComponentBindingRenderer.vue.d.ts +1 -1
- package/dist/components/ConfirmDialog.vue.d.ts +1 -1
- package/dist/components/ExperimentDataViewer.vue.d.ts +1 -1
- package/dist/components/ExperimentTimeline.vue.d.ts +1 -1
- package/dist/components/FileUploader.vue.d.ts +4 -2
- package/dist/components/FitPanel.vue.d.ts +1 -1
- package/dist/components/FormActions.vue.d.ts +1 -1
- package/dist/components/FormBuilder.vue.d.ts +1 -1
- package/dist/components/GeneratedPathInput.vue.d.ts +12 -0
- package/dist/components/GeneratedPlotlyResult.vue.d.ts +11 -0
- package/dist/components/GeneratedPluginView.vue.d.ts +10 -0
- package/dist/components/GeneratedResultRenderer.vue.d.ts +6 -0
- package/dist/components/JobsStatusTray.vue.d.ts +5 -1
- package/dist/components/LcmsSequenceTable.vue.d.ts +1 -1
- package/dist/components/SampleLegend.vue.d.ts +1 -1
- package/dist/components/SampleSelector.vue.d.ts +2 -2
- package/dist/components/SettingsModal.vue.d.ts +1 -1
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +2 -2
- package/dist/components/index.d.ts +4 -0
- package/dist/components/index.js +8 -9
- package/dist/components/internal/PlateMapEditorToolbarInternal.vue.d.ts +2 -2
- package/dist/{components-CpnOaPbP.js → components-UfkPN1bK.js} +2255 -2303
- package/dist/components-UfkPN1bK.js.map +1 -0
- package/dist/composables/index.d.ts +2 -1
- package/dist/composables/index.js +10 -11
- package/dist/composables/useGeneratedAnalysis.d.ts +62 -0
- package/dist/composables/useJobsStatusTray.d.ts +48 -4
- package/dist/composables/usePluginClient.d.ts +6 -3
- package/dist/{composables-OYTD0Y3r.js → composables-F4JFK9K1.js} +37 -15
- package/dist/{composables-OYTD0Y3r.js.map → composables-F4JFK9K1.js.map} +1 -1
- package/dist/experiment-utils-CxkruZ3G.js +392 -0
- package/dist/experiment-utils-CxkruZ3G.js.map +1 -0
- package/dist/generated/schema.d.ts +7 -0
- package/dist/generated-ui/main.d.ts +0 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +15 -18
- package/dist/index.js.map +1 -1
- package/dist/install.js +7 -8
- package/dist/install.js.map +1 -1
- package/dist/plotly.min-BR53hja4.js +180634 -0
- package/dist/plotly.min-BR53hja4.js.map +1 -0
- package/dist/stores/auth.d.ts +2 -2
- package/dist/stores/index.js +1 -1
- package/dist/styles.css +10543 -10404
- package/dist/templates/index.js +4 -3
- package/dist/templates/packs.d.ts +0 -1
- package/dist/{templates-CNbPQNID.js → templates-Cttbjbyc.js} +2 -4
- package/dist/templates-Cttbjbyc.js.map +1 -0
- package/dist/types/form-builder.d.ts +1 -1
- package/dist/types/generated-ui.d.ts +170 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/index.js +1 -1
- package/dist/types/instrument.d.ts +1 -0
- package/dist/types/jobs.d.ts +7 -1
- package/dist/{useControlSchema-BZNdalmL.js → useControlSchema-Dallso-L.js} +1852 -171
- package/dist/useControlSchema-Dallso-L.js.map +1 -0
- package/dist/{useDropdownState-Bb-c_PeE.js → useDropdownState-gVaMXtwW.js} +2 -2
- package/dist/{useDropdownState-Bb-c_PeE.js.map → useDropdownState-gVaMXtwW.js.map} +1 -1
- package/dist/{useExperimentSelector-DRbj6vse.js → useExperimentSelector-BMjzNcf2.js} +4 -255
- package/dist/useExperimentSelector-BMjzNcf2.js.map +1 -0
- package/dist/{useFormBuilder-DKekvXRs.js → useFormBuilder-BXaaDI41.js} +2 -2
- package/dist/{useFormBuilder-DKekvXRs.js.map → useFormBuilder-BXaaDI41.js.map} +1 -1
- package/dist/{useAnalysisArtifacts-Ob8UtVwl.js → useProtocolTemplates-BP7_4RGQ.js} +307 -449
- package/dist/useProtocolTemplates-BP7_4RGQ.js.map +1 -0
- package/package.json +7 -3
- package/src/__tests__/components/GeneratedPathInput.test.ts +132 -0
- package/src/__tests__/components/GeneratedPlotlyResult.test.ts +117 -0
- package/src/__tests__/components/GeneratedPluginView.test.ts +230 -0
- package/src/__tests__/components/GeneratedResultRenderer.test.ts +93 -0
- package/src/__tests__/components/InstrumentAlertLog.test.ts +59 -0
- package/src/__tests__/composables/formBuilderRegistry.test.ts +5 -1
- package/src/__tests__/composables/useApi.test.ts +8 -0
- package/src/__tests__/composables/useGeneratedAnalysis.test.ts +1740 -0
- package/src/__tests__/composables/useJobsStatusTray.test.ts +179 -1
- package/src/__tests__/composables/usePluginClient.test.ts +132 -1
- package/src/__tests__/docs/extractDocsTemplates.test.ts +0 -1
- package/src/__tests__/generatedUi.test.ts +119 -0
- package/src/__tests__/templates/templates.test.ts +1 -1
- package/src/components/FileUploader.vue +49 -11
- package/src/components/GeneratedPathInput.vue +218 -0
- package/src/components/GeneratedPlotlyResult.vue +87 -0
- package/src/components/GeneratedPluginView.vue +279 -0
- package/src/components/GeneratedResultRenderer.vue +137 -0
- package/src/components/InstrumentAlertLog.vue +2 -2
- package/src/components/JobsStatusTray.vue +14 -8
- package/src/components/index.ts +4 -0
- package/src/composables/formBuilderRegistry.ts +2 -0
- package/src/composables/index.ts +18 -0
- package/src/composables/useApi.ts +1 -0
- package/src/composables/useGeneratedAnalysis.ts +1252 -0
- package/src/composables/useJobsStatusTray.ts +234 -45
- package/src/composables/usePluginClient.ts +54 -14
- package/src/generated/schema.ts +130 -0
- package/src/generated-ui/main.ts +21 -0
- package/src/index.ts +3 -0
- package/src/jobs.ts +5 -0
- package/src/plotly-dist.d.ts +3 -0
- package/src/templates/packs.ts +0 -3
- package/src/types/form-builder.ts +1 -0
- package/src/types/generated-ui.ts +176 -0
- package/src/types/index.ts +2 -0
- package/src/types/instrument.ts +1 -0
- package/src/types/jobs.ts +10 -1
- package/dist/BaseButton-Dgqrze41.js +0 -74
- package/dist/BaseButton-Dgqrze41.js.map +0 -1
- package/dist/BaseModal-B9UA8Y_I.js +0 -165
- package/dist/BaseModal-B9UA8Y_I.js.map +0 -1
- package/dist/BaseSelect-ekgr9fDo.js +0 -179
- package/dist/BaseSelect-ekgr9fDo.js.map +0 -1
- package/dist/ExperimentPopover-Ryusjrhv.js +0 -4
- package/dist/ExperimentSelectorModal-Cg4-LXBD.js +0 -5
- package/dist/ExperimentSelectorModal-DZeSd-yr.js.map +0 -1
- package/dist/SettingsModal-Bl_9w2Y3.js +0 -5
- package/dist/components-CpnOaPbP.js.map +0 -1
- package/dist/experiment-utils-Bfa7CwPU.js +0 -108
- package/dist/experiment-utils-Bfa7CwPU.js.map +0 -1
- package/dist/templates-CNbPQNID.js.map +0 -1
- package/dist/useAnalysisArtifacts-Ob8UtVwl.js.map +0 -1
- package/dist/useControlSchema-BZNdalmL.js.map +0 -1
- package/dist/useEventListener-CfVkP9Xz.js +0 -57
- package/dist/useEventListener-CfVkP9Xz.js.map +0 -1
- package/dist/useExperimentSelector-DRbj6vse.js.map +0 -1
|
@@ -2,7 +2,12 @@ import { mount } from '@vue/test-utils'
|
|
|
2
2
|
import { defineComponent, nextTick, ref, shallowRef } from 'vue'
|
|
3
3
|
import { describe, expect, it, vi } from 'vitest'
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
combinePluginJobSources,
|
|
7
|
+
useJobsStatusTray,
|
|
8
|
+
usePluginJobCenter,
|
|
9
|
+
type UseJobsStatusTrayOptions,
|
|
10
|
+
} from '../../composables/useJobsStatusTray'
|
|
6
11
|
import type { JobState, JobStateInput, JobStatus } from '../../types/jobs'
|
|
7
12
|
|
|
8
13
|
function job(status: JobStatus, id: string = status): JobStateInput {
|
|
@@ -62,6 +67,179 @@ function deferred<T>(): {
|
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
describe('useJobsStatusTray', () => {
|
|
70
|
+
it('combines live source jobs and fans out or routes source actions', async () => {
|
|
71
|
+
const firstJobs = shallowRef([job('running', 'first')])
|
|
72
|
+
const secondJobs = shallowRef([job('completed', 'second')])
|
|
73
|
+
const firstRefresh = vi.fn(async () => firstJobs.value)
|
|
74
|
+
const secondRefresh = vi.fn(async () => secondJobs.value)
|
|
75
|
+
const secondCancel = vi.fn(async (jobId: string) => job('cancelled', jobId))
|
|
76
|
+
const firstClear = vi.fn(async () => 1)
|
|
77
|
+
const secondClear = vi.fn(async () => 2)
|
|
78
|
+
const firstDispose = vi.fn()
|
|
79
|
+
const secondDispose = vi.fn()
|
|
80
|
+
const combined = combinePluginJobSources([
|
|
81
|
+
{
|
|
82
|
+
jobs: firstJobs,
|
|
83
|
+
refreshJobs: firstRefresh,
|
|
84
|
+
clearFinishedJobs: firstClear,
|
|
85
|
+
dispose: firstDispose,
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
jobs: secondJobs,
|
|
89
|
+
refreshJobs: secondRefresh,
|
|
90
|
+
cancelJob: secondCancel,
|
|
91
|
+
clearFinishedJobs: secondClear,
|
|
92
|
+
dispose: secondDispose,
|
|
93
|
+
},
|
|
94
|
+
])
|
|
95
|
+
|
|
96
|
+
expect(combined.jobs.value.map(item => item.job_id)).toEqual(['first', 'second'])
|
|
97
|
+
secondJobs.value = [job('running', 'replacement')]
|
|
98
|
+
await nextTick()
|
|
99
|
+
expect(combined.jobs.value.map(item => item.job_id)).toEqual(['first', 'replacement'])
|
|
100
|
+
|
|
101
|
+
await combined.refreshJobs?.()
|
|
102
|
+
await combined.cancelJob?.('replacement')
|
|
103
|
+
await expect(combined.clearFinishedJobs?.()).resolves.toBe(3)
|
|
104
|
+
combined.dispose?.()
|
|
105
|
+
|
|
106
|
+
expect({
|
|
107
|
+
refreshes: [firstRefresh.mock.calls.length, secondRefresh.mock.calls.length],
|
|
108
|
+
cancelled: secondCancel.mock.calls[0]?.[0],
|
|
109
|
+
disposed: [firstDispose.mock.calls.length, secondDispose.mock.calls.length],
|
|
110
|
+
}).toEqual({
|
|
111
|
+
refreshes: [1, 1],
|
|
112
|
+
cancelled: 'replacement',
|
|
113
|
+
disposed: [1, 1],
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('retains every failure when disposing combined sources', () => {
|
|
118
|
+
const firstFailure = new Error('first dispose failed')
|
|
119
|
+
const secondFailure = { source: 'second', reason: 'dispose failed' }
|
|
120
|
+
const finalDispose = vi.fn()
|
|
121
|
+
const combined = combinePluginJobSources([
|
|
122
|
+
{
|
|
123
|
+
jobs: [],
|
|
124
|
+
dispose: () => {
|
|
125
|
+
throw firstFailure
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
jobs: [],
|
|
130
|
+
dispose: () => {
|
|
131
|
+
throw secondFailure
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{ jobs: [], dispose: finalDispose },
|
|
135
|
+
])
|
|
136
|
+
|
|
137
|
+
let failure: unknown
|
|
138
|
+
try {
|
|
139
|
+
combined.dispose?.()
|
|
140
|
+
} catch (cause) {
|
|
141
|
+
failure = cause
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
expect(failure).toMatchObject({
|
|
145
|
+
name: 'PluginJobSourceDisposeError',
|
|
146
|
+
message: 'Failed to dispose plugin job sources',
|
|
147
|
+
cause: firstFailure,
|
|
148
|
+
errors: [firstFailure, secondFailure],
|
|
149
|
+
})
|
|
150
|
+
expect(finalDispose).toHaveBeenCalledOnce()
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
it('rejects duplicate ids instead of routing to an arbitrary combined source', async () => {
|
|
154
|
+
const firstCancel = vi.fn(async () => undefined)
|
|
155
|
+
const secondCancel = vi.fn(async () => undefined)
|
|
156
|
+
const combined = combinePluginJobSources([
|
|
157
|
+
{ jobs: [job('running', 'duplicate')], cancelJob: firstCancel },
|
|
158
|
+
{ jobs: [job('queued', 'duplicate')], cancelJob: secondCancel },
|
|
159
|
+
])
|
|
160
|
+
|
|
161
|
+
expect(() => combined.jobs.value).toThrow('Duplicate plugin job id')
|
|
162
|
+
await expect(combined.cancelJob?.('duplicate')).rejects.toThrow('Duplicate plugin job id')
|
|
163
|
+
expect([firstCancel.mock.calls.length, secondCancel.mock.calls.length]).toEqual([0, 0])
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
it('does not expose a partial bulk clear across combined sources', () => {
|
|
167
|
+
const combined = combinePluginJobSources([
|
|
168
|
+
{ jobs: [job('completed', 'first')], clearFinishedJobs: async () => 1 },
|
|
169
|
+
{ jobs: [job('completed', 'second')] },
|
|
170
|
+
])
|
|
171
|
+
|
|
172
|
+
expect(combined.clearFinishedJobs).toBeUndefined()
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('does not partially delete finished jobs when one combined source cannot delete', async () => {
|
|
176
|
+
const firstDelete = vi.fn(async () => true)
|
|
177
|
+
const combined = combinePluginJobSources([
|
|
178
|
+
{ jobs: [job('completed', 'first')], deleteJob: firstDelete },
|
|
179
|
+
{ jobs: [job('completed', 'second')] },
|
|
180
|
+
])
|
|
181
|
+
let center!: ReturnType<typeof usePluginJobCenter>
|
|
182
|
+
const wrapper = mount(defineComponent({
|
|
183
|
+
setup() {
|
|
184
|
+
center = usePluginJobCenter({ source: combined, loadOnMount: false })
|
|
185
|
+
return () => null
|
|
186
|
+
},
|
|
187
|
+
}))
|
|
188
|
+
|
|
189
|
+
expect(combined.deleteJob).toBeUndefined()
|
|
190
|
+
expect(center.canClearFinished.value).toBe(false)
|
|
191
|
+
await expect(center.clearFinished()).resolves.toBe(false)
|
|
192
|
+
expect(firstDelete).not.toHaveBeenCalled()
|
|
193
|
+
|
|
194
|
+
wrapper.unmount()
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
it('derives jobs directly from a canonical source ref', async () => {
|
|
198
|
+
const sourceJobs = shallowRef([job('queued', 'first')])
|
|
199
|
+
let center!: ReturnType<typeof usePluginJobCenter>
|
|
200
|
+
const wrapper = mount(defineComponent({
|
|
201
|
+
setup() {
|
|
202
|
+
center = usePluginJobCenter({
|
|
203
|
+
source: { jobs: sourceJobs },
|
|
204
|
+
loadOnMount: false,
|
|
205
|
+
})
|
|
206
|
+
return () => null
|
|
207
|
+
},
|
|
208
|
+
}))
|
|
209
|
+
|
|
210
|
+
sourceJobs.value = [job('running', 'replacement')]
|
|
211
|
+
await nextTick()
|
|
212
|
+
|
|
213
|
+
expect(center.jobs.value.map(item => item.job_id)).toEqual(['replacement'])
|
|
214
|
+
wrapper.unmount()
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('delegates actions by id and leaves source mutation with the source', async () => {
|
|
218
|
+
const sourceJobs = shallowRef([job('running', 'fit')])
|
|
219
|
+
const cancelJob = vi.fn(async (jobId: string) => {
|
|
220
|
+
sourceJobs.value = [job('cancelled', jobId)]
|
|
221
|
+
return sourceJobs.value[0]
|
|
222
|
+
})
|
|
223
|
+
let center!: ReturnType<typeof usePluginJobCenter>
|
|
224
|
+
const wrapper = mount(defineComponent({
|
|
225
|
+
setup() {
|
|
226
|
+
center = usePluginJobCenter({
|
|
227
|
+
source: { jobs: sourceJobs, cancelJob },
|
|
228
|
+
loadOnMount: false,
|
|
229
|
+
})
|
|
230
|
+
return () => null
|
|
231
|
+
},
|
|
232
|
+
}))
|
|
233
|
+
|
|
234
|
+
await center.cancelJob(center.jobs.value[0]!)
|
|
235
|
+
|
|
236
|
+
expect([cancelJob.mock.calls[0]?.[0], center.jobs.value[0]?.status]).toEqual([
|
|
237
|
+
'fit',
|
|
238
|
+
'cancelled',
|
|
239
|
+
])
|
|
240
|
+
wrapper.unmount()
|
|
241
|
+
})
|
|
242
|
+
|
|
65
243
|
it('keeps awaiting input jobs in the active group', () => {
|
|
66
244
|
const { tray } = mountComposable({
|
|
67
245
|
jobs: [job('awaiting_input')],
|
|
@@ -62,8 +62,10 @@ function pendingEventStreamResponse(): {
|
|
|
62
62
|
describe('usePluginClient', () => {
|
|
63
63
|
let requestConfigs: AxiosRequestConfig[] = []
|
|
64
64
|
let requestBodies: unknown[] = []
|
|
65
|
+
let requestMethods: string[] = []
|
|
65
66
|
let nextGetError: Error | null = null
|
|
66
67
|
let nextGetData: unknown
|
|
68
|
+
let nextPostData: unknown
|
|
67
69
|
let nextPatchData: unknown
|
|
68
70
|
let nextPutData: unknown
|
|
69
71
|
|
|
@@ -71,8 +73,10 @@ describe('usePluginClient', () => {
|
|
|
71
73
|
setActivePinia(createPinia())
|
|
72
74
|
requestConfigs = []
|
|
73
75
|
requestBodies = []
|
|
76
|
+
requestMethods = []
|
|
74
77
|
nextGetError = null
|
|
75
78
|
nextGetData = undefined
|
|
79
|
+
nextPostData = undefined
|
|
76
80
|
nextPatchData = undefined
|
|
77
81
|
nextPutData = undefined
|
|
78
82
|
vi.spyOn(axios.Axios.prototype, 'get').mockImplementation(async function (
|
|
@@ -80,6 +84,7 @@ describe('usePluginClient', () => {
|
|
|
80
84
|
url: string,
|
|
81
85
|
config?: AxiosRequestConfig,
|
|
82
86
|
) {
|
|
87
|
+
requestMethods.push('get')
|
|
83
88
|
requestConfigs.push({ url, ...config })
|
|
84
89
|
if (nextGetError) throw nextGetError
|
|
85
90
|
return { data: nextGetData ?? { called: url } }
|
|
@@ -90,9 +95,10 @@ describe('usePluginClient', () => {
|
|
|
90
95
|
data?: unknown,
|
|
91
96
|
config?: AxiosRequestConfig,
|
|
92
97
|
) {
|
|
98
|
+
requestMethods.push('post')
|
|
93
99
|
requestConfigs.push({ url, ...config })
|
|
94
100
|
requestBodies.push(data)
|
|
95
|
-
return { data: { called: url } }
|
|
101
|
+
return { data: nextPostData ?? { called: url } }
|
|
96
102
|
})
|
|
97
103
|
vi.spyOn(axios.Axios.prototype, 'patch').mockImplementation(async function (
|
|
98
104
|
this: unknown,
|
|
@@ -100,6 +106,7 @@ describe('usePluginClient', () => {
|
|
|
100
106
|
data?: unknown,
|
|
101
107
|
config?: AxiosRequestConfig,
|
|
102
108
|
) {
|
|
109
|
+
requestMethods.push('patch')
|
|
103
110
|
requestConfigs.push({ url, ...config })
|
|
104
111
|
requestBodies.push(data)
|
|
105
112
|
return { data: nextPatchData ?? { called: url } }
|
|
@@ -110,6 +117,7 @@ describe('usePluginClient', () => {
|
|
|
110
117
|
data?: unknown,
|
|
111
118
|
config?: AxiosRequestConfig,
|
|
112
119
|
) {
|
|
120
|
+
requestMethods.push('put')
|
|
113
121
|
requestConfigs.push({ url, ...config })
|
|
114
122
|
requestBodies.push(data)
|
|
115
123
|
return { data: nextPutData ?? { called: url } }
|
|
@@ -119,6 +127,7 @@ describe('usePluginClient', () => {
|
|
|
119
127
|
url: string,
|
|
120
128
|
config?: AxiosRequestConfig,
|
|
121
129
|
) {
|
|
130
|
+
requestMethods.push('delete')
|
|
122
131
|
requestConfigs.push({ url, ...config })
|
|
123
132
|
requestBodies.push(config?.data)
|
|
124
133
|
return { data: { called: url } }
|
|
@@ -147,6 +156,21 @@ describe('usePluginClient', () => {
|
|
|
147
156
|
expect(requestConfigs[0]!.url).toBe('/health')
|
|
148
157
|
})
|
|
149
158
|
|
|
159
|
+
it('omits MINT auth transport for a public generated endpoint', async () => {
|
|
160
|
+
const client = createPluginClient(contract, {
|
|
161
|
+
endpoints: {
|
|
162
|
+
health: { method: 'get', path: '/health', requiresAuth: false },
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
await client.health()
|
|
167
|
+
|
|
168
|
+
expect(
|
|
169
|
+
(requestConfigs[0] as AxiosRequestConfig & { _mintSkipAuth?: boolean })
|
|
170
|
+
._mintSkipAuth,
|
|
171
|
+
).toBe(true)
|
|
172
|
+
})
|
|
173
|
+
|
|
150
174
|
it('VITE_API_PREFIX overrides contract apiPrefix', async () => {
|
|
151
175
|
vi.stubEnv('VITE_API_PREFIX', '/api/override')
|
|
152
176
|
const client = createPluginClient(contract, {
|
|
@@ -410,6 +434,91 @@ describe('usePluginClient', () => {
|
|
|
410
434
|
expect(requestConfigs[0]!.responseType).toBe('blob')
|
|
411
435
|
})
|
|
412
436
|
|
|
437
|
+
it('downloads generated POST endpoint blobs with the contract body and request options', async () => {
|
|
438
|
+
const expectedBlob = new Blob(['zip'])
|
|
439
|
+
const controller = new AbortController()
|
|
440
|
+
nextPostData = expectedBlob
|
|
441
|
+
|
|
442
|
+
const blob = await downloadPluginEndpoint(
|
|
443
|
+
contract,
|
|
444
|
+
{
|
|
445
|
+
method: 'post',
|
|
446
|
+
path: '/export/{sessionId}/results',
|
|
447
|
+
pathParams: ['sessionId'],
|
|
448
|
+
queryParams: [
|
|
449
|
+
{ name: 'revision', fieldName: 'revision', type: 'number', required: true },
|
|
450
|
+
],
|
|
451
|
+
hasBody: true,
|
|
452
|
+
requestContentType: 'application/json',
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
sessionId: 'session-1',
|
|
456
|
+
revision: 4,
|
|
457
|
+
body: { format: 'wide', correction: false },
|
|
458
|
+
},
|
|
459
|
+
{ signal: controller.signal },
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
expect({
|
|
463
|
+
blob,
|
|
464
|
+
body: requestBodies[0],
|
|
465
|
+
url: requestConfigs[0]!.url,
|
|
466
|
+
params: requestConfigs[0]!.params,
|
|
467
|
+
signal: requestConfigs[0]!.signal,
|
|
468
|
+
responseType: requestConfigs[0]!.responseType,
|
|
469
|
+
method: requestMethods[0],
|
|
470
|
+
}).toEqual({
|
|
471
|
+
blob: expectedBlob,
|
|
472
|
+
body: { format: 'wide', correction: false },
|
|
473
|
+
url: '/export/session-1/results',
|
|
474
|
+
params: { revision: 4 },
|
|
475
|
+
signal: controller.signal,
|
|
476
|
+
responseType: 'blob',
|
|
477
|
+
method: 'post',
|
|
478
|
+
})
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
it.each(['put', 'patch', 'delete'] as const)(
|
|
482
|
+
'downloads generated %s endpoint blobs with the contract body',
|
|
483
|
+
async (method) => {
|
|
484
|
+
await downloadPluginEndpoint(
|
|
485
|
+
contract,
|
|
486
|
+
{
|
|
487
|
+
method,
|
|
488
|
+
path: '/exports/{exportId}',
|
|
489
|
+
pathParams: ['exportId'],
|
|
490
|
+
hasBody: true,
|
|
491
|
+
},
|
|
492
|
+
{ exportId: 'export-1', body: { format: 'wide' } },
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
expect({
|
|
496
|
+
body: requestBodies[0],
|
|
497
|
+
method: requestMethods[0],
|
|
498
|
+
responseType: requestConfigs[0]!.responseType,
|
|
499
|
+
url: requestConfigs[0]!.url,
|
|
500
|
+
}).toEqual({
|
|
501
|
+
body: { format: 'wide' },
|
|
502
|
+
method,
|
|
503
|
+
responseType: 'blob',
|
|
504
|
+
url: '/exports/export-1',
|
|
505
|
+
})
|
|
506
|
+
},
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
it('rejects multipart download request bodies', async () => {
|
|
510
|
+
await expect(downloadPluginEndpoint(
|
|
511
|
+
contract,
|
|
512
|
+
{
|
|
513
|
+
method: 'post',
|
|
514
|
+
path: '/exports',
|
|
515
|
+
hasBody: true,
|
|
516
|
+
requestContentType: 'multipart/form-data',
|
|
517
|
+
},
|
|
518
|
+
{ body: { format: 'wide' } },
|
|
519
|
+
)).rejects.toThrow('Plugin download endpoints do not support multipart request bodies')
|
|
520
|
+
})
|
|
521
|
+
|
|
413
522
|
it.each([
|
|
414
523
|
new Blob(['chunk']),
|
|
415
524
|
new Uint8Array([1, 2, 3]),
|
|
@@ -680,6 +789,28 @@ describe('usePluginClient', () => {
|
|
|
680
789
|
expect(stream.lastEventId.value).toBe('7')
|
|
681
790
|
})
|
|
682
791
|
|
|
792
|
+
it('omits MINT auth from a public plugin event stream', async () => {
|
|
793
|
+
const authStore = useAuthStore()
|
|
794
|
+
authStore.setToken('stream-token', 3600)
|
|
795
|
+
const fetchMock = vi.fn<typeof fetch>(async () =>
|
|
796
|
+
new Response(new ReadableStream({ start: controller => controller.close() }), {
|
|
797
|
+
status: 200,
|
|
798
|
+
}),
|
|
799
|
+
)
|
|
800
|
+
vi.stubGlobal('fetch', fetchMock)
|
|
801
|
+
|
|
802
|
+
usePluginEventStream(
|
|
803
|
+
contract,
|
|
804
|
+
{ method: 'get', path: '/public/events', requiresAuth: false },
|
|
805
|
+
{ reconnect: false },
|
|
806
|
+
)
|
|
807
|
+
|
|
808
|
+
await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledOnce())
|
|
809
|
+
const [, init] = fetchMock.mock.calls[0]!
|
|
810
|
+
|
|
811
|
+
expect((init?.headers as Headers).get('Authorization')).toBeNull()
|
|
812
|
+
})
|
|
813
|
+
|
|
683
814
|
it('normalizes stream auth failures and does not reconnect rejected requests', async () => {
|
|
684
815
|
const authStore = useAuthStore()
|
|
685
816
|
authStore.setToken('expired-stream-token', 3600)
|
|
@@ -47,7 +47,6 @@ describe('extract docs template helpers', () => {
|
|
|
47
47
|
group: packEntry.category,
|
|
48
48
|
source: 'packages/sdk-frontend/src/templates/packs.ts',
|
|
49
49
|
add_command: packEntry.add_command,
|
|
50
|
-
init_command: packEntry.init_command,
|
|
51
50
|
docs_command: packEntry.docs_command,
|
|
52
51
|
})
|
|
53
52
|
})
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
generatedJobDefaults,
|
|
5
|
+
generatedJobFormSchema,
|
|
6
|
+
normalizeGeneratedJobInput,
|
|
7
|
+
} from '../generated/schema'
|
|
8
|
+
import type { GeneratedJobDefinition } from '../types/generated-ui'
|
|
9
|
+
|
|
10
|
+
const definition: GeneratedJobDefinition = {
|
|
11
|
+
id: 'summarize',
|
|
12
|
+
title: 'Summarize',
|
|
13
|
+
description: 'Summarize a dataset',
|
|
14
|
+
cpu: 2,
|
|
15
|
+
input_schema: {
|
|
16
|
+
type: 'object',
|
|
17
|
+
properties: {
|
|
18
|
+
threshold: { type: 'number', title: 'Threshold', default: 2.5, minimum: 0, maximum: 10 },
|
|
19
|
+
method: { type: 'string', title: 'Method', enum: ['fast', 'robust'], default: 'fast' },
|
|
20
|
+
normalize: { type: 'boolean', title: 'Normalize', default: true },
|
|
21
|
+
source: {
|
|
22
|
+
anyOf: [{ type: 'string', format: 'path' }, { type: 'null' }],
|
|
23
|
+
default: null,
|
|
24
|
+
'x-mint-component': 'path',
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
required: ['threshold'],
|
|
28
|
+
},
|
|
29
|
+
output_schema: {},
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('generated UI schema adapter', () => {
|
|
33
|
+
it('maps the supported JSON Schema subset to standard FormBuilder fields', () => {
|
|
34
|
+
expect(generatedJobFormSchema(definition).sections[0]?.fields).toMatchObject([
|
|
35
|
+
{ name: 'threshold', type: 'number', validation: { required: true, min: 0, max: 10 } },
|
|
36
|
+
{ name: 'method', type: 'select', props: { options: ['fast', 'robust'] } },
|
|
37
|
+
{ name: 'normalize', type: 'toggle' },
|
|
38
|
+
{ name: 'source', type: 'path' },
|
|
39
|
+
])
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('extracts defaults without sharing mutable input state', () => {
|
|
43
|
+
const defaults = generatedJobDefaults(definition)
|
|
44
|
+
const second = generatedJobDefaults(definition)
|
|
45
|
+
defaults.threshold = 9
|
|
46
|
+
|
|
47
|
+
expect(second).toEqual({ threshold: 2.5, method: 'fast', normalize: true, source: null })
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('preserves explicit null defaults without inventing values for missing defaults', () => {
|
|
51
|
+
const nullableDefinition: GeneratedJobDefinition = {
|
|
52
|
+
...definition,
|
|
53
|
+
input_schema: {
|
|
54
|
+
type: 'object',
|
|
55
|
+
properties: {
|
|
56
|
+
optional_count: {
|
|
57
|
+
anyOf: [{ type: 'integer' }, { type: 'null' }],
|
|
58
|
+
default: null,
|
|
59
|
+
},
|
|
60
|
+
required_count: { type: 'integer' },
|
|
61
|
+
},
|
|
62
|
+
required: ['required_count'],
|
|
63
|
+
},
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
expect({
|
|
67
|
+
defaults: generatedJobDefaults(nullableDefinition),
|
|
68
|
+
fields: generatedJobFormSchema(nullableDefinition).sections[0]?.fields.map(field => ({
|
|
69
|
+
name: field.name,
|
|
70
|
+
defaultValue: field.defaultValue,
|
|
71
|
+
})),
|
|
72
|
+
}).toEqual({
|
|
73
|
+
defaults: { optional_count: null },
|
|
74
|
+
fields: [
|
|
75
|
+
{ name: 'optional_count', defaultValue: null },
|
|
76
|
+
{ name: 'required_count', defaultValue: undefined },
|
|
77
|
+
],
|
|
78
|
+
})
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('keeps a selected path for SDK staging during submission', () => {
|
|
82
|
+
const selection = {
|
|
83
|
+
kind: 'path' as const,
|
|
84
|
+
files: [new File(['data'], 'data.csv')],
|
|
85
|
+
relativePaths: ['data.csv'],
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
expect(normalizeGeneratedJobInput(definition, {
|
|
89
|
+
threshold: 2.5,
|
|
90
|
+
source: selection,
|
|
91
|
+
})).toEqual({
|
|
92
|
+
threshold: 2.5,
|
|
93
|
+
source: selection,
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('marks path arrays as multiple path inputs', () => {
|
|
98
|
+
const listDefinition: GeneratedJobDefinition = {
|
|
99
|
+
...definition,
|
|
100
|
+
input_schema: {
|
|
101
|
+
type: 'object',
|
|
102
|
+
properties: {
|
|
103
|
+
sources: {
|
|
104
|
+
type: 'array',
|
|
105
|
+
items: { type: 'string', format: 'path' },
|
|
106
|
+
'x-mint-component': 'path',
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
required: ['sources'],
|
|
110
|
+
},
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
expect(generatedJobFormSchema(listDefinition).sections[0]?.fields[0]).toMatchObject({
|
|
114
|
+
name: 'sources',
|
|
115
|
+
type: 'path',
|
|
116
|
+
props: { multiple: true },
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
})
|
|
@@ -166,7 +166,7 @@ describe('bio data templates', () => {
|
|
|
166
166
|
expect(getBioTemplatePackInfo('metabolism')?.name).toBe('omics-assay')
|
|
167
167
|
expect(getBioTemplatePackInfo('metabolite profiling')?.name).toBe('omics-assay')
|
|
168
168
|
expect(getBioTemplatePackInfo('metabolomics')?.components).toContain('ExperimentTimeline')
|
|
169
|
-
expect(getBioTemplatePackInfo('longitudinal')?.
|
|
169
|
+
expect(getBioTemplatePackInfo('longitudinal')?.add_command).toBe('mint add data-template-pack longitudinal-study --page')
|
|
170
170
|
expect(getBioTemplatePackInfo('gene expression')?.templates.at(-1)).toBe('qpcr-plate')
|
|
171
171
|
expect(getBioTemplatePackInfo('gene-expression')?.templates.at(-1)).toBe('qpcr-plate')
|
|
172
172
|
expect(searchBioTemplatePacks('WellPlate').map(entry => entry.name)).toEqual([
|
|
@@ -19,7 +19,8 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
19
19
|
})
|
|
20
20
|
|
|
21
21
|
const emit = defineEmits<{
|
|
22
|
-
upload: [files: File[]]
|
|
22
|
+
upload: [files: File[], rootName?: string, relativePaths?: string[]]
|
|
23
|
+
remove: [files: File[]]
|
|
23
24
|
error: [message: string]
|
|
24
25
|
}>()
|
|
25
26
|
|
|
@@ -116,20 +117,31 @@ function validateFiles(files: FileList | File[]): File[] {
|
|
|
116
117
|
return validFiles
|
|
117
118
|
}
|
|
118
119
|
|
|
119
|
-
function handleFiles(
|
|
120
|
+
function handleFiles(
|
|
121
|
+
files: FileList | File[],
|
|
122
|
+
folder?: string,
|
|
123
|
+
explicitRelativePaths?: string[],
|
|
124
|
+
) {
|
|
125
|
+
const candidates = Array.from(files)
|
|
120
126
|
const validFiles = validateFiles(files)
|
|
121
127
|
if (validFiles.length === 0) return
|
|
128
|
+
const relativePaths = validFiles.map((file) => {
|
|
129
|
+
const candidateIndex = candidates.indexOf(file)
|
|
130
|
+
return explicitRelativePaths?.[candidateIndex]
|
|
131
|
+
|| file.webkitRelativePath
|
|
132
|
+
|| (folder ? `${folder}/${file.name}` : file.name)
|
|
133
|
+
})
|
|
122
134
|
|
|
123
135
|
if (isFolder.value) {
|
|
124
136
|
folderName.value = folder || extractFolderName(validFiles)
|
|
125
137
|
selectedFiles.value = validFiles
|
|
126
|
-
emit('upload', validFiles)
|
|
138
|
+
emit('upload', validFiles, folderName.value ?? undefined, relativePaths)
|
|
127
139
|
} else if (!props.multiple) {
|
|
128
140
|
selectedFiles.value = [validFiles[0]]
|
|
129
|
-
emit('upload', [validFiles[0]])
|
|
141
|
+
emit('upload', [validFiles[0]], undefined, [relativePaths[0]])
|
|
130
142
|
} else {
|
|
131
143
|
selectedFiles.value = [...selectedFiles.value, ...validFiles]
|
|
132
|
-
emit('upload', validFiles)
|
|
144
|
+
emit('upload', validFiles, undefined, relativePaths)
|
|
133
145
|
}
|
|
134
146
|
}
|
|
135
147
|
|
|
@@ -156,25 +168,39 @@ function handleDrop(event: DragEvent) {
|
|
|
156
168
|
|
|
157
169
|
async function handleFolderDrop(items: DataTransferItemList) {
|
|
158
170
|
const files: File[] = []
|
|
171
|
+
const relativePaths: string[] = []
|
|
159
172
|
let folderNameFromDrop: string | undefined
|
|
160
173
|
|
|
161
174
|
for (const item of Array.from(items)) {
|
|
162
175
|
const entry = item.webkitGetAsEntry?.()
|
|
163
176
|
if (entry?.isDirectory) {
|
|
164
177
|
folderNameFromDrop = entry.name
|
|
165
|
-
await readDirectory(
|
|
178
|
+
await readDirectory(
|
|
179
|
+
entry as FileSystemDirectoryEntry,
|
|
180
|
+
files,
|
|
181
|
+
relativePaths,
|
|
182
|
+
entry.name,
|
|
183
|
+
)
|
|
166
184
|
} else if (entry?.isFile) {
|
|
167
185
|
const file = await getFile(entry as FileSystemFileEntry)
|
|
168
|
-
if (file)
|
|
186
|
+
if (file) {
|
|
187
|
+
files.push(file)
|
|
188
|
+
relativePaths.push(file.name)
|
|
189
|
+
}
|
|
169
190
|
}
|
|
170
191
|
}
|
|
171
192
|
|
|
172
193
|
if (files.length > 0) {
|
|
173
|
-
handleFiles(files, folderNameFromDrop)
|
|
194
|
+
handleFiles(files, folderNameFromDrop, relativePaths)
|
|
174
195
|
}
|
|
175
196
|
}
|
|
176
197
|
|
|
177
|
-
async function readDirectory(
|
|
198
|
+
async function readDirectory(
|
|
199
|
+
directory: FileSystemDirectoryEntry,
|
|
200
|
+
files: File[],
|
|
201
|
+
relativePaths: string[],
|
|
202
|
+
prefix: string,
|
|
203
|
+
): Promise<void> {
|
|
178
204
|
const reader = directory.createReader()
|
|
179
205
|
// readEntries returns at most 100 entries per call per spec — must loop
|
|
180
206
|
let batch: FileSystemEntry[]
|
|
@@ -185,9 +211,17 @@ async function readDirectory(directory: FileSystemDirectoryEntry, files: File[])
|
|
|
185
211
|
for (const entry of batch) {
|
|
186
212
|
if (entry.isFile) {
|
|
187
213
|
const file = await getFile(entry as FileSystemFileEntry)
|
|
188
|
-
if (file)
|
|
214
|
+
if (file) {
|
|
215
|
+
files.push(file)
|
|
216
|
+
relativePaths.push(`${prefix}/${file.name}`)
|
|
217
|
+
}
|
|
189
218
|
} else if (entry.isDirectory) {
|
|
190
|
-
await readDirectory(
|
|
219
|
+
await readDirectory(
|
|
220
|
+
entry as FileSystemDirectoryEntry,
|
|
221
|
+
files,
|
|
222
|
+
relativePaths,
|
|
223
|
+
`${prefix}/${entry.name}`,
|
|
224
|
+
)
|
|
191
225
|
}
|
|
192
226
|
}
|
|
193
227
|
} while (batch.length > 0)
|
|
@@ -225,15 +259,19 @@ function openFilePicker() {
|
|
|
225
259
|
}
|
|
226
260
|
|
|
227
261
|
function removeFile(index: number) {
|
|
262
|
+
const removed = selectedFiles.value[index]
|
|
228
263
|
selectedFiles.value = selectedFiles.value.filter((_, i) => i !== index)
|
|
229
264
|
if (isFolder.value && selectedFiles.value.length === 0) {
|
|
230
265
|
folderName.value = null
|
|
231
266
|
}
|
|
267
|
+
if (removed) emit('remove', [removed])
|
|
232
268
|
}
|
|
233
269
|
|
|
234
270
|
function clearAll() {
|
|
271
|
+
const removed = [...selectedFiles.value]
|
|
235
272
|
selectedFiles.value = []
|
|
236
273
|
folderName.value = null
|
|
274
|
+
if (removed.length) emit('remove', removed)
|
|
237
275
|
}
|
|
238
276
|
|
|
239
277
|
function formatFileSize(bytes: number): string {
|