@morscherlab/mint-sdk 1.0.63 → 1.1.0-beta.10

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 (181) hide show
  1. package/README.md +119 -6
  2. package/dist/EmptyState-DgBRoYAy.js +647 -0
  3. package/dist/EmptyState-DgBRoYAy.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-BBWXC7KW.js} +12 -216
  7. package/dist/ExperimentSelectorModal-BBWXC7KW.js.map +1 -0
  8. package/dist/{SettingsModal-CJgLadsN.js → SettingsModal-Dpg-X1OM.js} +12 -9
  9. package/dist/{SettingsModal-CJgLadsN.js.map → SettingsModal-Dpg-X1OM.js.map} +1 -1
  10. package/dist/__tests__/components/BaseToggle.test.d.ts +1 -0
  11. package/dist/__tests__/components/GeneratedPathInput.test.d.ts +1 -0
  12. package/dist/__tests__/components/GeneratedPlotlyResult.test.d.ts +1 -0
  13. package/dist/__tests__/components/GeneratedPluginView.test.d.ts +1 -0
  14. package/dist/__tests__/components/GeneratedResultRenderer.test.d.ts +1 -0
  15. package/dist/__tests__/components/InstrumentAlertLog.test.d.ts +1 -0
  16. package/dist/__tests__/components/SectionCard.test.d.ts +1 -0
  17. package/dist/__tests__/composables/useGeneratedAnalysis.test.d.ts +1 -0
  18. package/dist/__tests__/generatedUi.test.d.ts +1 -0
  19. package/dist/__tests__/types/GeneratedJsonSchema.test.d.ts +1 -0
  20. package/dist/{analysisArtifactTypes-Cu40dzSG.js → analysisArtifactTypes-ejqR8MX3.js} +1 -1
  21. package/dist/{analysisArtifactTypes-Cu40dzSG.js.map → analysisArtifactTypes-ejqR8MX3.js.map} +1 -1
  22. package/dist/{auth-DZAwEeis.js → auth-B87xM--s.js} +1 -1
  23. package/dist/{auth-DZAwEeis.js.map → auth-B87xM--s.js.map} +1 -1
  24. package/dist/chunk-BoAXSpZd.js +33 -0
  25. package/dist/components/AppSidebar.vue.d.ts +1 -1
  26. package/dist/components/AutoGroupModal.vue.d.ts +2 -2
  27. package/dist/components/BaseSelect.vue.d.ts +1 -1
  28. package/dist/components/BaseToggle.vue.d.ts +2 -0
  29. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  30. package/dist/components/CollapsibleCard.vue.d.ts +1 -1
  31. package/dist/components/ComponentBindingRenderer.vue.d.ts +1 -1
  32. package/dist/components/ConfirmDialog.vue.d.ts +1 -1
  33. package/dist/components/ExperimentDataViewer.vue.d.ts +1 -1
  34. package/dist/components/ExperimentTimeline.vue.d.ts +1 -1
  35. package/dist/components/FileUploader.vue.d.ts +4 -2
  36. package/dist/components/FitPanel.vue.d.ts +2 -2
  37. package/dist/components/FormActions.vue.d.ts +1 -1
  38. package/dist/components/FormBuilder.vue.d.ts +1 -1
  39. package/dist/components/GeneratedPathInput.vue.d.ts +12 -0
  40. package/dist/components/GeneratedPlotlyResult.vue.d.ts +11 -0
  41. package/dist/components/GeneratedPluginView.vue.d.ts +10 -0
  42. package/dist/components/GeneratedResultRenderer.vue.d.ts +6 -0
  43. package/dist/components/JobsStatusTray.vue.d.ts +5 -1
  44. package/dist/components/LcmsSequenceTable.vue.d.ts +1 -1
  45. package/dist/components/PluginWorkspaceView.props.d.ts +1 -1
  46. package/dist/components/SampleLegend.vue.d.ts +1 -1
  47. package/dist/components/SampleSelector.vue.d.ts +2 -2
  48. package/dist/components/SectionCard.vue.d.ts +47 -0
  49. package/dist/components/SettingsModal.vue.d.ts +1 -1
  50. package/dist/components/SmartGroupFieldRecipe.vue.d.ts +2 -2
  51. package/dist/components/index.d.ts +5 -0
  52. package/dist/components/index.js +8 -9
  53. package/dist/components/internal/PlateMapEditorToolbarInternal.vue.d.ts +2 -2
  54. package/dist/{components-DdzHh3a_.js → components-Ci_BTx00.js} +2322 -2292
  55. package/dist/components-Ci_BTx00.js.map +1 -0
  56. package/dist/composables/index.d.ts +3 -2
  57. package/dist/composables/index.js +10 -11
  58. package/dist/composables/useGeneratedAnalysis.d.ts +62 -0
  59. package/dist/composables/useJobsStatusTray.d.ts +48 -4
  60. package/dist/composables/usePlatformContext.d.ts +3 -0
  61. package/dist/composables/usePluginClient.d.ts +17 -8
  62. package/dist/{composables-BUUphBqg.js → composables-C0VEJ73O.js} +154 -38
  63. package/dist/composables-C0VEJ73O.js.map +1 -0
  64. package/dist/experiment-utils-CxkruZ3G.js +392 -0
  65. package/dist/experiment-utils-CxkruZ3G.js.map +1 -0
  66. package/dist/generated/schema.d.ts +7 -0
  67. package/dist/generated-ui/main.d.ts +0 -0
  68. package/dist/index.d.ts +1 -0
  69. package/dist/index.js +15 -18
  70. package/dist/index.js.map +1 -1
  71. package/dist/install.js +7 -8
  72. package/dist/install.js.map +1 -1
  73. package/dist/plotly.min-BR53hja4.js +180634 -0
  74. package/dist/plotly.min-BR53hja4.js.map +1 -0
  75. package/dist/stores/auth.d.ts +2 -2
  76. package/dist/stores/index.js +1 -1
  77. package/dist/styles.css +8657 -8217
  78. package/dist/tailwind.preset.d.ts +5 -0
  79. package/dist/tailwind.preset.js +7 -2
  80. package/dist/tailwind.preset.js.map +1 -1
  81. package/dist/templates/index.js +4 -3
  82. package/dist/templates/packs.d.ts +0 -1
  83. package/dist/{templates-CNbPQNID.js → templates-Cdj93S9C.js} +2 -4
  84. package/dist/templates-Cdj93S9C.js.map +1 -0
  85. package/dist/types/components.d.ts +2 -0
  86. package/dist/types/form-builder.d.ts +1 -1
  87. package/dist/types/generated-ui.d.ts +183 -0
  88. package/dist/types/index.d.ts +2 -1
  89. package/dist/types/index.js +1 -1
  90. package/dist/types/instrument.d.ts +1 -0
  91. package/dist/types/jobs.d.ts +7 -1
  92. package/dist/types/platform.d.ts +2 -0
  93. package/dist/{useControlSchema-BZNdalmL.js → useControlSchema-B_qgU4rW.js} +1882 -172
  94. package/dist/useControlSchema-B_qgU4rW.js.map +1 -0
  95. package/dist/{useDropdownState-Bb-c_PeE.js → useDropdownState-gVaMXtwW.js} +2 -2
  96. package/dist/{useDropdownState-Bb-c_PeE.js.map → useDropdownState-gVaMXtwW.js.map} +1 -1
  97. package/dist/{useExperimentSelector-DRbj6vse.js → useExperimentSelector-BuQZRgMt.js} +4 -255
  98. package/dist/useExperimentSelector-BuQZRgMt.js.map +1 -0
  99. package/dist/{useFormBuilder-DKekvXRs.js → useFormBuilder-D1G5J5Z5.js} +2 -2
  100. package/dist/{useFormBuilder-DKekvXRs.js.map → useFormBuilder-D1G5J5Z5.js.map} +1 -1
  101. package/dist/{useAnalysisArtifacts-C3l0a0vu.js → useProtocolTemplates-DeaOmMaw.js} +307 -475
  102. package/dist/useProtocolTemplates-DeaOmMaw.js.map +1 -0
  103. package/package.json +7 -3
  104. package/src/__tests__/components/BaseSelect.test.ts +52 -0
  105. package/src/__tests__/components/BaseToggle.test.ts +40 -0
  106. package/src/__tests__/components/GeneratedPathInput.test.ts +132 -0
  107. package/src/__tests__/components/GeneratedPlotlyResult.test.ts +117 -0
  108. package/src/__tests__/components/GeneratedPluginView.test.ts +240 -0
  109. package/src/__tests__/components/GeneratedResultRenderer.test.ts +93 -0
  110. package/src/__tests__/components/InstrumentAlertLog.test.ts +59 -0
  111. package/src/__tests__/components/PluginWorkspaceView.test.ts +68 -14
  112. package/src/__tests__/components/SectionCard.test.ts +181 -0
  113. package/src/__tests__/composables/formBuilderRegistry.test.ts +5 -1
  114. package/src/__tests__/composables/useApi.test.ts +8 -0
  115. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +1742 -0
  116. package/src/__tests__/composables/useJobsStatusTray.test.ts +179 -1
  117. package/src/__tests__/composables/usePlatformContext.test.ts +28 -0
  118. package/src/__tests__/composables/usePluginClient.test.ts +402 -21
  119. package/src/__tests__/composables/usePluginConfig.test.ts +323 -7
  120. package/src/__tests__/docs/extractDocsTemplates.test.ts +0 -1
  121. package/src/__tests__/generatedUi.test.ts +119 -0
  122. package/src/__tests__/templates/templates.test.ts +1 -1
  123. package/src/__tests__/types/GeneratedJsonSchema.test.ts +62 -0
  124. package/src/components/BaseSelect.vue +16 -2
  125. package/src/components/BaseToggle.vue +3 -1
  126. package/src/components/FileUploader.vue +49 -11
  127. package/src/components/GeneratedPathInput.vue +218 -0
  128. package/src/components/GeneratedPlotlyResult.vue +87 -0
  129. package/src/components/GeneratedPluginView.vue +279 -0
  130. package/src/components/GeneratedResultRenderer.vue +137 -0
  131. package/src/components/InstrumentAlertLog.vue +2 -2
  132. package/src/components/JobsStatusTray.vue +14 -8
  133. package/src/components/PluginWorkspaceView.props.ts +1 -1
  134. package/src/components/PluginWorkspaceView.vue +1 -5
  135. package/src/components/SectionCard.story.vue +163 -0
  136. package/src/components/SectionCard.vue +98 -0
  137. package/src/components/index.ts +5 -0
  138. package/src/composables/formBuilderRegistry.ts +2 -0
  139. package/src/composables/index.ts +19 -0
  140. package/src/composables/useApi.ts +1 -0
  141. package/src/composables/useGeneratedAnalysis.ts +1270 -0
  142. package/src/composables/useJobsStatusTray.ts +234 -45
  143. package/src/composables/usePluginClient.ts +164 -30
  144. package/src/composables/usePluginConfig.ts +156 -20
  145. package/src/generated/schema.ts +130 -0
  146. package/src/generated-ui/main.ts +21 -0
  147. package/src/index.ts +3 -0
  148. package/src/jobs.ts +5 -0
  149. package/src/plotly-dist.d.ts +3 -0
  150. package/src/styles/components/section-card.css +161 -0
  151. package/src/styles/index.css +1 -0
  152. package/src/styles/variables.css +25 -0
  153. package/src/tailwind.preset.ts +5 -0
  154. package/src/templates/packs.ts +0 -3
  155. package/src/types/components.ts +12 -0
  156. package/src/types/form-builder.ts +1 -0
  157. package/src/types/generated-ui.ts +189 -0
  158. package/src/types/index.ts +4 -0
  159. package/src/types/instrument.ts +1 -0
  160. package/src/types/jobs.ts +10 -1
  161. package/src/types/platform.ts +2 -0
  162. package/dist/BaseButton-Dgqrze41.js +0 -74
  163. package/dist/BaseButton-Dgqrze41.js.map +0 -1
  164. package/dist/BaseModal-B9UA8Y_I.js +0 -165
  165. package/dist/BaseModal-B9UA8Y_I.js.map +0 -1
  166. package/dist/BaseSelect-ekgr9fDo.js +0 -179
  167. package/dist/BaseSelect-ekgr9fDo.js.map +0 -1
  168. package/dist/ExperimentPopover-Ryusjrhv.js +0 -4
  169. package/dist/ExperimentSelectorModal-Cg4-LXBD.js +0 -5
  170. package/dist/ExperimentSelectorModal-DZeSd-yr.js.map +0 -1
  171. package/dist/SettingsModal-Bl_9w2Y3.js +0 -5
  172. package/dist/components-DdzHh3a_.js.map +0 -1
  173. package/dist/composables-BUUphBqg.js.map +0 -1
  174. package/dist/experiment-utils-Bfa7CwPU.js +0 -108
  175. package/dist/experiment-utils-Bfa7CwPU.js.map +0 -1
  176. package/dist/templates-CNbPQNID.js.map +0 -1
  177. package/dist/useAnalysisArtifacts-C3l0a0vu.js.map +0 -1
  178. package/dist/useControlSchema-BZNdalmL.js.map +0 -1
  179. package/dist/useEventListener-CfVkP9Xz.js +0 -57
  180. package/dist/useEventListener-CfVkP9Xz.js.map +0 -1
  181. package/dist/useExperimentSelector-DRbj6vse.js.map +0 -1
