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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/README.md +5 -5
  2. package/dist/{ExperimentSelectorModal-DyPAUCLw.js → ExperimentSelectorModal-B6P2msT4.js} +3 -3
  3. package/dist/{ExperimentSelectorModal-DyPAUCLw.js.map → ExperimentSelectorModal-B6P2msT4.js.map} +1 -1
  4. package/dist/{SettingsModal-BgKY93zh.js → SettingsModal-CkNM8qAI.js} +4 -4
  5. package/dist/{SettingsModal-BgKY93zh.js.map → SettingsModal-CkNM8qAI.js.map} +1 -1
  6. package/dist/__tests__/composables/usePluginConfig.test.d.ts +15 -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 +9 -2
  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-DkHddRMl.js → components-MGNL6tRn.js} +16 -10
  16. package/dist/components-MGNL6tRn.js.map +1 -0
  17. package/dist/composables/index.d.ts +3 -2
  18. package/dist/composables/index.js +8 -8
  19. package/dist/composables/useGeneratedAnalysis.d.ts +3 -1
  20. package/dist/composables/useJobsStatusTray.d.ts +25 -1
  21. package/dist/composables/usePluginClient.d.ts +2 -0
  22. package/dist/composables/usePluginConfig.d.ts +25 -0
  23. package/dist/{composables-C_i90sGS.js → composables-CKxOcvgh.js} +140 -5
  24. package/dist/composables-CKxOcvgh.js.map +1 -0
  25. package/dist/{experiment-Cq145oEW.js → experiment-Y9Iz7KYi.js} +2 -2
  26. package/dist/{experiment-Cq145oEW.js.map → experiment-Y9Iz7KYi.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 +6 -0
  30. package/dist/stores/index.js +2 -2
  31. package/dist/templates/index.js +2 -2
  32. package/dist/{templates-DsP51i-S.js → templates-BTJ9C9Kb.js} +2 -2
  33. package/dist/{templates-DsP51i-S.js.map → templates-BTJ9C9Kb.js.map} +1 -1
  34. package/dist/types/auth.d.ts +1 -0
  35. package/dist/{useApi-J6zFkHmn.js → useApi-CDBq6z23.js} +2 -1
  36. package/dist/useApi-CDBq6z23.js.map +1 -0
  37. package/dist/{useControlSchema-BDx2Olha.js → useControlSchema-CXhxTul7.js} +10 -3
  38. package/dist/useControlSchema-CXhxTul7.js.map +1 -0
  39. package/dist/{useExperimentSelector-CV4iNfMS.js → useExperimentSelector-D0pjj9WH.js} +2 -2
  40. package/dist/{useExperimentSelector-CV4iNfMS.js.map → useExperimentSelector-D0pjj9WH.js.map} +1 -1
  41. package/dist/{useFormBuilder-BLxG3BOX.js → useFormBuilder-CuOFB3aP.js} +2 -2
  42. package/dist/{useFormBuilder-BLxG3BOX.js.map → useFormBuilder-CuOFB3aP.js.map} +1 -1
  43. package/dist/{useProtocolTemplates-CS_SQl6U.js → useProtocolTemplates-BWhQIWV5.js} +113 -22
  44. package/dist/useProtocolTemplates-BWhQIWV5.js.map +1 -0
  45. package/package.json +1 -1
  46. package/src/__tests__/composables/useAuth.test.ts +2 -0
  47. package/src/__tests__/composables/usePluginConfig.test.ts +497 -0
  48. package/src/__tests__/docs/frontendDocsCatalog.test.ts +17 -6
  49. package/src/components/JobsStatusTray.vue +16 -1
  50. package/src/composables/index.ts +7 -0
  51. package/src/composables/useAuth.ts +3 -0
  52. package/src/composables/useGeneratedAnalysis.ts +14 -1
  53. package/src/composables/useJobsStatusTray.ts +184 -17
  54. package/src/composables/usePluginClient.ts +5 -0
  55. package/src/composables/usePluginConfig.ts +250 -0
  56. package/src/stores/auth.ts +1 -0
  57. package/src/types/auth.ts +1 -0
  58. package/dist/components-DkHddRMl.js.map +0 -1
  59. package/dist/composables-C_i90sGS.js.map +0 -1
  60. package/dist/useApi-J6zFkHmn.js.map +0 -1
  61. package/dist/useControlSchema-BDx2Olha.js.map +0 -1
  62. package/dist/useProtocolTemplates-CS_SQl6U.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morscherlab/mint-sdk",
3
- "version": "1.2.0-beta.2",
3
+ "version": "1.2.0-beta.4",
4
4
  "description": "MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -57,6 +57,7 @@ describe('useAuth', () => {
57
57
  passkey_enabled: true,
58
58
  passkey_registered: false,
59
59
  registration_enabled: true,
60
+ database_mode: 'postgresql',
60
61
  access: { experiment_visibility_mode: 'restricted' },
61
62
  },
