@morscherlab/mint-sdk 1.1.25 → 1.2.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +9 -7
  2. package/dist/{ExperimentSelectorModal-B6P2msT4.js → ExperimentSelectorModal-DyPAUCLw.js} +3 -3
  3. package/dist/{ExperimentSelectorModal-B6P2msT4.js.map → ExperimentSelectorModal-DyPAUCLw.js.map} +1 -1
  4. package/dist/{SettingsModal-BW3Ar-uy.js → SettingsModal-BgKY93zh.js} +4 -4
  5. package/dist/{SettingsModal-BW3Ar-uy.js.map → SettingsModal-BgKY93zh.js.map} +1 -1
  6. package/dist/__tests__/composables/useEventStream.test.d.ts +1 -0
  7. package/dist/components/AutoGroupModal.vue.d.ts +1 -1
  8. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +1 -1
  9. package/dist/components/JobsStatusTray.vue.d.ts +2 -9
  10. package/dist/components/SampleLegend.vue.d.ts +1 -1
  11. package/dist/components/SampleSelector.vue.d.ts +1 -1
  12. package/dist/components/SettingsModal.vue.d.ts +1 -1
  13. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +1 -1
  14. package/dist/components/index.js +4 -4
  15. package/dist/{components-DQt0tcpY.js → components-DkHddRMl.js} +10 -16
  16. package/dist/components-DkHddRMl.js.map +1 -0
  17. package/dist/composables/index.d.ts +2 -3
  18. package/dist/composables/index.js +8 -8
  19. package/dist/composables/useEventStream.d.ts +43 -0
  20. package/dist/composables/useGeneratedAnalysis.d.ts +1 -3
  21. package/dist/composables/useJobsStatusTray.d.ts +1 -25
  22. package/dist/composables/usePluginClient.d.ts +3 -18
  23. package/dist/{composables-CkXDe7nm.js → composables-C_i90sGS.js} +32 -308
  24. package/dist/composables-C_i90sGS.js.map +1 -0
  25. package/dist/{experiment-Y9Iz7KYi.js → experiment-Cq145oEW.js} +2 -2
  26. package/dist/{experiment-Y9Iz7KYi.js.map → experiment-Cq145oEW.js.map} +1 -1
  27. package/dist/index.js +12 -12
  28. package/dist/install.js +4 -4
  29. package/dist/stores/auth.d.ts +0 -6
  30. package/dist/stores/index.js +2 -2
  31. package/dist/templates/index.js +2 -2
  32. package/dist/{templates-CovvRZ-9.js → templates-DsP51i-S.js} +2 -2
  33. package/dist/{templates-CovvRZ-9.js.map → templates-DsP51i-S.js.map} +1 -1
  34. package/dist/types/auth.d.ts +0 -1
  35. package/dist/{useApi-CDBq6z23.js → useApi-J6zFkHmn.js} +1 -2
  36. package/dist/useApi-J6zFkHmn.js.map +1 -0
  37. package/dist/{useControlSchema-BhoJzWzx.js → useControlSchema-BDx2Olha.js} +239 -103
  38. package/dist/useControlSchema-BDx2Olha.js.map +1 -0
  39. package/dist/{useExperimentSelector-D0pjj9WH.js → useExperimentSelector-CV4iNfMS.js} +2 -2
  40. package/dist/{useExperimentSelector-D0pjj9WH.js.map → useExperimentSelector-CV4iNfMS.js.map} +1 -1
  41. package/dist/{useFormBuilder-DiEcDis2.js → useFormBuilder-BLxG3BOX.js} +2 -2
  42. package/dist/{useFormBuilder-DiEcDis2.js.map → useFormBuilder-BLxG3BOX.js.map} +1 -1
  43. package/dist/{useProtocolTemplates-DHwSveE6.js → useProtocolTemplates-CS_SQl6U.js} +22 -113
  44. package/dist/useProtocolTemplates-CS_SQl6U.js.map +1 -0
  45. package/package.json +1 -1
  46. package/src/__tests__/components/GeneratedPluginView.test.ts +0 -5
  47. package/src/__tests__/components/JobsStatusTray.test.ts +20 -18
  48. package/src/__tests__/composables/useAuth.test.ts +23 -0
  49. package/src/__tests__/composables/useEventStream.test.ts +140 -0
  50. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +103 -0
  51. package/src/__tests__/composables/useJobsStatusTray.test.ts +25 -161
  52. package/src/__tests__/composables/usePluginClient.test.ts +8 -13
  53. package/src/__tests__/docs/frontendDocsCatalog.test.ts +22 -1
  54. package/src/components/JobsStatusTray.story.vue +6 -10
  55. package/src/components/JobsStatusTray.vue +1 -16
  56. package/src/composables/index.ts +0 -7
  57. package/src/composables/useAuth.ts +0 -3
  58. package/src/composables/useEventStream.ts +314 -0
  59. package/src/composables/useGeneratedAnalysis.ts +40 -109
  60. package/src/composables/useJobsStatusTray.ts +17 -184
  61. package/src/composables/usePluginClient.ts +45 -249
  62. package/src/stores/auth.ts +0 -1
  63. package/src/types/auth.ts +0 -1
  64. package/dist/__tests__/composables/usePluginConfig.test.d.ts +0 -15
  65. package/dist/components-DQt0tcpY.js.map +0 -1
  66. package/dist/composables/usePluginConfig.d.ts +0 -25
  67. package/dist/composables-CkXDe7nm.js.map +0 -1
  68. package/dist/useApi-CDBq6z23.js.map +0 -1
  69. package/dist/useControlSchema-BhoJzWzx.js.map +0 -1
  70. package/dist/useProtocolTemplates-DHwSveE6.js.map +0 -1
  71. package/src/__tests__/composables/usePluginConfig.test.ts +0 -497
  72. package/src/composables/usePluginConfig.ts +0 -250