@@ -22,12 +22,37 @@ import { AuthenticationRequiredError } from '../../composables/useApi'
22
22
  import { useAuthStore } from '../../stores/auth'
23
23
 
24
24
  const contract: PluginContract = {
25
- schemaVersion: 1,
25
+ schemaVersion: 2,
26
26
  plugin: {
27
27
  routesPrefix: '/drp',
28
28
  apiPrefix: '/api/drp',
29
29
  type: 'analysis',
30
30
  },
31
+ settings: {
32
+ modelName: 'PluginSettings',
33
+ valueSchema: {
34
+ type: 'object',
35
+ properties: {
36
+ threshold: { type: 'number', default: 0.05 },
37
+ },
38
+ },
39
+ formSchema: {
40
+ groups: [
41
+ {
42
+ id: 'settings',
43
+ label: 'Settings',
44
+ fields: [
45
+ {
46
+ name: 'threshold',
47
+ label: 'Threshold',
48
+ type: 'number',
49
+ defaultValue: 0.05,
50
+ },
51
+ ],
52
+ },
53
+ ],
54
+ },
55
+ },
31
56
  endpoints: [],
32
57
  hash: 'sha256:test',
33
58
  }
@@ -156,6 +181,21 @@ describe('usePluginClient', () => {
156
181
  expect(requestConfigs[0]!.url).toBe('/health')
157
182
  })