62
63
  })
@@ -67,6 +68,7 @@ describe('useAuth', () => {
67
68
  passkeyEnabled: true,
68
69
  passkeyRegistered: false,
69
70
  registrationEnabled: true,
71
+ databaseMode: 'postgresql',
70
72
  experimentVisibilityMode: 'restricted',
71
73
  }
72
74
 
@@ -0,0 +1,497 @@
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
+ })
@@ -271,11 +271,14 @@ describe('frontend docs catalog coverage', () => {
271
271
  })
272
272
  })
273
273
 
274
- it('documents only the source-driven JobsStatusTray props', () => {
274
+ it('documents the source and MINT 1.1 JobsStatusTray props', () => {
275
275
  const manifest = extractFrontendManifest()
276
276
  const jobsStatusTray = manifest.categories.components.find(item => item.name === 'JobsStatusTray')
277
277
 
278
278
  expect(jobsStatusTray?.props?.map(prop => prop.name).sort()).toEqual([
279
+ 'adapter',
280
+ 'eventStream',
281
+ 'jobs',
279
282
  'loadOnMount',
280
283
  'source',
281
284
  'teleportTo',
@@ -319,6 +322,18 @@ describe('frontend docs catalog coverage', () => {
319
322
  'AppPillNav',
320
323
  'usePluginApi',
321
324
  'UsePluginApiOptions',
325
+ ]
326
+
327
+ for (const name of retiredNames) {
328
+ expect(rootIndex).not.toMatch(new RegExp(`\\b${name}\\b`))
329
+ expect(componentsIndex).not.toMatch(new RegExp(`\\b${name}\\b`))
330
+ expect(composableExports).not.toMatch(new RegExp(`\\b${name}\\b`))
331
+ }
332
+ })
333
+
334
+ it('keeps the MINT 1.1 composable compatibility exports', () => {
335
+ const composablesIndex = readFileSync(join(SDK_ROOT, 'src', 'composables', 'index.ts'), 'utf8')
336
+ const expected = [
322
337
  'useJobsStatusTray',
323
338
  'UseJobsStatusTrayOptions',
324
339
  'UseJobsStatusTrayReturn',
@@ -329,11 +344,7 @@ describe('frontend docs catalog coverage', () => {
329
344
  'usePluginClient',
330
345
  ]
331
346
 
332
- for (const name of retiredNames) {
333
- expect(rootIndex).not.toMatch(new RegExp(`\\b${name}\\b`))
334
- expect(componentsIndex).not.toMatch(new RegExp(`\\b${name}\\b`))
335
- expect(composableExports).not.toMatch(new RegExp(`\\b${name}\\b`))
336
- }
347
+ expect(expected.filter(name => !new RegExp(`\\b${name}\\b`).test(composablesIndex))).toEqual([])
337
348
  })
338
349
 
339
350
  it('documents every public composable/helper export used by docs and doctor', () => {
@@ -15,12 +15,24 @@ import {
15
15
  } from '../jobs'
16
16
  import {
17
17
  usePluginJobCenter,
18
+ type JobsStatusTrayAdapter,
18
19
  type PluginJobCenterSource,
19
20
  } from '../composables/useJobsStatusTray'
20
- import type { JobState } from '../types/jobs'
21
+ import type { UsePluginEventStreamReturn } from '../composables/usePluginClient'
22
+ import type {
23
+ JobState,
24
+ JobStateInput,
25
+ JobStreamData,
26
+ } from '../types/jobs'
21
27
 
22
28
  interface Props {
23
29
  source?: PluginJobCenterSource
30
+ /** @deprecated Pass a source instead. */
31
+ jobs?: readonly JobStateInput[]
32
+ /** @deprecated Pass source actions instead. */
33
+ adapter?: JobsStatusTrayAdapter
34
+ /** @deprecated Event streams belong to the source runtime. */
35
+ eventStream?: UsePluginEventStreamReturn<JobStreamData>
24
36
  loadOnMount?: boolean
25
37
  title?: string
26
38
  teleportTo?: string | HTMLElement | false
@@ -49,6 +61,9 @@ const panelRef = ref<HTMLElement | null>(null)
49
61
  const teleportTarget = computed(() => props.teleportTo || 'body')
50
62
  const tray = usePluginJobCenter({
51
63
  source: toRef(props, 'source'),
64
+ jobs: toRef(props, 'jobs'),
65
+ adapter: toRef(props, 'adapter'),
66
+ eventStream: toRef(props, 'eventStream'),
52
67
  loadOnMount: props.loadOnMount,
53
68
  })
54
69
  const canClear = computed(() => (
@@ -153,6 +153,7 @@ export {
153
153
  type UseGeneratedAnalysisReturn,
154
154
  } from './useGeneratedAnalysis'
155
155
  export { useAutoGroup, DEFAULT_COLORS, extractSamplesFromDesignData } from './useAutoGroup'
156
+ export { usePluginConfig, type UsePluginConfigReturn } from './usePluginConfig'
156
157
  export {
157
158
  useReagentSeries,
158
159
  generateDilutionSeries,
@@ -487,6 +488,7 @@ export {
487
488
  pluginFormDataFromPayload,
488
489
  resolvePluginBaseUrl,
489
490
  uploadPluginEndpoint,
491
+ usePluginClient,
490
492
  usePluginEventStream,
491
493
  usePluginSettings,
492
494
  useCurrentExperiment,
@@ -515,9 +517,14 @@ export {
515
517
  export {
516
518
  combinePluginJobSources,
517
519
  usePluginJobCenter,
520
+ useJobsStatusTray,
518
521
  type CombinedPluginJobCenterSource,
522
+ type JobsStatusTrayAdapter,
523
+ type JobStreamMessage,
519
524
  type PluginJobCenterSource,
520
525
  type JobsStatusTrayTab,
526
+ type UseJobsStatusTrayOptions,
527
+ type UseJobsStatusTrayReturn,
521
528
  type UsePluginJobCenterOptions,
522
529
  type UsePluginJobCenterReturn,
523
530
  } from './useJobsStatusTray'
@@ -93,6 +93,7 @@ export function useAuth(): UseAuthReturn {
93
93
  passkey_enabled: boolean
94
94
  passkey_registered?: boolean
95
95
  registration_enabled?: boolean
96
+ database_mode?: string
96
97
  access?: {
97
98
  experiment_visibility_mode?: 'open' | 'restricted'
98
99
  experimentVisibilityMode?: 'open' | 'restricted'
@@ -104,6 +105,7 @@ export function useAuth(): UseAuthReturn {
104
105
  passkeyEnabled: response.data.passkey_enabled,
105
106
  passkeyRegistered: response.data.passkey_registered ?? false,
106
107
  registrationEnabled: response.data.registration_enabled ?? false,
108
+ databaseMode: response.data.database_mode ?? 'none',
107
109
  experimentVisibilityMode:
108
110
  response.data.access?.experiment_visibility_mode
109
111
  ?? response.data.access?.experimentVisibilityMode
@@ -119,6 +121,7 @@ export function useAuth(): UseAuthReturn {
119
121
  passkeyEnabled: false,
120
122
  passkeyRegistered: false,
121
123
  registrationEnabled: false,
124
+ databaseMode: 'none',
122
125
  experimentVisibilityMode: 'open',
123
126
  }
124
127
  }
@@ -8,7 +8,10 @@ import {
8
8
  type ShallowRef,
9
9
  } from 'vue'
10
10
 
11
- import type { PluginJobCenterSource } from './useJobsStatusTray'
11
+ import type {
12
+ JobsStatusTrayAdapter,
13
+ PluginJobCenterSource,
14
+ } from './useJobsStatusTray'
12
15
  import { useEventStream, type EventStreamMessage } from './useEventStream'
13
16
  import { normalizeJobState } from '../jobs'
14
17
  import type {
@@ -102,6 +105,8 @@ export interface UsePluginJobsReturn extends PluginJobCenterSource {
102
105
  pause: () => void
103
106
  resume: () => void
104
107
  dispose: () => void
108
+ /** @deprecated Pass the runtime itself to SDK job UI as its source. */
109
+ jobsAdapter: JobsStatusTrayAdapter
105
110
  }
106
111
 
107
112
  export type UseGeneratedAnalysisReturn = UsePluginJobsReturn
@@ -972,6 +977,13 @@ function createPluginJobs(
972
977
  return payload.deleted
973
978
  }
974
979
 
980
+ const jobsAdapter: JobsStatusTrayAdapter = {
981
+ listJobs: async () => ({ jobs: await refreshJobs() }),
982
+ cancelJob: async job => cancelJob(job as GeneratedAnalysisJob),
983
+ deleteJob: async job => deleteJob(job as GeneratedAnalysisJob),
984
+ clearFinishedJobs: async () => clearFinishedJobs(),
985
+ }
986
+
975
987
  function pause(): void {
976
988
  if (disposed || paused) return
977
989
  paused = true
@@ -1065,6 +1077,7 @@ function createPluginJobs(
1065
1077
  pause,
1066
1078
  resume,
1067
1079
  dispose,
1080
+ jobsAdapter,
1068
1081
  }
1069
1082
  }
1070
1083