@@ -1,497 +0,0 @@
1
- /**
2
- * Tests for usePluginConfig.
3
- *
4
- * Covers the plugin settings life-cycle used by every plugin's
5
- * settings pane:
6
- *
7
- * - resolvedName prefers explicit pluginName, falls back to platform plugin
8
- * - load() populates config + savedConfig from /plugins/{name}/config
9
- * - save() PATCHes the endpoint, updates savedConfig, returns true
10
- * - isDirty reflects user edits vs last saved state
11
- * - reset() restores config to savedConfig and clears error
12
- * - lastLoadedAt / lastSavedAt track successful syncs
13
- * - error messages bubble up from network failures
14
- */
15
-
16
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
17
- import { createPinia, setActivePinia } from 'pinia'
18
- import axios, { type AxiosRequestConfig } from 'axios'
19
- import { nextTick } from 'vue'
20
-
21
- // usePluginConfig calls onMounted(load); happy-dom doesn't run Vue's
22
- // lifecycle outside a component, so we stub onMounted to call the
23
- // callback synchronously if and only if the test opts in.
24
- let autoLoadOnMount = false
25
- vi.mock('vue', async () => {
26
- const actual = await vi.importActual<typeof import('vue')>('vue')
27
- return {
28
- ...actual,
29
- onMounted: vi.fn((cb: () => void) => {
30
- if (autoLoadOnMount) cb()
31
- }),
32
- }
33
- })
34
-
35
- import { usePluginConfig } from '../../composables/usePluginConfig'
36
-
37
- type FakeResponse = Record<string, unknown>
38
-
39
- let getResponse: FakeResponse = { plugin_name: 'drp', config: {} }
40
- let patchResponse: FakeResponse = { plugin_name: 'drp', config: {} }
41
- let putResponse: FakeResponse = { plugin_name: 'drp', config: {} }
42
- let getError: Error | null = null
43
- let patchError: Error | null = null
44
- let recordedBaseUrls: Array<string | undefined> = []
45
- let recordedCalls: {
46
- method: string
47
- url: string
48
- data?: unknown
49
- headers?: AxiosRequestConfig['headers']
50
- }[] = []
51
-
52
- function mockAxios() {
53
- vi.spyOn(axios.Axios.prototype, 'get').mockImplementation(async function (
54
- this: unknown,
55
- url: string,
56
- config?: AxiosRequestConfig,
57
- ) {
58
- recordedBaseUrls.push(config?.baseURL)
59
- recordedCalls.push({ method: 'GET', url })
60
- if (getError) throw getError
61
- return { data: getResponse }
62
- })
63
- vi.spyOn(axios.Axios.prototype, 'patch').mockImplementation(async function (
64
- this: unknown,
65
- url: string,
66
- data?: unknown,
67
- config?: AxiosRequestConfig,
68
- ) {
69
- recordedBaseUrls.push(config?.baseURL)
70
- recordedCalls.push({ method: 'PATCH', url, data })
71
- if (patchError) throw patchError
72
- return { data: patchResponse }
73
- })
74
- vi.spyOn(axios.Axios.prototype, 'put').mockImplementation(async function (
75
- this: unknown,
76
- url: string,
77
- data?: unknown,
78
- config?: AxiosRequestConfig,
79
- ) {
80
- recordedBaseUrls.push(config?.baseURL)
81
- recordedCalls.push({ method: 'PUT', url, data, headers: config?.headers })
82
- return { data: putResponse }
83
- })
84
- }
85
-
86
- describe('usePluginConfig', () => {
87
- beforeEach(() => {
88
- setActivePinia(createPinia())
89
- autoLoadOnMount = false
90
- getResponse = { plugin_name: 'drp', config: {} }
91
- patchResponse = { plugin_name: 'drp', config: {} }
92
- putResponse = { plugin_name: 'drp', config: {} }
93
- getError = null
94
- patchError = null
95
- recordedCalls = []
96
- recordedBaseUrls = []
97
- delete (window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__
98
- mockAxios()
99
- })
100
-
101
- afterEach(() => {
102
- delete (window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__
103
- vi.restoreAllMocks()
104
- })
105
-
106
- it('load fetches config from correct endpoint and populates state', async () => {
107
- getResponse = {
108
- plugin_name: 'drp',
109
- config: { raw_files_path: '/data/rfa', threshold: 0.5 },
110
- }
111
- const hook = usePluginConfig('drp')
112
- await hook.load()
113
-
114
- expect(recordedCalls).toEqual([
115
- { method: 'GET', url: '/plugins/drp/config' },
116
- ])
117
- expect(hook.config.value).toEqual({
118
- raw_files_path: '/data/rfa',
119
- threshold: 0.5,
120
- })
121
- expect(hook.isDirty.value).toBe(false)
122
- expect(hook.error.value).toBeNull()
123
- expect(hook.lastLoadedAt.value).toBeInstanceOf(Date)
124
- })
125
-
126
- it('encodes special characters in plugin name', async () => {
127
- const hook = usePluginConfig('my/plugin')
128
- await hook.load()
129
- expect(recordedCalls[0]!.url).toBe('/plugins/my%2Fplugin/config')
130
- })
131
-
132
- it.each([
133
- 'platform',
134
- undefined,
135
- ] as const)(
136
- 'should use the platform config facade when settings_api is %s',
137
- async (settingsApi) => {
138
- ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
139
- isIntegrated: true,
140
- theme: 'light',
141
- platformApiUrl: '/api',
142
- plugin: {
143
- id: 'drp',
144
- name: 'drp',
145
- version: '1.0.0',
146
- route_prefix: '/drp',
147
- api_prefix: '/api/drp',
148
- ...(settingsApi ? { settings_api: settingsApi } : {}),
149
- },
150
- }
151
- getResponse = {
152
- plugin_name: 'drp',
153
- config: { n: 1 },
154
- revision: 'revision-1',
155
- }
156
- const hook = usePluginConfig('drp')
157
-
158
- await hook.load()
159
-
160
- expect(recordedCalls).toEqual([
161
- { method: 'GET', url: '/plugins/drp/config' },
162
- ])
163
- },
164
- )
165
-
166
- it('should use the plugin settings router for an integrated plugin capability', async () => {
167
- ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
168
- isIntegrated: true,
169
- theme: 'light',
170
- platformApiUrl: '/api',
171
- plugin: {
172
- id: 'drp',
173
- name: 'drp',
174
- version: '1.0.0',
175
- route_prefix: '/drp',
176
- api_prefix: '/api/drp',
177
- settings_api: 'plugin',
178
- },
179
- }
180
- getResponse = {
181
- settings: { n: 1 },
182
- mode: 'integrated',
183
- revision: 'revision-1',
184
- }
185
- putResponse = {
186
- settings: { n: 2 },
187
- mode: 'integrated',
188
- revision: 'revision-2',
189
- }
190
- const hook = usePluginConfig('drp')
191
- await hook.load()
192
- hook.config.value = { n: 2 }
193
-
194
- const saved = await hook.save()
195
-
196
- expect({
197
- saved,
198
- calls: recordedCalls,
199
- baseUrls: recordedBaseUrls,
200
- config: hook.config.value,
201
- }).toEqual({
202
- saved: true,
203
- baseUrls: ['/api/drp', '/api/drp'],
204
- calls: [
205
- { method: 'GET', url: '/settings' },
206
- {
207
- method: 'PUT',
208
- url: '/settings',
209
- data: { n: 2 },
210
- headers: { 'If-Match': '"revision-1"' },
211
- },
212
- ],
213
- config: { n: 2 },
214
- })
215
- })
216
-
217
- it('should use the platform facade for an explicit different plugin name', async () => {
218
- ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
219
- isIntegrated: true,
220
- theme: 'light',
221
- platformApiUrl: '/api',
222
- plugin: {
223
- id: 'current-plugin',
224
- name: 'current-plugin',
225
- version: '1.0.0',
226
- route_prefix: '/current-plugin',
227
- api_prefix: '/api/current-plugin',
228
- settings_api: 'plugin',
229
- },
230
- }
231
- getResponse = {
232
- plugin_name: 'other-plugin',
233
- config: { n: 1 },
234
- revision: 'revision-1',
235
- }
236
-
237
- const hook = usePluginConfig('other-plugin')
238
- await hook.load()
239
-
240
- expect({
241
- calls: recordedCalls,
242
- config: hook.config.value,
243
- }).toEqual({
244
- calls: [
245
- { method: 'GET', url: '/plugins/other-plugin/config' },
246
- ],
247
- config: { n: 1 },
248
- })
249
- })
250
-
251
- it('should PATCH only dirty typed fields through a versioned platform facade', async () => {
252
- getResponse = {
253
- plugin_name: 'drp',
254
- config: {
255
- n: 1,
256
- allowed_experiment_types: ['metabolomics'],
257
- },
258
- revision: 'revision-1',
259
- }
260
- patchResponse = {
261
- plugin_name: 'drp',
262
- config: {
263
- n: 2,
264
- allowed_experiment_types: ['metabolomics'],
265
- },
266
- revision: 'revision-2',
267
- }
268
- const hook = usePluginConfig('drp')
269
- await hook.load()
270
- hook.config.value = {
271
- n: 2,
272
- allowed_experiment_types: ['metabolomics'],
273
- }
274
- await nextTick()
275
- expect(hook.isDirty.value).toBe(true)
276
-
277
- const ok = await hook.save()
278
- expect(ok).toBe(true)
279
- const patchCall = recordedCalls.find((c) => c.method === 'PATCH')!
280
- expect(patchCall.url).toBe('/plugins/drp/config')
281
- expect(patchCall.data).toEqual({ config: { n: 2 } })
282
- expect(patchCall.headers).toBeUndefined()
283
- expect(hook.config.value).toEqual({
284
- n: 2,
285
- allowed_experiment_types: ['metabolomics'],
286
- })
287
- expect(hook.isDirty.value).toBe(false)
288
- expect(hook.lastSavedAt.value).toBeInstanceOf(Date)
289
- })
290
-
291
- it('should split changed platform-owned config from a versioned typed save', async () => {
292
- getResponse = {
293
- plugin_name: 'drp',
294
- config: {
295
- n: 1,
296
- allowed_experiment_types: ['metabolomics'],
297
- },
298
- revision: 'revision-1',
299
- }
300
- patchResponse = {
301
- plugin_name: 'drp',
302
- config: {
303
- n: 2,
304
- allowed_experiment_types: ['proteomics'],
305
- },
306
- revision: 'untrusted-platform-revision',
307
- }
308
- const hook = usePluginConfig('drp')
309
- await hook.load()
310
- hook.config.value = {
311
- n: 2,
312
- allowed_experiment_types: ['proteomics'],
313
- }
314
-
315
- const saved = await hook.save()
316
- patchResponse = {
317
- plugin_name: 'drp',
318
- config: {
319
- n: 3,
320
- allowed_experiment_types: ['proteomics'],
321
- },
322
- revision: 'revision-3',
323
- }
324
- hook.config.value = {
325
- n: 3,
326
- allowed_experiment_types: ['proteomics'],
327
- }
328
- const savedAgain = await hook.save()
329
-
330
- expect({
331
- saved,
332
- savedAgain,
333
- calls: recordedCalls,
334
- config: hook.config.value,
335
- isDirty: hook.isDirty.value,
336
- }).toEqual({
337
- saved: true,
338
- savedAgain: true,
339
- calls: [
340
- { method: 'GET', url: '/plugins/drp/config' },
341
- {
342
- method: 'PATCH',
343
- url: '/plugins/drp/config',
344
- data: { config: { n: 2 } },
345
- },
346
- {
347
- method: 'PATCH',
348
- url: '/plugins/drp/config',
349
- data: { config: { allowed_experiment_types: ['proteomics'] } },
350
- },
351
- {
352
- method: 'PATCH',
353
- url: '/plugins/drp/config',
354
- data: { config: { n: 3 } },
355
- },
356
- ],
357
- config: {
358
- n: 3,
359
- allowed_experiment_types: ['proteomics'],
360
- },
361
- isDirty: false,
362
- })
363
- })
364
-
365
- it('should avoid a typed PUT when only the platform-owned config changes', async () => {
366
- getResponse = {
367
- plugin_name: 'drp',
368
- config: {
369
- n: 1,
370
- allowed_experiment_types: ['metabolomics'],
371
- },
372
- revision: 'revision-1',
373
- }
374
- patchResponse = {
375
- plugin_name: 'drp',
376
- config: {
377
- n: 1,
378
- allowed_experiment_types: ['proteomics'],
379
- },
380
- }
381
- const hook = usePluginConfig('drp')
382
- await hook.load()
383
- hook.config.value = {
384
- n: 1,
385
- allowed_experiment_types: ['proteomics'],
386
- }
387
-
388
- const saved = await hook.save()
389
-
390
- expect({
391
- saved,
392
- calls: recordedCalls,
393
- config: hook.config.value,
394
- }).toEqual({
395
- saved: true,
396
- calls: [
397
- { method: 'GET', url: '/plugins/drp/config' },
398
- {
399
- method: 'PATCH',
400
- url: '/plugins/drp/config',
401
- data: { config: { allowed_experiment_types: ['proteomics'] } },
402
- },
403
- ],
404
- config: {
405
- n: 1,
406
- allowed_experiment_types: ['proteomics'],
407
- },
408
- })
409
- })
410
-
411
- it('should detect nested config edits and restore the saved JSON snapshot', async () => {
412
- getResponse = {
413
- plugin_name: 'drp',
414
- config: {
415
- rules: {
416
- thresholds: [1, 2],
417
- },
418
- },
419
- }
420
- const hook = usePluginConfig('drp')
421
- await hook.load()
422
- const rules = hook.config.value.rules as { thresholds: number[] }
423
- rules.thresholds[0] = 9
424
- await nextTick()
425
-
426
- const dirtyAfterMutation = hook.isDirty.value
427
- hook.reset()
428
-
429
- expect({
430
- dirtyAfterMutation,
431
- config: hook.config.value,
432
- isDirty: hook.isDirty.value,
433
- }).toEqual({
434
- dirtyAfterMutation: true,
435
- config: {
436
- rules: {
437
- thresholds: [1, 2],
438
- },
439
- },
440
- isDirty: false,
441
- })
442
- })
443
-
444
- it('isDirty reflects edits vs last saved snapshot', async () => {
445
- getResponse = { plugin_name: 'drp', config: { k: 'v' } }
446
- const hook = usePluginConfig('drp')
447
- await hook.load()
448
- expect(hook.isDirty.value).toBe(false)
449
-
450
- hook.config.value = { k: 'changed' }
451
- await nextTick()
452
- expect(hook.isDirty.value).toBe(true)
453
-
454
- hook.config.value = { k: 'v' }
455
- await nextTick()
456
- expect(hook.isDirty.value).toBe(false)
457
- })
458
-
459
- it('reset restores saved config and clears error', async () => {
460
- getResponse = { plugin_name: 'drp', config: { k: 'v' } }
461
- const hook = usePluginConfig('drp')
462
- await hook.load()
463
- hook.config.value = { k: 'edited' }
464
- hook.error.value = 'something'
465
-
466
- hook.reset()
467
- expect(hook.config.value).toEqual({ k: 'v' })
468
- expect(hook.error.value).toBeNull()
469
- })
470
-
471
- it('load surfaces network error message', async () => {
472
- getError = new Error('network down')
473
- const hook = usePluginConfig('drp')
474
- await hook.load()
475
- expect(hook.error.value).toBe('network down')
476
- expect(hook.isLoading.value).toBe(false)
477
- expect(hook.lastLoadedAt.value).toBeNull()
478
- })
479
-
480
- it('save returns false and records error on failure', async () => {
481
- patchError = new Error('403')
482
- const hook = usePluginConfig('drp')
483
- await hook.load()
484
- hook.config.value = { x: 1 }
485
- const ok = await hook.save()
486
- expect(ok).toBe(false)
487
- expect(hook.error.value).toBe('403')
488
- expect(hook.lastSavedAt.value).toBeNull()
489
- })
490
-
491
- it('empty plugin name produces a noop load', async () => {
492
- const hook = usePluginConfig('')
493
- await hook.load()
494
- expect(recordedCalls).toEqual([])
495
- expect(hook.error.value).toBeNull()
496
- })
497
- })