@morscherlab/mint-sdk 1.0.62 → 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-GpJD6R1R.js → composables-F4JFK9K1.js} +22 -13
- package/dist/{composables-GpJD6R1R.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 +37 -0
- 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 +34 -10
- 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')],
|
|
@@ -156,6 +156,21 @@ describe('usePluginClient', () => {
|
|
|
156
156
|
expect(requestConfigs[0]!.url).toBe('/health')
|
|
157
157
|
})
|
|
158
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
|
+
|
|
159
174
|
it('VITE_API_PREFIX overrides contract apiPrefix', async () => {
|
|
160
175
|
vi.stubEnv('VITE_API_PREFIX', '/api/override')
|
|
161
176
|
const client = createPluginClient(contract, {
|
|
@@ -774,6 +789,28 @@ describe('usePluginClient', () => {
|
|
|
774
789
|
expect(stream.lastEventId.value).toBe('7')
|
|
775
790
|
})
|
|
776
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
|
+
|
|
777
814
|
it('normalizes stream auth failures and does not reconnect rejected requests', async () => {
|
|
778
815
|
const authStore = useAuthStore()
|
|
779
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 {
|