158
183
 
184
+ it('omits MINT auth transport for a public generated endpoint', async () => {
185
+ const client = createPluginClient(contract, {
186
+ endpoints: {
187
+ health: { method: 'get', path: '/health', requiresAuth: false },
188
+ },
189
+ })
190
+
191
+ await client.health()
192
+
193
+ expect(
194
+ (requestConfigs[0] as AxiosRequestConfig & { _mintSkipAuth?: boolean })
195
+ ._mintSkipAuth,
196
+ ).toBe(true)
197
+ })
198
+
159
199
  it('VITE_API_PREFIX overrides contract apiPrefix', async () => {
160
200
  vi.stubEnv('VITE_API_PREFIX', '/api/override')
161
201
  const client = createPluginClient(contract, {
@@ -672,7 +712,79 @@ describe('usePluginClient', () => {
672
712
  expect(requestConfigs).toHaveLength(0)
673
713
  })
674
714
 
675
- it('loads and saves plugin settings through the integrated platform config endpoint', async () => {
715
+ it.each([
716
+ 'platform',
717
+ undefined,
718
+ ] as const)(
719
+ 'should use the integrated platform config facade when settings_api is %s',
720
+ async (settingsApi) => {
721
+ ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
722
+ isIntegrated: true,
723
+ theme: 'light',
724
+ platformApiUrl: '/api',
725
+ plugin: {
726
+ id: 'drp',
727
+ name: 'drp',
728
+ version: '1.0.0',
729
+ route_prefix: '/drp',
730
+ api_prefix: '/api/drp',
731
+ ...(settingsApi ? { settings_api: settingsApi } : {}),
732
+ },
733
+ }
734
+ nextGetData = {
735
+ plugin_name: 'drp',
736
+ config: {
737
+ nasBasePath: '/data',
738
+ allowed_experiment_types: ['metabolomics'],
739
+ },
740
+ revision: 'revision-1',
741
+ }
742
+ nextPatchData = {
743
+ plugin_name: 'drp',
744
+ config: {
745
+ nasBasePath: '/mnt',
746
+ allowed_experiment_types: ['metabolomics'],
747
+ },
748
+ revision: 'revision-2',
749
+ }
750
+
751
+ const settings = usePluginSettings<{ nasBasePath?: string }>({ pluginName: 'drp' })
752
+ await settings.load()
753
+
754
+ expect(requestConfigs[0]!.baseURL).toBe('/api')
755
+ expect(requestConfigs[0]!.url).toBe('/plugins/drp/config')
756
+ expect(settings.values.value).toEqual({
757
+ nasBasePath: '/data',
758
+ allowed_experiment_types: ['metabolomics'],
759
+ })
760
+ expect(settings.settings.value).toEqual({
761
+ nasBasePath: '/data',
762
+ allowed_experiment_types: ['metabolomics'],
763
+ })
764
+ expect(settings.isDirty.value).toBe(false)
765
+
766
+ settings.setValues({
767
+ nasBasePath: '/mnt',
768
+ allowed_experiment_types: ['metabolomics'],
769
+ })
770
+ expect(settings.isDirty.value).toBe(true)
771
+ const ok = await settings.save()
772
+
773
+ expect(ok).toBe(true)
774
+ expect(requestConfigs[1]!.baseURL).toBe('/api')
775
+ expect(requestConfigs[1]!.url).toBe('/plugins/drp/config')
776
+ expect(requestMethods[1]).toBe('patch')
777
+ expect(requestConfigs[1]!.headers).toBeUndefined()
778
+ expect(requestBodies[0]).toEqual({ config: { nasBasePath: '/mnt' } })
779
+ expect(settings.values.value).toEqual({
780
+ nasBasePath: '/mnt',
781
+ allowed_experiment_types: ['metabolomics'],
782
+ })
783
+ expect(settings.isDirty.value).toBe(false)
784
+ },
785
+ )
786
+
787
+ it('should use the plugin settings router for an integrated plugin capability', async () => {
676
788
  ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
677
789
  isIntegrated: true,
678
790
  theme: 'light',
@@ -683,35 +795,281 @@ describe('usePluginClient', () => {
683
795
  version: '1.0.0',
684
796
  route_prefix: '/drp',
685
797
  api_prefix: '/api/drp',
798
+ settings_api: 'plugin',
686
799
  },
687
800
  }
688
- nextGetData = { plugin_name: 'drp', config: { nasBasePath: '/data' } }
689
- nextPatchData = { plugin_name: 'drp', config: { nasBasePath: '/mnt' } }
801
+ nextGetData = {
802
+ settings: { threshold: 1 },
803
+ mode: 'integrated',
804
+ revision: 'revision-1',
805
+ }
806
+ nextPutData = {
807
+ settings: { threshold: 2 },
808
+ mode: 'integrated',
809
+ revision: 'revision-2',
810
+ }
690
811
 
691
- const settings = usePluginSettings<{ nasBasePath?: string }>({ pluginName: 'drp' })
812
+ const settings = usePluginSettings<{ threshold?: number }>({ pluginName: 'drp' })
692
813
  await settings.load()
814
+ const saved = await settings.save({ threshold: 2 })
815
+
816
+ expect({
817
+ saved,
818
+ methods: requestMethods,
819
+ requests: requestConfigs.map(({ baseURL, url, headers }) => ({
820
+ baseURL,
821
+ url,
822
+ headers,
823
+ })),
824
+ bodies: requestBodies,
825
+ values: settings.values.value,
826
+ }).toEqual({
827
+ saved: true,
828
+ methods: ['get', 'put'],
829
+ requests: [
830
+ {
831
+ baseURL: '/api/drp',
832
+ url: '/settings',
833
+ headers: undefined,
834
+ },
835
+ {
836
+ baseURL: '/api/drp',
837
+ url: '/settings',
838
+ headers: { 'If-Match': '"revision-1"' },
839
+ },
840
+ ],
841
+ bodies: [{ threshold: 2 }],
842
+ values: { threshold: 2 },
843
+ })
844
+ })
693
845
 
694
- expect(requestConfigs[0]!.baseURL).toBe('/api')
695
- expect(requestConfigs[0]!.url).toBe('/plugins/drp/config')
696
- expect(settings.values.value).toEqual({ nasBasePath: '/data' })
697
- expect(settings.settings.value).toEqual({ nasBasePath: '/data' })
698
- expect(settings.isDirty.value).toBe(false)
846
+ it('should preserve the contract v2 form schema in settingsConfig', () => {
847
+ const settings = usePluginSettings({
848
+ apiBaseUrl: '/api/drp',
849
+ loadOnMount: false,
850
+ schema: contract.settings.formSchema,
851
+ })
699
852
 
700
- settings.setValues({ nasBasePath: '/mnt' })
701
- expect(settings.isDirty.value).toBe(true)
702
- const ok = await settings.save()
853
+ expect(settings.settingsConfig.value.schema).toBe(contract.settings.formSchema)
854
+ })
703
855
 
704
- expect(ok).toBe(true)
705
- expect(requestConfigs[1]!.baseURL).toBe('/api')
706
- expect(requestConfigs[1]!.url).toBe('/plugins/drp/config')
707
- expect(requestBodies[0]).toEqual({ config: { nasBasePath: '/mnt' } })
708
- expect(settings.values.value).toEqual({ nasBasePath: '/mnt' })
709
- expect(settings.isDirty.value).toBe(false)
856
+ it('should use the platform facade for an explicit different plugin name', async () => {
857
+ ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
858
+ isIntegrated: true,
859
+ theme: 'light',
860
+ platformApiUrl: '/api',
861
+ plugin: {
862
+ id: 'current-plugin',
863
+ name: 'current-plugin',
864
+ version: '1.0.0',
865
+ route_prefix: '/current-plugin',
866
+ api_prefix: '/api/current-plugin',
867
+ settings_api: 'plugin',
868
+ },
869
+ }
870
+ nextGetData = {
871
+ plugin_name: 'other-plugin',
872
+ config: { threshold: 1 },
873
+ revision: 'revision-1',
874
+ }
875
+
876
+ const settings = usePluginSettings<{ threshold?: number }>({
877
+ pluginName: 'other-plugin',
878
+ })
879
+ await settings.load()
880
+
881
+ expect({
882
+ method: requestMethods[0],
883
+ baseURL: requestConfigs[0]!.baseURL,
884
+ url: requestConfigs[0]!.url,
885
+ values: settings.values.value,
886
+ }).toEqual({
887
+ method: 'get',
888
+ baseURL: '/api',
889
+ url: '/plugins/other-plugin/config',
890
+ values: { threshold: 1 },
891
+ })
892
+ })
893
+
894
+ it('should split changed platform-owned settings from a versioned typed save', async () => {
895
+ ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
896
+ isIntegrated: true,
897
+ theme: 'light',
898
+ platformApiUrl: '/api',
899
+ plugin: {
900
+ id: 'drp',
901
+ name: 'drp',
902
+ version: '1.0.0',
903
+ route_prefix: '/drp',
904
+ api_prefix: '/api/drp',
905
+ },
906
+ }
907
+ nextGetData = {
908
+ plugin_name: 'drp',
909
+ config: {
910
+ threshold: 1,
911
+ allowed_experiment_types: ['metabolomics'],
912
+ },
913
+ revision: 'revision-1',
914
+ }
915
+ nextPatchData = {
916
+ plugin_name: 'drp',
917
+ config: {
918
+ threshold: 2,
919
+ allowed_experiment_types: ['proteomics'],
920
+ },
921
+ revision: 'untrusted-platform-revision',
922
+ }
923
+ const settings = usePluginSettings<{ threshold?: number }>({ pluginName: 'drp' })
924
+ await settings.load()
925
+ settings.setValues({
926
+ threshold: 2,
927
+ allowed_experiment_types: ['proteomics'],
928
+ })
929
+
930
+ const saved = await settings.save()
931
+ nextPatchData = {
932
+ plugin_name: 'drp',
933
+ config: {
934
+ threshold: 3,
935
+ allowed_experiment_types: ['proteomics'],
936
+ },
937
+ revision: 'revision-3',
938
+ }
939
+ settings.setValues({
940
+ threshold: 3,
941
+ allowed_experiment_types: ['proteomics'],
942
+ })
943
+ const savedAgain = await settings.save()
944
+
945
+ expect({
946
+ saved,
947
+ savedAgain,
948
+ methods: requestMethods,
949
+ bodies: requestBodies,
950
+ writeHeaders: requestConfigs.slice(1).map(config => config.headers),
951
+ values: settings.values.value,
952
+ isDirty: settings.isDirty.value,
953
+ }).toEqual({
954
+ saved: true,
955
+ savedAgain: true,
956
+ methods: ['get', 'patch', 'patch', 'patch'],
957
+ bodies: [
958
+ { config: { threshold: 2 } },
959
+ { config: { allowed_experiment_types: ['proteomics'] } },
960
+ { config: { threshold: 3 } },
961
+ ],
962
+ writeHeaders: [undefined, undefined, undefined],
963
+ values: {
964
+ threshold: 3,
965
+ allowed_experiment_types: ['proteomics'],
966
+ },
967
+ isDirty: false,
968
+ })
969
+ })
970
+
971
+ it('should avoid a typed PUT when only the platform-owned setting changes', async () => {
972
+ ;(window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__ = {
973
+ isIntegrated: true,
974
+ theme: 'light',
975
+ platformApiUrl: '/api',
976
+ plugin: {
977
+ id: 'drp',
978
+ name: 'drp',
979
+ version: '1.0.0',
980
+ route_prefix: '/drp',
981
+ api_prefix: '/api/drp',
982
+ },
983
+ }
984
+ nextGetData = {
985
+ plugin_name: 'drp',
986
+ config: {
987
+ threshold: 1,
988
+ allowed_experiment_types: ['metabolomics'],
989
+ },
990
+ revision: 'revision-1',
991
+ }
992
+ nextPatchData = {
993
+ plugin_name: 'drp',
994
+ config: {
995
+ threshold: 1,
996
+ allowed_experiment_types: ['proteomics'],
997
+ },
998
+ }
999
+
1000
+ const settings = usePluginSettings<{ threshold?: number }>({ pluginName: 'drp' })
1001
+ await settings.load()
1002
+ settings.setValues({
1003
+ threshold: 1,
1004
+ allowed_experiment_types: ['proteomics'],
1005
+ })
1006
+
1007
+ const saved = await settings.save()
1008
+
1009
+ expect({
1010
+ saved,
1011
+ methods: requestMethods,
1012
+ bodies: requestBodies,
1013
+ values: settings.values.value,
1014
+ }).toEqual({
1015
+ saved: true,
1016
+ methods: ['get', 'patch'],
1017
+ bodies: [{ config: { allowed_experiment_types: ['proteomics'] } }],
1018
+ values: {
1019
+ threshold: 1,
1020
+ allowed_experiment_types: ['proteomics'],
1021
+ },
1022
+ })
1023
+ })
1024
+
1025
+ it('should detect nested settings edits and restore the saved JSON snapshot', async () => {
1026
+ nextGetData = {
1027
+ settings: {
1028
+ rules: {
1029
+ thresholds: [1, 2],
1030
+ },
1031
+ },
1032
+ mode: 'standalone',
1033
+ }
1034
+
1035
+ const settings = usePluginSettings<{
1036
+ rules?: { thresholds: number[] }
1037
+ }>({
1038
+ apiBaseUrl: '/api/drp',
1039
+ loadOnMount: false,
1040
+ })
1041
+ await settings.load()
1042
+ settings.values.value.rules!.thresholds[0] = 9
1043
+
1044
+ const dirtyAfterMutation = settings.isDirty.value
1045
+ settings.reset()
1046
+
1047
+ expect({
1048
+ dirtyAfterMutation,
1049
+ values: settings.values.value,
1050
+ isDirty: settings.isDirty.value,
1051
+ }).toEqual({
1052
+ dirtyAfterMutation: true,
1053
+ values: {
1054
+ rules: {
1055
+ thresholds: [1, 2],
1056
+ },
1057
+ },
1058
+ isDirty: false,
1059
+ })
710
1060
  })
711
1061
 
712
1062
  it('loads and saves plugin settings through the standalone settings router', async () => {
713
- nextGetData = { settings: { nas_base_path: '/data' }, mode: 'standalone' }
714
- nextPutData = { settings: { nas_base_path: '/mnt' }, mode: 'standalone' }
1063
+ nextGetData = {
1064
+ settings: { nas_base_path: '/data' },
1065
+ mode: 'standalone',
1066
+ revision: 'revision-1',
1067
+ }
1068
+ nextPutData = {
1069
+ settings: { nas_base_path: '/mnt' },
1070
+ mode: 'standalone',
1071
+ revision: 'revision-2',
1072
+ }
715
1073
 
716
1074
  const settings = usePluginSettings<{ nas_base_path?: string }>({
717
1075
  apiBaseUrl: '/api/drp',
@@ -725,6 +1083,7 @@ describe('usePluginClient', () => {
725
1083
  expect(requestConfigs[0]!.url).toBe('/settings')
726
1084
  expect(requestConfigs[1]!.baseURL).toBe('/api/drp')
727
1085
  expect(requestConfigs[1]!.url).toBe('/settings')
1086
+ expect(requestConfigs[1]!.headers).toEqual({ 'If-Match': '"revision-1"' })
728
1087
  expect(requestBodies[0]).toEqual({ nas_base_path: '/mnt' })
729
1088
  expect(settings.settingsConfig.value.values).toEqual({ nas_base_path: '/mnt' })
730
1089
  })
@@ -774,6 +1133,28 @@ describe('usePluginClient', () => {
774
1133
  expect(stream.lastEventId.value).toBe('7')
775
1134
  })
776
1135
 
1136
+ it('omits MINT auth from a public plugin event stream', async () => {
1137
+ const authStore = useAuthStore()
1138
+ authStore.setToken('stream-token', 3600)
1139
+ const fetchMock = vi.fn<typeof fetch>(async () =>
1140
+ new Response(new ReadableStream({ start: controller => controller.close() }), {
1141
+ status: 200,
1142
+ }),
1143
+ )
1144
+ vi.stubGlobal('fetch', fetchMock)
1145
+
1146
+ usePluginEventStream(
1147
+ contract,
1148
+ { method: 'get', path: '/public/events', requiresAuth: false },
1149
+ { reconnect: false },
1150
+ )
1151
+
1152
+ await vi.waitFor(() => expect(fetchMock).toHaveBeenCalledOnce())
1153
+ const [, init] = fetchMock.mock.calls[0]!
1154
+
1155
+ expect((init?.headers as Headers).get('Authorization')).toBeNull()
1156
+ })
1157
+
777
1158
  it('normalizes stream auth failures and does not reconnect rejected requests', async () => {
778
1159
  const authStore = useAuthStore()
779
1160
  authStore.setToken('expired-stream-token', 3600)