@morscherlab/mint-sdk 1.0.63 → 1.1.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 (165) hide show
  1. package/README.md +101 -2
  2. package/dist/EmptyState-D1c4RdC7.js +638 -0
  3. package/dist/EmptyState-D1c4RdC7.js.map +1 -0
  4. package/dist/{ExperimentPopover-BbPkIFsI.js → ExperimentPopover-BFw3QX8L.js} +10 -9
  5. package/dist/{ExperimentPopover-BbPkIFsI.js.map → ExperimentPopover-BFw3QX8L.js.map} +1 -1
  6. package/dist/{ExperimentSelectorModal-DZeSd-yr.js → ExperimentSelectorModal-B7w6k3Y8.js} +12 -216
  7. package/dist/ExperimentSelectorModal-B7w6k3Y8.js.map +1 -0
  8. package/dist/{SettingsModal-CJgLadsN.js → SettingsModal-DPDwCbXK.js} +12 -9
  9. package/dist/{SettingsModal-CJgLadsN.js.map → SettingsModal-DPDwCbXK.js.map} +1 -1
  10. package/dist/__tests__/components/GeneratedPlotlyResult.test.d.ts +1 -0
  11. package/dist/__tests__/components/GeneratedPluginView.test.d.ts +1 -0
  12. package/dist/__tests__/components/GeneratedResultRenderer.test.d.ts +1 -0
  13. package/dist/__tests__/components/InstrumentAlertLog.test.d.ts +1 -0
  14. package/dist/__tests__/composables/useGeneratedAnalysis.test.d.ts +1 -0
  15. package/dist/__tests__/generatedUi.test.d.ts +1 -0
  16. package/dist/{analysisArtifactTypes-Cu40dzSG.js → analysisArtifactTypes-ejqR8MX3.js} +1 -1
  17. package/dist/analysisArtifactTypes-ejqR8MX3.js.map +1 -0
  18. package/dist/{auth-DZAwEeis.js → auth-B87xM--s.js} +1 -1
  19. package/dist/{auth-DZAwEeis.js.map → auth-B87xM--s.js.map} +1 -1
  20. package/dist/chunk-BoAXSpZd.js +33 -0
  21. package/dist/components/AppSidebar.vue.d.ts +1 -1
  22. package/dist/components/AutoGroupModal.vue.d.ts +2 -2
  23. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  24. package/dist/components/CollapsibleCard.vue.d.ts +1 -1
  25. package/dist/components/ComponentBindingRenderer.vue.d.ts +1 -1
  26. package/dist/components/ConfirmDialog.vue.d.ts +1 -1
  27. package/dist/components/ExperimentDataViewer.vue.d.ts +1 -1
  28. package/dist/components/ExperimentTimeline.vue.d.ts +1 -1
  29. package/dist/components/FileUploader.vue.d.ts +4 -2
  30. package/dist/components/FitPanel.vue.d.ts +1 -1
  31. package/dist/components/FormActions.vue.d.ts +1 -1
  32. package/dist/components/FormBuilder.vue.d.ts +1 -1
  33. package/dist/components/GeneratedPathInput.vue.d.ts +12 -0
  34. package/dist/components/GeneratedPlotlyResult.vue.d.ts +11 -0
  35. package/dist/components/GeneratedPluginView.vue.d.ts +10 -0
  36. package/dist/components/GeneratedResultRenderer.vue.d.ts +6 -0
  37. package/dist/components/JobsStatusTray.vue.d.ts +5 -1
  38. package/dist/components/LcmsSequenceTable.vue.d.ts +1 -1
  39. package/dist/components/SampleLegend.vue.d.ts +1 -1
  40. package/dist/components/SampleSelector.vue.d.ts +2 -2
  41. package/dist/components/SettingsModal.vue.d.ts +1 -1
  42. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +2 -2
  43. package/dist/components/index.d.ts +4 -0
  44. package/dist/components/index.js +8 -9
  45. package/dist/components/internal/PlateMapEditorToolbarInternal.vue.d.ts +2 -2
  46. package/dist/{components-DdzHh3a_.js → components-UfkPN1bK.js} +2255 -2303
  47. package/dist/components-UfkPN1bK.js.map +1 -0
  48. package/dist/composables/index.d.ts +2 -3
  49. package/dist/composables/index.js +10 -11
  50. package/dist/composables/platformContextHelpers.d.ts +0 -2
  51. package/dist/composables/useAnalysisArtifacts.d.ts +1 -3
  52. package/dist/composables/useGeneratedAnalysis.d.ts +62 -0
  53. package/dist/composables/useJobsStatusTray.d.ts +48 -4
  54. package/dist/composables/usePlatformContext.d.ts +3 -0
  55. package/dist/composables/usePluginClient.d.ts +6 -3
  56. package/dist/{composables-BUUphBqg.js → composables-B512bWJP.js} +155 -74
  57. package/dist/composables-B512bWJP.js.map +1 -0
  58. package/dist/experiment-utils-CxkruZ3G.js +392 -0
  59. package/dist/experiment-utils-CxkruZ3G.js.map +1 -0
  60. package/dist/generated/schema.d.ts +7 -0
  61. package/dist/generated-ui/main.d.ts +0 -0
  62. package/dist/index.d.ts +1 -0
  63. package/dist/index.js +15 -18
  64. package/dist/index.js.map +1 -1
  65. package/dist/install.js +7 -8
  66. package/dist/install.js.map +1 -1
  67. package/dist/plotly.min-BR53hja4.js +180634 -0
  68. package/dist/plotly.min-BR53hja4.js.map +1 -0
  69. package/dist/stores/auth.d.ts +2 -2
  70. package/dist/stores/index.js +1 -1
  71. package/dist/styles.css +10543 -10404
  72. package/dist/templates/index.js +4 -3
  73. package/dist/templates/packs.d.ts +0 -1
  74. package/dist/{templates-CNbPQNID.js → templates-Cttbjbyc.js} +2 -4
  75. package/dist/templates-Cttbjbyc.js.map +1 -0
  76. package/dist/types/analysisArtifactTypes.d.ts +0 -8
  77. package/dist/types/form-builder.d.ts +1 -1
  78. package/dist/types/generated-ui.d.ts +170 -0
  79. package/dist/types/index.d.ts +2 -1
  80. package/dist/types/index.js +1 -1
  81. package/dist/types/instrument.d.ts +1 -0
  82. package/dist/types/jobs.d.ts +7 -1
  83. package/dist/types/platform.d.ts +2 -0
  84. package/dist/{useControlSchema-BZNdalmL.js → useControlSchema-Dallso-L.js} +1852 -171
  85. package/dist/useControlSchema-Dallso-L.js.map +1 -0
  86. package/dist/{useDropdownState-Bb-c_PeE.js → useDropdownState-gVaMXtwW.js} +2 -2
  87. package/dist/{useDropdownState-Bb-c_PeE.js.map → useDropdownState-gVaMXtwW.js.map} +1 -1
  88. package/dist/{useExperimentSelector-DRbj6vse.js → useExperimentSelector-BMjzNcf2.js} +4 -255
  89. package/dist/useExperimentSelector-BMjzNcf2.js.map +1 -0
  90. package/dist/{useFormBuilder-DKekvXRs.js → useFormBuilder-BXaaDI41.js} +2 -2
  91. package/dist/{useFormBuilder-DKekvXRs.js.map → useFormBuilder-BXaaDI41.js.map} +1 -1
  92. package/dist/{useAnalysisArtifacts-C3l0a0vu.js → useProtocolTemplates-BP7_4RGQ.js} +307 -475
  93. package/dist/useProtocolTemplates-BP7_4RGQ.js.map +1 -0
  94. package/package.json +7 -3
  95. package/src/__tests__/components/GeneratedPathInput.test.ts +132 -0
  96. package/src/__tests__/components/GeneratedPlotlyResult.test.ts +117 -0
  97. package/src/__tests__/components/GeneratedPluginView.test.ts +230 -0
  98. package/src/__tests__/components/GeneratedResultRenderer.test.ts +93 -0
  99. package/src/__tests__/components/InstrumentAlertLog.test.ts +59 -0
  100. package/src/__tests__/composables/formBuilderRegistry.test.ts +5 -1
  101. package/src/__tests__/composables/useAnalysisArtifacts.test.ts +1 -74
  102. package/src/__tests__/composables/useApi.test.ts +8 -0
  103. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +1740 -0
  104. package/src/__tests__/composables/useJobsStatusTray.test.ts +179 -1
  105. package/src/__tests__/composables/usePlatformContext.test.ts +28 -0
  106. package/src/__tests__/composables/usePluginClient.test.ts +366 -20
  107. package/src/__tests__/composables/usePluginConfig.test.ts +323 -7
  108. package/src/__tests__/docs/extractDocsTemplates.test.ts +0 -1
  109. package/src/__tests__/generatedUi.test.ts +119 -0
  110. package/src/__tests__/templates/templates.test.ts +1 -1
  111. package/src/components/FileUploader.vue +49 -11
  112. package/src/components/GeneratedPathInput.vue +218 -0
  113. package/src/components/GeneratedPlotlyResult.vue +87 -0
  114. package/src/components/GeneratedPluginView.vue +279 -0
  115. package/src/components/GeneratedResultRenderer.vue +137 -0
  116. package/src/components/InstrumentAlertLog.vue +2 -2
  117. package/src/components/JobsStatusTray.vue +14 -8
  118. package/src/components/index.ts +4 -0
  119. package/src/composables/formBuilderRegistry.ts +2 -0
  120. package/src/composables/index.ts +18 -5
  121. package/src/composables/platformContextHelpers.ts +3 -10
  122. package/src/composables/useAnalysisArtifacts.ts +0 -48
  123. package/src/composables/useApi.ts +1 -0
  124. package/src/composables/useGeneratedAnalysis.ts +1252 -0
  125. package/src/composables/useJobsStatusTray.ts +234 -45
  126. package/src/composables/usePluginClient.ts +152 -25
  127. package/src/composables/usePluginConfig.ts +156 -20
  128. package/src/generated/schema.ts +130 -0
  129. package/src/generated-ui/main.ts +21 -0
  130. package/src/index.ts +3 -0
  131. package/src/jobs.ts +5 -0
  132. package/src/plotly-dist.d.ts +3 -0
  133. package/src/templates/packs.ts +0 -3
  134. package/src/types/analysisArtifactTypes.ts +0 -9
  135. package/src/types/form-builder.ts +1 -0
  136. package/src/types/generated-ui.ts +176 -0
  137. package/src/types/index.ts +2 -1
  138. package/src/types/instrument.ts +1 -0
  139. package/src/types/jobs.ts +10 -1
  140. package/src/types/platform.ts +2 -0
  141. package/dist/BaseButton-Dgqrze41.js +0 -74
  142. package/dist/BaseButton-Dgqrze41.js.map +0 -1
  143. package/dist/BaseModal-B9UA8Y_I.js +0 -165
  144. package/dist/BaseModal-B9UA8Y_I.js.map +0 -1
  145. package/dist/BaseSelect-ekgr9fDo.js +0 -179
  146. package/dist/BaseSelect-ekgr9fDo.js.map +0 -1
  147. package/dist/ExperimentPopover-Ryusjrhv.js +0 -4
  148. package/dist/ExperimentSelectorModal-Cg4-LXBD.js +0 -5
  149. package/dist/ExperimentSelectorModal-DZeSd-yr.js.map +0 -1
  150. package/dist/SettingsModal-Bl_9w2Y3.js +0 -5
  151. package/dist/analysisArtifactTypes-Cu40dzSG.js.map +0 -1
  152. package/dist/components-DdzHh3a_.js.map +0 -1
  153. package/dist/composables/analysisArtifactTarget.d.ts +0 -5
  154. package/dist/composables-BUUphBqg.js.map +0 -1
  155. package/dist/experiment-utils-Bfa7CwPU.js +0 -108
  156. package/dist/experiment-utils-Bfa7CwPU.js.map +0 -1
  157. package/dist/templates-CNbPQNID.js.map +0 -1
  158. package/dist/useAnalysisArtifacts-C3l0a0vu.js.map +0 -1
  159. package/dist/useControlSchema-BZNdalmL.js.map +0 -1
  160. package/dist/useEventListener-CfVkP9Xz.js +0 -57
  161. package/dist/useEventListener-CfVkP9Xz.js.map +0 -1
  162. package/dist/useExperimentSelector-DRbj6vse.js.map +0 -1
  163. package/src/__tests__/composables/analysisArtifactTarget.test.ts +0 -77
  164. package/src/composables/analysisArtifactTarget.ts +0 -53
  165. /package/dist/__tests__/{composables/analysisArtifactTarget.test.d.ts → components/GeneratedPathInput.test.d.ts} +0 -0
@@ -28,7 +28,13 @@ import {
28
28
  pluginEndpointRequestParts,
29
29
  requestBodyFromPayload,
30
30
  } from './pluginEndpointBuilder'
31
- import type { ExperimentSummary, PageSelectorItem, SettingsModalSchema, TopBarSettingsConfig } from '../types'
31
+ import type {
32
+ ExperimentSummary,
33
+ GeneratedJobDefinition,
34
+ PageSelectorItem,
35
+ SettingsModalSchema,
36
+ TopBarSettingsConfig,
37
+ } from '../types'
32
38
 
33
39
  export {
34
40
  buildPluginEndpointUrl,
@@ -41,6 +47,7 @@ export interface PluginEndpointContract {
41
47
  name: string
42
48
  method: PluginHttpMethod
43
49
  path: string
50
+ requiresAuth?: boolean
44
51
  pathParams?: PluginEndpointParamContract[]
45
52
  queryParams?: PluginEndpointParamContract[]
46
53
  requestType?: string | null
@@ -90,12 +97,14 @@ export interface PluginContract {
90
97
  schema?: SettingsModalSchema | null
91
98
  }
92
99
  endpoints: PluginEndpointContract[]
100
+ jobs?: GeneratedJobDefinition[]
93
101
  hash: string
94
102
  }
95
103
 
96
104
  export interface PluginEndpointDefinition {
97
105
  method: PluginHttpMethod
98
106
  path: string
107
+ requiresAuth?: boolean
99
108
  pathParams?: PluginEndpointParamDefinition[]
100
109
  queryParams?: PluginEndpointParamDefinition[]
101
110
  hasBody?: boolean
@@ -157,7 +166,7 @@ export interface UsePluginSettingsOptions {
157
166
  apiBaseUrl?: string
158
167
  /** Platform API base URL for /plugins/{name}/config. Defaults to the injected platform API URL. */
159
168
  platformApiBaseUrl?: string
160
- /** SettingsModal schema generated from the backend settings_model. */
169
+ /** SettingsModal schema generated from the backend plugin config model. */
161
170
  schema?: SettingsModalSchema | null
162
171
  /** SettingsModal title when using settingsConfig. */
163
172
  title?: string
@@ -383,7 +392,7 @@ function pluginPageIdFromPath(path: string, fallbackIndex: number): string {
383
392
  return normalizedPath.replace(/^\/+/, '').replace(/\/+/g, '-') || `page-${fallbackIndex + 1}`
384
393
  }
385
394
 
386
- /** Convert PluginMetadata.nav_items into AppTopBar pageSelector items for topbar page switches and homepage PluginCards. */
395
+ /** Convert resolved @mint_plugin nav_items into page selectors for the topbar and homepage PluginCards. */
387
396
  export function getPluginPageSelectorItems(contract: PluginContract): PageSelectorItem[] {
388
397
  const pluginIcon = contract.plugin.icon ?? ''
389
398
  const navItems = contract.plugin.navItems ?? []
@@ -413,7 +422,10 @@ export function createPluginClient<TClient extends object = GeneratedPluginClien
413
422
  ? explicitOptions
414
423
  : (payloadOrOptions as PluginClientRequestOptions | undefined) ?? explicitOptions
415
424
  const parts = pluginEndpointRequestParts(contract, endpoint, payload, options.baseUrl)
416
- const api = useApi({ baseUrl: parts.baseUrl })
425
+ const api = useApi({
426
+ baseUrl: parts.baseUrl,
427
+ withAuth: endpoint.requiresAuth !== false,
428
+ })
417
429
  const config = endpointRequestConfig(endpoint, parts.query, callOptions)
418
430
 
419
431
  if (endpoint.method === 'get') {
@@ -456,7 +468,10 @@ export async function uploadPluginEndpoint<TResponse = unknown>(
456
468
  }
457
469
 
458
470
  const parts = pluginEndpointRequestParts(contract, endpoint, payload, options.baseUrl)
459
- const api = useApi({ baseUrl: parts.baseUrl })
471
+ const api = useApi({
472
+ baseUrl: parts.baseUrl,
473
+ withAuth: endpoint.requiresAuth !== false,
474
+ })
460
475
  const body = endpoint.hasBody
461
476
  ? requestBodyFromPayload(payload, parts.requestPayload, endpoint)
462
477
  : payload
@@ -491,7 +506,10 @@ export async function downloadPluginEndpoint(
491
506
  }
492
507
 
493
508
  const parts = pluginEndpointRequestParts(contract, endpoint, payload, options.baseUrl)
494
- const api = useApi({ baseUrl: parts.baseUrl })
509
+ const api = useApi({
510
+ baseUrl: parts.baseUrl,
511
+ withAuth: endpoint.requiresAuth !== false,
512
+ })
495
513
  const config = {
496
514
  ...(endpointRequestConfig(endpoint, parts.query, { signal: options.signal }) ?? {}),
497
515
  responseType: 'blob' as const,
@@ -528,8 +546,14 @@ function normalizePluginSettingsOptions(options: UsePluginSettingsOptions | stri
528
546
  return typeof options === 'string' ? { pluginName: options } : options
529
547
  }
530
548
 
549
+ const PLATFORM_OWNED_SETTINGS_KEY = 'allowed_experiment_types'
550
+
531
551
  function cloneRecord(value: Record<string, unknown>): Record<string, unknown> {
532
- return { ...value }
552
+ return JSON.parse(JSON.stringify(value)) as Record<string, unknown>
553
+ }
554
+
555
+ function hasOwnSetting(settings: Record<string, unknown>, key: string): boolean {
556
+ return Object.prototype.hasOwnProperty.call(settings, key)
533
557
  }
534
558
 
535
559
  function responseSettingsPayload(response: unknown): Record<string, unknown> {
@@ -547,6 +571,43 @@ function responseSettingsPayload(response: unknown): Record<string, unknown> {
547
571
  return {}
548
572
  }
549
573
 
574
+ function responseSettingsRevision(response: unknown): string | null {
575
+ if (!response || typeof response !== 'object') return null
576
+ const revision = (response as { revision?: unknown }).revision
577
+ return typeof revision === 'string' && revision ? revision : null
578
+ }
579
+
580
+ function topLevelSettingsPatch(
581
+ current: Record<string, unknown>,
582
+ keys: Iterable<string>,
583
+ ): Record<string, unknown> {
584
+ return Object.fromEntries(
585
+ [...keys].map(key => [
586
+ key,
587
+ hasOwnSetting(current, key) ? current[key] : null,
588
+ ]),
589
+ )
590
+ }
591
+
592
+ function changedTopLevelSettingKeys(
593
+ current: Record<string, unknown>,
594
+ saved: Record<string, unknown>,
595
+ ): Set<string> {
596
+ return new Set(
597
+ [...new Set([...Object.keys(current), ...Object.keys(saved)])].filter(
598
+ key => JSON.stringify(current[key]) !== JSON.stringify(saved[key]),
599
+ ),
600
+ )
601
+ }
602
+
603
+ function platformOwnedSettingsPatch(settings: Record<string, unknown>): Record<string, unknown> {
604
+ return {
605
+ [PLATFORM_OWNED_SETTINGS_KEY]: hasOwnSetting(settings, PLATFORM_OWNED_SETTINGS_KEY)
606
+ ? settings[PLATFORM_OWNED_SETTINGS_KEY]
607
+ : null,
608
+ }
609
+ }
610
+
550
611
  /** Load, edit, and persist plugin settings from platform config or the plugin-local settings router. */
551
612
  export function usePluginSettings<TSettings = Record<string, unknown>>(
552
613
  options: UsePluginSettingsOptions | string = {},
@@ -557,8 +618,9 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
557
618
  const platformApi = useApi({ baseUrl: resolvedOptions.platformApiBaseUrl ?? injectedContext?.platformApiUrl })
558
619
  const pluginApi = useApi({ baseUrl: resolvedOptions.apiBaseUrl ?? injectedContext?.plugin?.api_prefix })
559
620
 
560
- const values = shallowRef<PluginSettingsValues<TSettings>>({} as PluginSettingsValues<TSettings>)
621
+ const values = ref({}) as Ref<PluginSettingsValues<TSettings>>
561
622
  const savedValues = shallowRef<Record<string, unknown>>({})
623
+ const revision = ref<string | null>(null)
562
624
  const request = useRequestSyncState('Plugin settings request failed.')
563
625
  const isLoading = ref(false)
564
626
  const isSaving = ref(false)
@@ -566,8 +628,17 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
566
628
  const resolvedPluginName = computed(() =>
567
629
  resolvedOptions.pluginName ?? plugin.value?.name ?? injectedContext?.plugin?.name ?? '',
568
630
  )
631
+ const usesPluginSettingsApi = computed(() => {
632
+ const currentPlugin = injectedContext?.plugin ?? plugin.value
633
+ return currentPlugin?.settings_api === 'plugin'
634
+ && resolvedPluginName.value === currentPlugin.name
635
+ })
569
636
  const usesPlatformConfig = computed(() =>
570
- Boolean((isIntegrated.value || injectedContext?.isIntegrated) && resolvedPluginName.value),
637
+ Boolean(
638
+ (isIntegrated.value || injectedContext?.isIntegrated)
639
+ && resolvedPluginName.value
640
+ && !usesPluginSettingsApi.value,
641
+ ),
571
642
  )
572
643
  const settings = computed(() => values.value)
573
644
  const isDirty = computed(() => JSON.stringify(values.value) !== JSON.stringify(savedValues.value))
@@ -582,10 +653,11 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
582
653
  values.value = cloneRecord(nextValues) as PluginSettingsValues<TSettings>
583
654
  }
584
655
 
585
- function applyLoaded(nextValues: Record<string, unknown>): void {
656
+ function applyLoaded(nextValues: Record<string, unknown>, nextRevision: string | null): void {
586
657
  const cloned = cloneRecord(nextValues)
587
658
  values.value = cloned as PluginSettingsValues<TSettings>
588
659
  savedValues.value = cloneRecord(cloned)
660
+ revision.value = nextRevision
589
661
  }
590
662
 
591
663
  async function load(): Promise<void> {
@@ -602,7 +674,10 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
602
674
  () => pluginApi.get<{ settings: Record<string, unknown>; mode?: string }>('/settings'),
603
675
  { success: 'load', errorMessage: 'Failed to load plugin settings' },
604
676
  )
605
- applyLoaded(responseSettingsPayload(response))
677
+ applyLoaded(
678
+ responseSettingsPayload(response),
679
+ responseSettingsRevision(response),
680
+ )
606
681
  } catch {
607
682
  // Error state is handled by request.run().
608
683
  } finally {
@@ -618,19 +693,65 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
618
693
  isSaving.value = true
619
694
  try {
620
695
  const payload = cloneRecord(values.value)
621
- const response = usesPlatformConfig.value
622
- ? await request.run(
623
- () => platformApi.patch<{ plugin_name: string; config: Record<string, unknown> }>(
624
- `/plugins/${encodeURIComponent(resolvedPluginName.value)}/config`,
625
- { config: payload },
626
- ),
627
- { success: 'save', errorMessage: 'Failed to save plugin settings' },
696
+ let response: unknown
697
+ if (usesPlatformConfig.value) {
698
+ const dirtyKeys = changedTopLevelSettingKeys(payload, savedValues.value)
699
+ const platformOwnedChanged = dirtyKeys.has(PLATFORM_OWNED_SETTINGS_KEY)
700
+ const typedKeys = [...dirtyKeys].filter(
701
+ key => key !== PLATFORM_OWNED_SETTINGS_KEY,
628
702
  )
629
- : await request.run(
630
- () => pluginApi.put<{ settings: Record<string, unknown>; mode?: string }>('/settings', payload),
703
+ let nextRevision = revision.value
704
+
705
+ if (typedKeys.length) {
706
+ response = await request.run(
707
+ () => platformApi.patch<{
708
+ plugin_name: string
709
+ config: Record<string, unknown>
710
+ revision?: string
711
+ }>(
712
+ `/plugins/${encodeURIComponent(resolvedPluginName.value)}/config`,
713
+ { config: topLevelSettingsPatch(payload, typedKeys) },
714
+ ),
715
+ { success: 'save', errorMessage: 'Failed to save plugin settings' },
716
+ )
717
+ nextRevision = responseSettingsRevision(response) ?? nextRevision
718
+ }
719
+
720
+ if (platformOwnedChanged) {
721
+ response = await request.run(
722
+ () => platformApi.patch<{
723
+ plugin_name: string
724
+ config: Record<string, unknown>
725
+ revision?: string
726
+ }>(
727
+ `/plugins/${encodeURIComponent(resolvedPluginName.value)}/config`,
728
+ { config: platformOwnedSettingsPatch(payload) },
729
+ ),
730
+ { success: 'save', errorMessage: 'Failed to save plugin settings' },
731
+ )
732
+ nextRevision = responseSettingsRevision(response) ?? nextRevision
733
+ }
734
+
735
+ if (!response) return true
736
+ applyLoaded(responseSettingsPayload(response), nextRevision)
737
+ return true
738
+ } else {
739
+ const ifMatch = revision.value
740
+ ? { headers: { 'If-Match': `"${revision.value}"` } }
741
+ : undefined
742
+ response = await request.run(
743
+ () => pluginApi.put<{
744
+ settings: Record<string, unknown>
745
+ mode?: string
746
+ revision?: string
747
+ }>('/settings', payload, ifMatch),
631
748
  { success: 'save', errorMessage: 'Failed to save plugin settings' },
632
749
  )
633
- applyLoaded(responseSettingsPayload(response))
750
+ }
751
+ applyLoaded(
752
+ responseSettingsPayload(response),
753
+ responseSettingsRevision(response) ?? revision.value,
754
+ )
634
755
  return true
635
756
  } catch {
636
757
  return false
@@ -668,7 +789,10 @@ export function usePluginSettings<TSettings = Record<string, unknown>>(
668
789
  }
669
790
  }
670
791
 
671
- function streamRequestAuth(options: PluginEventStreamOptions<unknown>): {
792
+ function streamRequestAuth(
793
+ options: PluginEventStreamOptions<unknown>,
794
+ policy: { requiresAuth: boolean },
795
+ ): {
672
796
  headers: Headers
673
797
  token?: string
674
798
  } {
@@ -677,11 +801,11 @@ function streamRequestAuth(options: PluginEventStreamOptions<unknown>): {
677
801
  if (!authStore.isInitialized) {
678
802
  authStore.initialize()
679
803
  }
680
- if (authStore.token && !headers.has('Authorization')) {
804
+ if (policy.requiresAuth && authStore.token && !headers.has('Authorization')) {
681
805
  headers.set('Authorization', `Bearer ${authStore.token}`)
682
806
  }
683
807
  headers.set('Accept', 'text/event-stream')
684
- const token = authStore.token
808
+ const token = policy.requiresAuth ? authStore.token : undefined
685
809
  return {
686
810
  headers,
687
811
  token: token && headers.get('Authorization') === `Bearer ${token}` ? token : undefined,
@@ -832,7 +956,10 @@ export function usePluginEventStream<TData = string>(
832
956
  const url = buildPluginEndpointUrl(contract, endpoint, payload, {
833
957
  baseUrl: options.baseUrl,
834
958
  })
835
- const requestAuth = streamRequestAuth(options as PluginEventStreamOptions<unknown>)
959
+ const requestAuth = streamRequestAuth(
960
+ options as PluginEventStreamOptions<unknown>,
961
+ { requiresAuth: endpoint.requiresAuth !== false },
962
+ )
836
963
  const response = await fetch(url, {
837
964
  method: 'GET',
838
965
  headers: requestAuth.headers,
@@ -1,8 +1,57 @@
1
1
  import { ref, computed, onMounted, type Ref, type ComputedRef } from 'vue'
2
2
  import { useApi } from './useApi'
3
3
  import { usePlatformContext } from './usePlatformContext'
4
+ import { getInjectedPlatformContext } from './platformContextHelpers'
4
5
  import { useRequestSyncState } from './useRequestSyncState'
5
6
 
7
+ const PLATFORM_OWNED_SETTINGS_KEY = 'allowed_experiment_types'
8
+
9
+ function cloneConfig(config: Record<string, unknown>): Record<string, unknown> {
10
+ return JSON.parse(JSON.stringify(config)) as Record<string, unknown>
11
+ }
12
+
13
+ function hasOwnConfig(config: Record<string, unknown>, key: string): boolean {
14
+ return Object.prototype.hasOwnProperty.call(config, key)
15
+ }
16
+
17
+ function changedTopLevelKeys(
18
+ current: Record<string, unknown>,
19
+ saved: Record<string, unknown>,
20
+ ): Set<string> {
21
+ return new Set(
22
+ [...new Set([...Object.keys(current), ...Object.keys(saved)])].filter(
23
+ key => JSON.stringify(current[key]) !== JSON.stringify(saved[key]),
24
+ ),
25
+ )
26
+ }
27
+
28
+ function topLevelConfigPatch(
29
+ current: Record<string, unknown>,
30
+ keys: Iterable<string>,
31
+ ): Record<string, unknown> {
32
+ return Object.fromEntries(
33
+ [...keys].map(key => [
34
+ key,
35
+ hasOwnConfig(current, key) ? current[key] : null,
36
+ ]),
37
+ )
38
+ }
39
+
40
+ function platformOwnedConfigPatch(config: Record<string, unknown>): Record<string, unknown> {
41
+ return {
42
+ [PLATFORM_OWNED_SETTINGS_KEY]: hasOwnConfig(config, PLATFORM_OWNED_SETTINGS_KEY)
43
+ ? config[PLATFORM_OWNED_SETTINGS_KEY]
44
+ : null,
45
+ }
46
+ }
47
+
48
+ function responseConfigPayload(response: {
49
+ config?: Record<string, unknown>
50
+ settings?: Record<string, unknown>
51
+ }): Record<string, unknown> {
52
+ return response.config ?? response.settings ?? {}
53
+ }
54
+
6
55
  export interface UsePluginConfigReturn {
7
56
  /** Editable plugin configuration values. */
8
57
  config: Ref<Record<string, unknown>>
@@ -28,13 +77,25 @@ export interface UsePluginConfigReturn {
28
77
 
29
78
  /** Loads, saves, and tracks dirty state for a plugin's persistent configuration via the platform API. */
30
79
  export function usePluginConfig(pluginName?: string): UsePluginConfigReturn {
31
- const api = useApi()
80
+ const injectedContext = getInjectedPlatformContext()
32
81
  const { plugin } = usePlatformContext()
82
+ const platformApi = useApi()
83
+ const pluginApi = useApi({
84
+ baseUrl: injectedContext?.plugin?.api_prefix ?? plugin.value?.api_prefix,
85
+ })
33
86
 
34
- const resolvedName = computed(() => pluginName ?? plugin.value?.name ?? '')
87
+ const resolvedName = computed(
88
+ () => pluginName ?? plugin.value?.name ?? injectedContext?.plugin?.name ?? '',
89
+ )
90
+ const usesPluginSettingsApi = computed(() => {
91
+ const currentPlugin = injectedContext?.plugin ?? plugin.value
92
+ return currentPlugin?.settings_api === 'plugin'
93
+ && resolvedName.value === currentPlugin.name
94
+ })
35
95
 
36
96
  const config = ref<Record<string, unknown>>({})
37
97
  const savedConfig = ref<Record<string, unknown>>({})
98
+ const revision = ref<string | null>(null)
38
99
  const request = useRequestSyncState('Plugin config request failed.')
39
100
  const isLoading = ref(false)
40
101
  const isSaving = ref(false)
@@ -52,14 +113,29 @@ export function usePluginConfig(pluginName?: string): UsePluginConfigReturn {
52
113
 
53
114
  isLoading.value = true
54
115
  try {
55
- const response = await request.run(
56
- () => api.get<{ plugin_name: string; config: Record<string, unknown> }>(
57
- `/plugins/${encodeURIComponent(name)}/config`,
58
- ),
59
- { success: 'load', errorMessage: 'Failed to load plugin config' },
60
- )
61
- config.value = { ...response.config }
62
- savedConfig.value = { ...response.config }
116
+ const response = usesPluginSettingsApi.value
117
+ ? await request.run(
118
+ () => pluginApi.get<{
119
+ settings: Record<string, unknown>
120
+ mode?: string
121
+ revision?: string
122
+ }>('/settings'),
123
+ { success: 'load', errorMessage: 'Failed to load plugin config' },
124
+ )
125
+ : await request.run(
126
+ () => platformApi.get<{
127
+ plugin_name: string
128
+ config: Record<string, unknown>
129
+ revision?: string
130
+ }>(
131
+ `/plugins/${encodeURIComponent(name)}/config`,
132
+ ),
133
+ { success: 'load', errorMessage: 'Failed to load plugin config' },
134
+ )
135
+ const responseConfig = responseConfigPayload(response)
136
+ config.value = cloneConfig(responseConfig)
137
+ savedConfig.value = cloneConfig(responseConfig)
138
+ revision.value = response.revision ?? null
63
139
  } catch {
64
140
  // Error state is handled by request.run().
65
141
  } finally {
@@ -73,15 +149,75 @@ export function usePluginConfig(pluginName?: string): UsePluginConfigReturn {
73
149
 
74
150
  isSaving.value = true
75
151
  try {
76
- const response = await request.run(
77
- () => api.patch<{ plugin_name: string; config: Record<string, unknown> }>(
78
- `/plugins/${encodeURIComponent(name)}/config`,
79
- { config: config.value },
80
- ),
81
- { success: 'save', errorMessage: 'Failed to save plugin config' },
82
- )
83
- config.value = { ...response.config }
84
- savedConfig.value = { ...response.config }
152
+ const payload = cloneConfig(config.value)
153
+ let response: {
154
+ plugin_name?: string
155
+ config?: Record<string, unknown>
156
+ settings?: Record<string, unknown>
157
+ mode?: string
158
+ revision?: string
159
+ } | undefined
160
+ let nextRevision = revision.value
161
+
162
+ if (usesPluginSettingsApi.value) {
163
+ response = await request.run(
164
+ () => pluginApi.put<{
165
+ settings: Record<string, unknown>
166
+ mode?: string
167
+ revision?: string
168
+ }>(
169
+ '/settings',
170
+ payload,
171
+ revision.value
172
+ ? { headers: { 'If-Match': `"${revision.value}"` } }
173
+ : undefined,
174
+ ),
175
+ { success: 'save', errorMessage: 'Failed to save plugin config' },
176
+ )
177
+ nextRevision = response.revision ?? nextRevision
178
+ } else {
179
+ const dirtyKeys = changedTopLevelKeys(payload, savedConfig.value)
180
+ const platformOwnedChanged = dirtyKeys.has(PLATFORM_OWNED_SETTINGS_KEY)
181
+ const typedKeys = [...dirtyKeys].filter(
182
+ key => key !== PLATFORM_OWNED_SETTINGS_KEY,
183
+ )
184
+
185
+ if (typedKeys.length) {
186
+ response = await request.run(
187
+ () => platformApi.patch<{
188
+ plugin_name: string
189
+ config: Record<string, unknown>
190
+ revision?: string
191
+ }>(
192
+ `/plugins/${encodeURIComponent(name)}/config`,
193
+ { config: topLevelConfigPatch(payload, typedKeys) },
194
+ ),
195
+ { success: 'save', errorMessage: 'Failed to save plugin config' },
196
+ )
197
+ nextRevision = response.revision ?? nextRevision
198
+ }
199
+
200
+ if (platformOwnedChanged) {
201
+ response = await request.run(
202
+ () => platformApi.patch<{
203
+ plugin_name: string
204
+ config: Record<string, unknown>
205
+ revision?: string
206
+ }>(
207
+ `/plugins/${encodeURIComponent(name)}/config`,
208
+ { config: platformOwnedConfigPatch(payload) },
209
+ ),
210
+ { success: 'save', errorMessage: 'Failed to save plugin config' },
211
+ )
212
+ nextRevision = response.revision ?? nextRevision
213
+ }
214
+ }
215
+
216
+ if (!response) return true
217
+ const responseConfig = responseConfigPayload(response)
218
+ config.value = cloneConfig(responseConfig)
219
+ savedConfig.value = cloneConfig(responseConfig)
220
+ revision.value = response.revision ?? nextRevision
85
221
  return true
86
222
  } catch {
87
223
  return false
@@ -91,7 +227,7 @@ export function usePluginConfig(pluginName?: string): UsePluginConfigReturn {
91
227
  }
92
228
 
93
229
  function reset(): void {
94
- config.value = { ...savedConfig.value }
230
+ config.value = cloneConfig(savedConfig.value)
95
231
  request.clearError()
96
232
  }
97
233
 
@@ -0,0 +1,130 @@
1
+ import type {
2
+ FormFieldSchema,
3
+ FormFieldType,
4
+ FormSchema,
5
+ } from '../types/form-builder'
6
+ import type {
7
+ GeneratedJsonSchema,
8
+ GeneratedJobDefinition,
9
+ } from '../types/generated-ui'
10
+
11
+ type GeneratedReference = 'path'
12
+
13
+ interface ResolvedGeneratedField {
14
+ schema: GeneratedJsonSchema
15
+ reference?: GeneratedReference
16
+ hasDefault: boolean
17
+ defaultValue?: unknown
18
+ }
19
+
20
+ export function generatedJobFormSchema(definition: GeneratedJobDefinition): FormSchema & { sections: NonNullable<FormSchema['sections']> } {
21
+ const root = definition.input_schema
22
+ const required = new Set(root.required ?? [])
23
+ const fields = Object.entries(root.properties ?? {}).map(([name, schema]) =>
24
+ toFormField(name, resolveGeneratedField(root, schema), required.has(name)),
25
+ )
26
+ return {
27
+ sections: [{
28
+ id: definition.id,
29
+ title: definition.title,
30
+ description: definition.description || undefined,
31
+ columns: 1,
32
+ fields,
33
+ }],
34
+ submitLabel: 'Run analysis',
35
+ }
36
+ }
37
+
38
+ export function generatedJobDefaults(definition: GeneratedJobDefinition): Record<string, unknown> {
39
+ return Object.fromEntries(
40
+ Object.entries(definition.input_schema.properties ?? {})
41
+ .filter(([, schema]) => hasOwnDefault(schema))
42
+ .map(([name, schema]) => [name, structuredClone(schema.default)]),
43
+ )
44
+ }
45
+
46
+ export function normalizeGeneratedJobInput(
47
+ definition: GeneratedJobDefinition,
48
+ values: Record<string, unknown>,
49
+ ): Record<string, unknown> {
50
+ const normalized: Record<string, unknown> = {}
51
+ for (const [name, value] of Object.entries(values)) {
52
+ const field = definition.input_schema.properties?.[name]
53
+ if (!field) continue
54
+ normalized[name] = value
55
+ }
56
+ return normalized
57
+ }
58
+
59
+ function toFormField(
60
+ name: string,
61
+ resolved: ResolvedGeneratedField,
62
+ required: boolean,
63
+ ): FormFieldSchema {
64
+ const { schema, reference } = resolved
65
+ const validation: NonNullable<FormFieldSchema['validation']> = {}
66
+ if (required) validation.required = true
67
+ if (schema.minimum !== undefined) validation.min = schema.minimum
68
+ if (schema.maximum !== undefined) validation.max = schema.maximum
69
+ if (schema.minLength !== undefined) validation.minLength = schema.minLength
70
+ if (schema.maxLength !== undefined) validation.maxLength = schema.maxLength
71
+
72
+ const props: Record<string, unknown> = {}
73
+ if (schema.enum) props.options = [...schema.enum]
74
+ if (reference === 'path' && schema.type === 'array') props.multiple = true
75
+ return {
76
+ name,
77
+ label: schema.title || humanize(name),
78
+ type: fieldType(schema, reference),
79
+ defaultValue: resolved.hasDefault ? structuredClone(resolved.defaultValue) : undefined,
80
+ hint: schema.description,
81
+ validation,
82
+ props,
83
+ }
84
+ }
85
+
86
+ function resolveGeneratedField(
87
+ root: GeneratedJsonSchema,
88
+ field: GeneratedJsonSchema,
89
+ ): ResolvedGeneratedField {
90
+ const hasDefault = hasOwnDefault(field)
91
+ const candidate = field.anyOf?.find(item => item.type !== 'null')
92
+ ?? field.allOf?.[0]
93
+ ?? field
94
+ if (candidate.$ref) {
95
+ const referenceParts = candidate.$ref.split('/')
96
+ const key = referenceParts[referenceParts.length - 1]
97
+ const schema = key ? root.$defs?.[key] : undefined
98
+ if (!schema) throw new Error(`Generated field reference is missing: ${candidate.$ref}`)
99
+ return {
100
+ schema: { ...schema, title: field.title ?? schema.title },
101
+ reference: field['x-mint-component'] ?? schema['x-mint-component'],
102
+ hasDefault,
103
+ defaultValue: field.default,
104
+ }
105
+ }
106
+ return {
107
+ schema: candidate,
108
+ reference: field['x-mint-component'] ?? candidate['x-mint-component'],
109
+ hasDefault,
110
+ defaultValue: field.default,
111
+ }
112
+ }
113
+
114
+ function fieldType(schema: GeneratedJsonSchema, reference?: GeneratedReference): FormFieldType {
115
+ if (reference === 'path') return 'path'
116
+ if (schema.enum) return 'select'
117
+ const type = Array.isArray(schema.type) ? schema.type.find(value => value !== 'null') : schema.type
118
+ if (type === 'boolean') return 'toggle'
119
+ if (type === 'number' || type === 'integer') return 'number'
120
+ if (type === 'array') return 'tags'
121
+ return 'text'
122
+ }
123
+
124
+ function hasOwnDefault(schema: GeneratedJsonSchema): boolean {
125
+ return Object.prototype.hasOwnProperty.call(schema, 'default')
126
+ }
127
+
128
+ function humanize(value: string): string {
129
+ return value.replace(/[_-]+/g, ' ').replace(/^\w/, character => character.toUpperCase())
130
+ }
@@ -0,0 +1,21 @@
1
+ import { createPinia } from 'pinia'
2
+ import { createApp, h } from 'vue'
3
+
4
+ import GeneratedPluginView from '../components/GeneratedPluginView.vue'
5
+ import '../styles/index.css'
6
+
7
+ function generatedApiBase(location: Location): string {
8
+ const pluginPath = new URL('.', location.href).pathname.replace(/\/$/, '')
9
+ return `/api${pluginPath}`
10
+ }
11
+
12
+ const app = createApp({
13
+ name: 'MintGeneratedUiApp',
14
+ render: () => h(GeneratedPluginView, {
15
+ baseUrl: generatedApiBase(window.location),
16
+ showStandaloneLabel: true,
17
+ }),
18
+ })
19
+
20
+ app.use(createPinia())
21
+ app.mount('#app')