@morscherlab/mint-sdk 1.2.0-beta.9 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (215) hide show
  1. package/README.md +30 -0
  2. package/dist/{ExperimentPopover-oL0fg_A0.js → ExperimentPopover-CJdNlCZb.js} +21 -14
  3. package/dist/ExperimentPopover-CJdNlCZb.js.map +1 -0
  4. package/dist/{ExperimentSelectorModal-BeEOomw_.js → ExperimentSelectorModal-DHE9k8TP.js} +5 -5
  5. package/dist/{ExperimentSelectorModal-BeEOomw_.js.map → ExperimentSelectorModal-DHE9k8TP.js.map} +1 -1
  6. package/dist/{SettingsModal-Dphh3RrI.js → SettingsModal-DOcCf0Vo.js} +41 -18
  7. package/dist/SettingsModal-DOcCf0Vo.js.map +1 -0
  8. package/dist/{Skeleton-DxtwJ5gt.js → Skeleton-jhF9wUkU.js} +19 -9
  9. package/dist/Skeleton-jhF9wUkU.js.map +1 -0
  10. package/dist/__tests__/apiErrors.test.d.ts +1 -0
  11. package/dist/__tests__/components/FileBrowserModal.test.d.ts +1 -0
  12. package/dist/__tests__/components/FilePicker.state.test.d.ts +1 -0
  13. package/dist/__tests__/components/FilePicker.test.d.ts +1 -0
  14. package/dist/__tests__/components/FormField.test.d.ts +1 -0
  15. package/dist/__tests__/components/NumberInput.test.d.ts +1 -0
  16. package/dist/__tests__/components/PlotlyChart.test.d.ts +1 -0
  17. package/dist/__tests__/components/iconButtonIconSizing.test.d.ts +1 -0
  18. package/dist/__tests__/composables/localFiles.test.d.ts +1 -0
  19. package/dist/__tests__/composables/useFileBrowser.test.d.ts +1 -0
  20. package/dist/__tests__/composables/usePickerSelection.test.d.ts +1 -0
  21. package/dist/__tests__/composables/usePlatformFilePickerAdapter.test.d.ts +1 -0
  22. package/dist/__tests__/styles/tokenOnlyColors.test.d.ts +1 -0
  23. package/dist/apiErrors.d.ts +26 -0
  24. package/dist/components/AppSidebar.vue.d.ts +7 -7
  25. package/dist/components/BaseInput.vue.d.ts +1 -1
  26. package/dist/components/BasePill.vue.d.ts +3 -0
  27. package/dist/components/BaseSelect.vue.d.ts +1 -1
  28. package/dist/components/BaseTabs.vue.d.ts +1 -4
  29. package/dist/components/BaseTextarea.vue.d.ts +1 -1
  30. package/dist/components/BioTemplateExperimentWorkspaceView.vue.d.ts +2 -2
  31. package/dist/components/BioTemplatePackWorkspaceView.vue.d.ts +2 -2
  32. package/dist/components/ConcentrationInput.vue.d.ts +1 -1
  33. package/dist/components/DatePicker.vue.d.ts +1 -1
  34. package/dist/components/DateTimePicker.vue.d.ts +1 -1
  35. package/dist/components/FileBrowserModal.vue.d.ts +99 -0
  36. package/dist/components/FilePicker.vue.d.ts +32 -0
  37. package/dist/components/FileUploader.vue.d.ts +8 -2
  38. package/dist/components/FormField.vue.d.ts +8 -1
  39. package/dist/components/FormulaInput.vue.d.ts +1 -1
  40. package/dist/components/GeneratedPlotlyResult.vue.d.ts +1 -3
  41. package/dist/components/MobileSupportGate.vue.d.ts +1 -1
  42. package/dist/components/MoleculeInput.vue.d.ts +1 -1
  43. package/dist/components/NumberInput.vue.d.ts +1 -1
  44. package/dist/components/PlotlyChart.vue.d.ts +59 -0
  45. package/dist/components/PluginWorkspaceView.props.d.ts +3 -3
  46. package/dist/components/ResourceCard.vue.d.ts +1 -1
  47. package/dist/components/SecretInput.vue.d.ts +1 -1
  48. package/dist/components/SequenceInput.vue.d.ts +1 -1
  49. package/dist/components/StatusIndicator.vue.d.ts +1 -1
  50. package/dist/components/TagsInput.vue.d.ts +1 -1
  51. package/dist/components/TimePicker.vue.d.ts +1 -1
  52. package/dist/components/TimeRangeInput.vue.d.ts +1 -1
  53. package/dist/components/UnitInput.vue.d.ts +1 -1
  54. package/dist/components/WellPlate.vue.d.ts +1 -1
  55. package/dist/components/fileBrowserPresentation.d.ts +10 -0
  56. package/dist/components/index.d.ts +4 -0
  57. package/dist/components/index.js +8 -8
  58. package/dist/components/internal/FileBrowserDialogInternal.vue.d.ts +34 -0
  59. package/dist/components/internal/FileBrowserListingInternal.vue.d.ts +25 -0
  60. package/dist/components/internal/FileBrowserToolbarInternal.vue.d.ts +26 -0
  61. package/dist/components/internal/FilePickerPreviewInternal.vue.d.ts +18 -0
  62. package/dist/components/internal/FilePickerTreeInternal.vue.d.ts +31 -0
  63. package/dist/{components-CgAUzF8F.js → components-Cp8DcNrU.js} +4363 -1730
  64. package/dist/components-Cp8DcNrU.js.map +1 -0
  65. package/dist/composables/filePicker/localFiles.d.ts +3 -0
  66. package/dist/composables/filePicker/usePickerNavigation.d.ts +96 -0
  67. package/dist/composables/filePicker/usePickerSelection.d.ts +31 -0
  68. package/dist/composables/filePicker/validation.d.ts +5 -0
  69. package/dist/composables/index.d.ts +2 -0
  70. package/dist/composables/index.js +10 -10
  71. package/dist/composables/pluginWorkspaceModel.d.ts +0 -3
  72. package/dist/composables/useApi.d.ts +14 -7
  73. package/dist/composables/useFileBrowser.d.ts +55 -0
  74. package/dist/composables/useFilePicker.d.ts +189 -0
  75. package/dist/composables/usePlatformFilePickerAdapter.d.ts +11 -0
  76. package/dist/composables/useToast.d.ts +22 -1
  77. package/dist/{composables-B_pia3hO.js → composables-D_4qqOlp.js} +300 -11
  78. package/dist/composables-D_4qqOlp.js.map +1 -0
  79. package/dist/{errors-_W0-LbA6.js → errors-BLuqMee5.js} +78 -15
  80. package/dist/errors-BLuqMee5.js.map +1 -0
  81. package/dist/{experiment-Cwl9AYxw.js → experiment-CVDRAnqp.js} +2 -2
  82. package/dist/{experiment-Cwl9AYxw.js.map → experiment-CVDRAnqp.js.map} +1 -1
  83. package/dist/{experiment-utils-DV3DrvE_.js → experiment-utils-CV2Wza3z.js} +1 -1
  84. package/dist/{experiment-utils-DV3DrvE_.js.map → experiment-utils-CV2Wza3z.js.map} +1 -1
  85. package/dist/index.d.ts +1 -0
  86. package/dist/index.js +15 -15
  87. package/dist/install.js +2 -2
  88. package/dist/stores/index.js +2 -2
  89. package/dist/styles.css +2957 -1708
  90. package/dist/templates/index.js +2 -2
  91. package/dist/{templates-Qq113pyZ.js → templates-Dd9bBSs6.js} +2 -2
  92. package/dist/{templates-Qq113pyZ.js.map → templates-Dd9bBSs6.js.map} +1 -1
  93. package/dist/types/components.d.ts +21 -0
  94. package/dist/types/fileBrowserTypes.d.ts +77 -0
  95. package/dist/types/filePicker.d.ts +77 -0
  96. package/dist/types/form-builder.d.ts +2 -0
  97. package/dist/types/generated-ui.d.ts +2 -0
  98. package/dist/types/index.d.ts +3 -1
  99. package/dist/{useControlSchema-aMZlb4Sc.js → useControlSchema-BB2GRPhW.js} +190 -105
  100. package/dist/useControlSchema-BB2GRPhW.js.map +1 -0
  101. package/dist/{useDropdownState-DitCtrTT.js → useDropdownState-C7hRiVQf.js} +2 -2
  102. package/dist/{useDropdownState-DitCtrTT.js.map → useDropdownState-C7hRiVQf.js.map} +1 -1
  103. package/dist/{useExperimentSelector-I8HC8hUu.js → useExperimentSelector-G_CAOFTX.js} +4 -4
  104. package/dist/{useExperimentSelector-I8HC8hUu.js.map → useExperimentSelector-G_CAOFTX.js.map} +1 -1
  105. package/dist/{useFormBuilder-TCxNnqeb.js → useFormBuilder-BnGSdSRG.js} +2 -2
  106. package/dist/{useFormBuilder-TCxNnqeb.js.map → useFormBuilder-BnGSdSRG.js.map} +1 -1
  107. package/dist/{useProtocolTemplates-BmX_iVBR.js → validation-CTxuzLIX.js} +70 -13
  108. package/dist/validation-CTxuzLIX.js.map +1 -0
  109. package/package.json +1 -1
  110. package/src/__tests__/apiErrors.test.ts +81 -0
  111. package/src/__tests__/components/AppSidebar.test.ts +84 -94
  112. package/src/__tests__/components/AppToastContainer.test.ts +55 -1
  113. package/src/__tests__/components/BioTemplatePresetWorkspaceView.test.ts +0 -21
  114. package/src/__tests__/components/ControlWorkspaceView.test.ts +0 -2
  115. package/src/__tests__/components/DoseDesignWorkspaceView.test.ts +0 -2
  116. package/src/__tests__/components/ExperimentPopover.test.ts +30 -0
  117. package/src/__tests__/components/FileBrowserModal.test.ts +460 -0
  118. package/src/__tests__/components/FilePicker.state.test.ts +287 -0
  119. package/src/__tests__/components/FilePicker.test.ts +487 -0
  120. package/src/__tests__/components/FormBuilder.test.ts +26 -0
  121. package/src/__tests__/components/FormField.test.ts +26 -0
  122. package/src/__tests__/components/GeneratedPathInput.test.ts +18 -0
  123. package/src/__tests__/components/GeneratedPlotlyResult.test.ts +52 -5
  124. package/src/__tests__/components/NumberInput.test.ts +105 -0
  125. package/src/__tests__/components/PlotlyChart.test.ts +305 -0
  126. package/src/__tests__/components/PluginWorkspaceView.internal.test.ts +1 -2
  127. package/src/__tests__/components/PluginWorkspaceView.test.ts +1 -2
  128. package/src/__tests__/components/SegmentedControl.test.ts +19 -0
  129. package/src/__tests__/components/iconButtonIconSizing.test.ts +18 -0
  130. package/src/__tests__/composables/localFiles.test.ts +57 -0
  131. package/src/__tests__/composables/useApi.test.ts +92 -9
  132. package/src/__tests__/composables/useFileBrowser.test.ts +305 -0
  133. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +7 -2
  134. package/src/__tests__/composables/usePickerSelection.test.ts +98 -0
  135. package/src/__tests__/composables/usePlatformFilePickerAdapter.test.ts +67 -0
  136. package/src/__tests__/composables/usePluginClient.test.ts +3 -0
  137. package/src/__tests__/generatedUi.test.ts +111 -0
  138. package/src/__tests__/styles/tokenOnlyColors.test.ts +16 -0
  139. package/src/apiErrors.ts +85 -0
  140. package/src/components/AppLayout.story.vue +1 -1
  141. package/src/components/AppSidebar.story.vue +7 -10
  142. package/src/components/AppSidebar.vue +178 -58
  143. package/src/components/AppToastContainer.vue +65 -23
  144. package/src/components/BasePill.story.vue +2 -0
  145. package/src/components/BasePill.vue +4 -0
  146. package/src/components/BaseTabs.story.vue +0 -42
  147. package/src/components/BaseTabs.vue +3 -6
  148. package/src/components/ExperimentPopover.vue +10 -2
  149. package/src/components/FileBrowserModal.story.vue +203 -0
  150. package/src/components/FileBrowserModal.vue +384 -0
  151. package/src/components/FilePicker.story.vue +44 -0
  152. package/src/components/FilePicker.vue +284 -0
  153. package/src/components/FileUploader.vue +42 -15
  154. package/src/components/FormField.vue +10 -3
  155. package/src/components/GeneratedPathInput.vue +11 -7
  156. package/src/components/GeneratedPlotlyResult.vue +34 -69
  157. package/src/components/GeneratedResultRenderer.vue +0 -5
  158. package/src/components/NumberInput.story.vue +32 -13
  159. package/src/components/NumberInput.vue +116 -55
  160. package/src/components/PlotlyChart.story.vue +61 -0
  161. package/src/components/PlotlyChart.vue +233 -0
  162. package/src/components/PluginWorkspaceView.props.ts +3 -3
  163. package/src/components/SegmentedControl.vue +2 -2
  164. package/src/components/fileBrowserPresentation.ts +39 -0
  165. package/src/components/index.ts +5 -0
  166. package/src/components/internal/FileBrowserDialogInternal.vue +75 -0
  167. package/src/components/internal/FileBrowserListingInternal.vue +197 -0
  168. package/src/components/internal/FileBrowserToolbarInternal.vue +136 -0
  169. package/src/components/internal/FilePickerPreviewInternal.vue +100 -0
  170. package/src/components/internal/FilePickerTreeInternal.vue +238 -0
  171. package/src/components/internal/FormFieldRendererInternal.vue +17 -0
  172. package/src/components/internal/WorkspaceSidebarInternal.vue +1 -2
  173. package/src/composables/filePicker/localFiles.ts +65 -0
  174. package/src/composables/filePicker/usePickerNavigation.ts +220 -0
  175. package/src/composables/filePicker/usePickerSelection.ts +270 -0
  176. package/src/composables/filePicker/validation.ts +41 -0
  177. package/src/composables/index.ts +7 -0
  178. package/src/composables/pluginWorkspaceModel.ts +0 -3
  179. package/src/composables/useApi.ts +57 -13
  180. package/src/composables/useFileBrowser.ts +314 -0
  181. package/src/composables/useFilePicker.ts +366 -0
  182. package/src/composables/useGeneratedAnalysis.ts +10 -17
  183. package/src/composables/usePlatformFilePickerAdapter.ts +104 -0
  184. package/src/composables/usePluginClient.ts +3 -0
  185. package/src/composables/usePluginWorkspace.ts +1 -2
  186. package/src/composables/useToast.ts +21 -5
  187. package/src/generated/schema.ts +58 -18
  188. package/src/index.ts +3 -0
  189. package/src/styles/components/app-sidebar.css +282 -121
  190. package/src/styles/components/file-browser-modal.css +489 -0
  191. package/src/styles/components/file-picker.css +502 -0
  192. package/src/styles/components/form-builder.css +1 -1
  193. package/src/styles/components/form-field.css +28 -0
  194. package/src/styles/components/icon-button.css +9 -0
  195. package/src/styles/components/number-input.css +73 -91
  196. package/src/styles/components/pill.css +49 -51
  197. package/src/styles/components/segmented-control.css +16 -11
  198. package/src/styles/components/tabs.css +18 -34
  199. package/src/styles/components/toast.css +136 -27
  200. package/src/styles/index.css +3 -0
  201. package/src/styles/variables.css +25 -5
  202. package/src/types/components.ts +23 -0
  203. package/src/types/fileBrowserTypes.ts +85 -0
  204. package/src/types/filePicker.ts +72 -0
  205. package/src/types/form-builder.ts +2 -0
  206. package/src/types/generated-ui.ts +2 -0
  207. package/src/types/index.ts +16 -0
  208. package/dist/ExperimentPopover-oL0fg_A0.js.map +0 -1
  209. package/dist/SettingsModal-Dphh3RrI.js.map +0 -1
  210. package/dist/Skeleton-DxtwJ5gt.js.map +0 -1
  211. package/dist/components-CgAUzF8F.js.map +0 -1
  212. package/dist/composables-B_pia3hO.js.map +0 -1
  213. package/dist/errors-_W0-LbA6.js.map +0 -1
  214. package/dist/useControlSchema-aMZlb4Sc.js.map +0 -1
  215. package/dist/useProtocolTemplates-BmX_iVBR.js.map +0 -1
@@ -14,6 +14,7 @@ import type {
14
14
  } from './useJobsStatusTray'
15
15
  import { useEventStream, type EventStreamMessage } from './useEventStream'
16
16
  import { isActiveJobStatus, normalizeJobState } from '../jobs'
17
+ import { MintApiError, mintApiErrorFromResponse } from '../apiErrors'
17
18
  import type {
18
19
  GeneratedAnalysisJob,
19
20
  GeneratedAnalysisJobInput,
@@ -129,16 +130,6 @@ type PluginJobStreamData = GeneratedAnalysisJobInput | {
129
130
  jobs: GeneratedAnalysisJobInput[]
130
131
  }
131
132
 
132
- class GeneratedAnalysisRequestError extends Error {
133
- constructor(
134
- readonly status: number,
135
- readonly detail: unknown,
136
- ) {
137
- super(typeof detail === 'string' ? detail : `Request failed (${status})`)
138
- this.name = 'GeneratedAnalysisRequestError'
139
- }
140
- }
141
-
142
133
  export function createGeneratedAnalysisTransport(
143
134
  options: GeneratedAnalysisTransportOptions = {},
144
135
  ): GeneratedAnalysisTransport {
@@ -838,7 +829,7 @@ function createPluginJobs(
838
829
  job: GeneratedAnalysisJob,
839
830
  cause: unknown,
840
831
  ): void {
841
- if (cause instanceof GeneratedAnalysisRequestError) {
832
+ if (cause instanceof MintApiError) {
842
833
  if (cause.status === 404 || cause.status === 410) {
843
834
  discardCachedResult(job.job_id)
844
835
  jobs.value = jobs.value.map(candidate => (
@@ -1037,8 +1028,10 @@ function createPluginJobs(
1037
1028
  },
1038
1029
  })
1039
1030
  if (!response.ok) {
1040
- const payload = await response.json().catch(() => null) as { detail?: unknown } | null
1041
- throw new GeneratedAnalysisRequestError(response.status, payload?.detail)
1031
+ const payload: unknown = await response.json().catch(() => null)
1032
+ throw mintApiErrorFromResponse(response.status, payload, {
1033
+ requestId: response.headers.get('X-Request-ID'),
1034
+ })
1042
1035
  }
1043
1036
  return await response.json() as T
1044
1037
  }
@@ -1177,20 +1170,20 @@ function createGeneratedSessionStorage(candidate?: Storage): GeneratedSessionSto
1177
1170
  }
1178
1171
 
1179
1172
  function isSessionNotFoundError(error: unknown): boolean {
1180
- return error instanceof GeneratedAnalysisRequestError
1173
+ return error instanceof MintApiError
1181
1174
  && error.status === 404
1182
- && error.detail === SESSION_NOT_FOUND_DETAIL
1175
+ && (error.detail === SESSION_NOT_FOUND_DETAIL || error.message === SESSION_NOT_FOUND_DETAIL)
1183
1176
  }
1184
1177
 
1185
1178
  function isRetryableResultError(error: unknown): boolean {
1186
- return !(error instanceof GeneratedAnalysisRequestError)
1179
+ return !(error instanceof MintApiError)
1187
1180
  || error.status === 409
1188
1181
  || error.status === 429
1189
1182
  || error.status >= 500
1190
1183
  }
1191
1184
 
1192
1185
  function isRetryableJobLookupError(error: unknown): boolean {
1193
- return !(error instanceof GeneratedAnalysisRequestError)
1186
+ return !(error instanceof MintApiError)
1194
1187
  || error.status === 408
1195
1188
  || error.status === 429
1196
1189
  || error.status >= 500
@@ -0,0 +1,104 @@
1
+ import type { PickerAdapter, PickerNode, PickerRequest } from '../types/filePicker'
2
+ import type { FileDirectoryListing, ServerMount } from '../types/fileBrowserTypes'
3
+ import { useApi } from './useApi'
4
+ import { getInjectedPlatformContext } from './platformContextHelpers'
5
+ import { pickerFormat } from './filePicker/validation'
6
+
7
+ /** Opaque picker identity; decode before passing a selection to a mount-scoped API. */
8
+ export function encodePlatformPickerPath(mountId: string, path = ''): string {
9
+ return JSON.stringify([mountId, path])
10
+ }
11
+ export function decodePlatformPickerPath(path: string): { mountId: string; path: string } {
12
+ const decoded: unknown = JSON.parse(path)
13
+ if (
14
+ !Array.isArray(decoded) ||
15
+ decoded.length !== 2 ||
16
+ !decoded.every((item) => typeof item === 'string') ||
17
+ !decoded[0]
18
+ ) {
19
+ throw new Error('Invalid platform picker path.')
20
+ }
21
+ return { mountId: decoded[0], path: decoded[1] }
22
+ }
23
+
24
+ /** Adapt the authenticated MINT mount API without exposing absolute host paths. */
25
+ export function usePlatformFilePickerAdapter(options: { apiBaseUrl?: string } = {}): PickerAdapter {
26
+ const api = useApi({
27
+ baseUrl: options.apiBaseUrl ?? getInjectedPlatformContext()?.platformApiUrl,
28
+ })
29
+ const mountLabels = new Map<string, string>()
30
+ async function listRoot(request?: PickerRequest): Promise<PickerNode[]> {
31
+ try {
32
+ const { mounts } = await api.get<{ mounts: ServerMount[] }>('/filesystem/mounts', {
33
+ signal: request?.signal,
34
+ })
35
+ return mounts.map((mount) => {
36
+ mountLabels.set(mount.id, mount.label)
37
+ return {
38
+ kind: 'folder',
39
+ path: encodePlatformPickerPath(mount.id),
40
+ name: mount.label,
41
+ parentPath: null,
42
+ parentName: 'Server mounts',
43
+ unavailableReason: mount.available === false ? 'This mount is offline.' : undefined,
44
+ }
45
+ })
46
+ } catch (cause) {
47
+ if ((cause as { response?: { status?: number } }).response?.status === 404) return []
48
+ throw cause
49
+ }
50
+ }
51
+ async function listChildren(path: string, request?: PickerRequest): Promise<PickerNode[]> {
52
+ const location = decodePlatformPickerPath(path)
53
+ const query = new URLSearchParams({ mount_id: location.mountId, path: location.path })
54
+ const listing = await api.get<FileDirectoryListing>(`/filesystem/browse?${query}`, {
55
+ signal: request?.signal,
56
+ })
57
+ if (listing.truncated)
58
+ throw new Error(
59
+ 'This directory exceeds the server listing limit. Choose a smaller data directory.',
60
+ )
61
+ return (listing.entries ?? []).map(
62
+ (entry) =>
63
+ ({
64
+ kind: entry.kind === 'directory' ? 'folder' : 'file',
65
+ path: encodePlatformPickerPath(location.mountId, entry.path),
66
+ parentPath: path,
67
+ parentName: `${mountLabels.get(location.mountId) ?? location.mountId}${location.path ? ` / ${location.path}` : ''}`,
68
+ name: entry.name,
69
+ bytes: entry.size_bytes,
70
+ ...(entry.kind === 'directory'
71
+ ? { hasChildren: entry.child_count !== 0 }
72
+ : { format: pickerFormat(entry.name) }),
73
+ }) as PickerNode,
74
+ )
75
+ }
76
+ return {
77
+ listRoot,
78
+ listChildren,
79
+ async search(query, scope, request) {
80
+ const matches: PickerNode[] = []
81
+ const queue = [scope.path]
82
+ const visited = new Set<string>()
83
+ while (queue.length) {
84
+ request?.signal?.throwIfAborted()
85
+ const path = queue.shift()!
86
+ if (visited.has(path)) continue
87
+ visited.add(path)
88
+ if (visited.size > 500)
89
+ throw new Error(
90
+ 'Search reached its folder limit. Search a smaller location or turn off Subfolders.',
91
+ )
92
+ const nodes = path ? await listChildren(path, request) : await listRoot(request)
93
+ for (const node of nodes) {
94
+ if (node.name.toLowerCase().includes(query.toLowerCase())) matches.push(node)
95
+ if (scope.recursive && node.kind === 'folder' && !node.unavailableReason)
96
+ queue.push(node.path)
97
+ }
98
+ if (matches.length > 10000)
99
+ throw new Error('Too many search results. Use a more specific search.')
100
+ }
101
+ return matches
102
+ },
103
+ }
104
+ }
@@ -423,6 +423,7 @@ export function createPluginClient<TClient extends object = GeneratedPluginClien
423
423
  const api = useApi({
424
424
  baseUrl: parts.baseUrl,
425
425
  withAuth: endpoint.requiresAuth !== false,
426
+ typedErrors: true,
426
427
  })
427
428
  const config = endpointRequestConfig(endpoint, parts.query, callOptions)
428
429
 
@@ -469,6 +470,7 @@ export async function uploadPluginEndpoint<TResponse = unknown>(
469
470
  const api = useApi({
470
471
  baseUrl: parts.baseUrl,
471
472
  withAuth: endpoint.requiresAuth !== false,
473
+ typedErrors: true,
472
474
  })
473
475
  const body = endpoint.hasBody
474
476
  ? requestBodyFromPayload(payload, parts.requestPayload, endpoint)
@@ -507,6 +509,7 @@ export async function downloadPluginEndpoint(
507
509
  const api = useApi({
508
510
  baseUrl: parts.baseUrl,
509
511
  withAuth: endpoint.requiresAuth !== false,
512
+ typedErrors: true,
510
513
  })
511
514
  const config = {
512
515
  ...(endpointRequestConfig(endpoint, parts.query, { signal: options.signal }) ?? {}),
@@ -167,7 +167,6 @@ export function usePluginWorkspace(options: UsePluginWorkspaceOptions): UsePlugi
167
167
  title: props.sidebarTitle,
168
168
  subtitle: props.sidebarSubtitle,
169
169
  badge: props.sidebarBadge,
170
- variant: props.sidebarVariant ?? 'analysis',
171
170
  panels: props.panels ?? {},
172
171
  activeView: navigationState.resolvedActiveView.value,
173
172
  floating: false,
@@ -187,7 +186,7 @@ export function usePluginWorkspace(options: UsePluginWorkspaceOptions): UsePlugi
187
186
  formDisabled: props.formDisabled ?? false,
188
187
  formReadonly: props.formReadonly ?? false,
189
188
  formSize: props.formSize ?? 'sm',
190
- collapsible: props.sidebarCollapsible,
189
+ collapsible: props.sidebarCollapsible ?? true,
191
190
  collapsed: props.sidebarCollapsed,
192
191
  defaultCollapsed: props.defaultSidebarCollapsed ?? false,
193
192
  collapsedWidth: props.sidebarCollapsedWidth ?? '3rem',
@@ -1,15 +1,31 @@
1
1
  import { ref } from 'vue'
2
- import type { Toast } from '../types'
2
+ import type { Toast, ToastOptions } from '../types'
3
3
 
4
4
  const toasts = ref<Toast[]>([])
5
5
  let nextId = 0
6
6
 
7
7
  /** Reactive global toast queue with success/error/warning/info variants and auto-dismiss timers. */
8
8
  export function useToast() {
9
- function show(message: string, type: Toast['type'] = 'success', duration = 3500) {
9
+ /** Push a structured toast. `progress: 'indeterminate'` is the in-progress form and never auto-dismisses. */
10
+ function push(options: ToastOptions): number {
10
11
  const id = nextId++
11
- toasts.value.push({ id, message, type, duration })
12
- setTimeout(() => dismiss(id), duration)
12
+ const duration = options.duration ?? 3500
13
+ toasts.value.push({
14
+ id,
15
+ type: 'success',
16
+ ...options,
17
+ message: options.message ?? options.title,
18
+ duration,
19
+ actions: options.actions?.slice(0, 2),
20
+ })
21
+ if (options.progress !== 'indeterminate') {
22
+ setTimeout(() => dismiss(id), duration)
23
+ }
24
+ return id
25
+ }
26
+
27
+ function show(message: string, type: Toast['type'] = 'success', duration = 3500) {
28
+ push({ title: message, type, duration })
13
29
  }
14
30
 
15
31
  function success(message: string, duration = 3500) {
@@ -36,5 +52,5 @@ export function useToast() {
36
52
  toasts.value = []
37
53
  }
38
54
 
39
- return { toasts, show, success, error, warning, info, dismiss, clear }
55
+ return { toasts, push, show, success, error, warning, info, dismiss, clear }
40
56
  }
@@ -38,8 +38,9 @@ export function generatedJobFormSchema(definition: GeneratedJobDefinition): Form
38
38
  export function generatedJobDefaults(definition: GeneratedJobDefinition): Record<string, unknown> {
39
39
  return Object.fromEntries(
40
40
  Object.entries(definition.input_schema.properties ?? {})
41
- .filter(([, schema]) => hasOwnDefault(schema))
42
- .map(([name, schema]) => [name, structuredClone(schema.default)]),
41
+ .map(([name, schema]) => [name, resolveGeneratedField(definition.input_schema, schema)] as const)
42
+ .filter(([, field]) => field.hasDefault)
43
+ .map(([name, field]) => [name, structuredClone(field.defaultValue)]),
43
44
  )
44
45
  }
45
46
 
@@ -51,7 +52,17 @@ export function normalizeGeneratedJobInput(
51
52
  for (const [name, value] of Object.entries(values)) {
52
53
  const field = definition.input_schema.properties?.[name]
53
54
  if (!field) continue
54
- normalized[name] = value
55
+ const { schema, reference } = resolveGeneratedField(definition.input_schema, field)
56
+ if (reference !== 'path' && Array.isArray(value) && ['integer', 'number'].includes(String(schema.items?.type))) {
57
+ normalized[name] = value.map(item => {
58
+ const numeric = typeof item === 'string' && item.trim() !== '' ? Number(item) : item
59
+ return typeof numeric === 'number' && Number.isFinite(numeric) ? numeric : item
60
+ })
61
+ } else if (reference !== 'path' && Array.isArray(value) && schema.items?.type === 'boolean') {
62
+ normalized[name] = value.map(item => item === 'true' ? true : item === 'false' ? false : item)
63
+ } else {
64
+ normalized[name] = value === undefined && allowsNull(schema) ? null : value
65
+ }
55
66
  }
56
67
  return normalized
57
68
  }
@@ -63,7 +74,12 @@ function toFormField(
63
74
  ): FormFieldSchema {
64
75
  const { schema, reference } = resolved
65
76
  const validation: NonNullable<FormFieldSchema['validation']> = {}
66
- if (required) validation.required = true
77
+ const allowsEmpty = schema.type === 'array' || (
78
+ schema.type === 'string' && reference !== 'path'
79
+ && !(schema.minLength && schema.minLength > 0)
80
+ && (!schema.enum || schema.enum.includes(''))
81
+ )
82
+ if (required && !allowsNull(schema) && !allowsEmpty) validation.required = true
67
83
  if (schema.minimum !== undefined) validation.min = schema.minimum
68
84
  if (schema.maximum !== undefined) validation.max = schema.maximum
69
85
  if (schema.minLength !== undefined) validation.minLength = schema.minLength
@@ -71,15 +87,37 @@ function toFormField(
71
87
 
72
88
  const props: Record<string, unknown> = {}
73
89
  if (schema.enum) props.options = [...schema.enum]
90
+ if (schema.type === 'integer') {
91
+ if (schema.exclusiveMinimum !== undefined) validation.min = Math.max(Number(validation.min ?? -Infinity), Math.floor(schema.exclusiveMinimum) + 1)
92
+ if (schema.exclusiveMaximum !== undefined) validation.max = Math.min(Number(validation.max ?? Infinity), Math.ceil(schema.exclusiveMaximum) - 1)
93
+ if (validation.min !== undefined) validation.min = Math.ceil(Number(validation.min))
94
+ if (validation.max !== undefined) validation.max = Math.floor(Number(validation.max))
95
+ if (schema.multipleOf && Number.isInteger(schema.multipleOf)) {
96
+ if (validation.min !== undefined) validation.min = Math.ceil(Number(validation.min) / schema.multipleOf) * schema.multipleOf
97
+ if (validation.max !== undefined) validation.max = Math.floor(Number(validation.max) / schema.multipleOf) * schema.multipleOf
98
+ }
99
+ props.step = 1
100
+ }
101
+ if (schema.type === 'integer' || schema.type === 'number') {
102
+ if (validation.min !== undefined) props.min = validation.min
103
+ if (validation.max !== undefined) props.max = validation.max
104
+ if (schema.multipleOf !== undefined) props.step = schema.multipleOf
105
+ }
106
+ if (schema.type === 'array' && reference !== 'path') {
107
+ if (schema.items?.enum) props.options = [...schema.items.enum]
108
+ else props.allowDuplicates = true
109
+ }
74
110
  if (reference === 'path' && schema.type === 'array') props.multiple = true
75
111
  return {
76
112
  name,
77
113
  label: schema.title || humanize(name),
78
114
  type: fieldType(schema, reference),
79
115
  defaultValue: resolved.hasDefault ? structuredClone(resolved.defaultValue) : undefined,
116
+ disabled: Object.prototype.hasOwnProperty.call(schema, 'const') || undefined,
80
117
  hint: schema.description,
81
118
  validation,
82
119
  props,
120
+ itemType: schema.items?.type === 'number' || schema.items?.type === 'integer' ? 'number' : undefined,
83
121
  }
84
122
  }
85
123
 
@@ -87,27 +125,24 @@ function resolveGeneratedField(
87
125
  root: GeneratedJsonSchema,
88
126
  field: GeneratedJsonSchema,
89
127
  ): ResolvedGeneratedField {
90
- const hasDefault = hasOwnDefault(field)
91
128
  const candidate = field.anyOf?.find(item => item.type !== 'null')
92
129
  ?? field.allOf?.[0]
93
130
  ?? field
131
+ let schema = { ...candidate, ...field }
94
132
  if (candidate.$ref) {
95
133
  const referenceParts = candidate.$ref.split('/')
96
134
  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
- }
135
+ const referenced = key ? root.$defs?.[key] : undefined
136
+ if (!referenced) throw new Error(`Generated field reference is missing: ${candidate.$ref}`)
137
+ schema = { ...referenced, ...candidate, ...field }
105
138
  }
139
+ if (schema.items) schema.items = resolveGeneratedField(root, schema.items).schema
140
+ const constant = Object.prototype.hasOwnProperty.call(schema, 'const')
106
141
  return {
107
- schema: candidate,
108
- reference: field['x-mint-component'] ?? candidate['x-mint-component'],
109
- hasDefault,
110
- defaultValue: field.default,
142
+ schema,
143
+ reference: schema['x-mint-component'],
144
+ hasDefault: constant || hasOwnDefault(field) || allowsNull(schema),
145
+ defaultValue: constant ? schema.const : hasOwnDefault(field) ? field.default : null,
111
146
  }
112
147
  }
113
148
 
@@ -117,7 +152,7 @@ function fieldType(schema: GeneratedJsonSchema, reference?: GeneratedReference):
117
152
  const type = Array.isArray(schema.type) ? schema.type.find(value => value !== 'null') : schema.type
118
153
  if (type === 'boolean') return 'toggle'
119
154
  if (type === 'number' || type === 'integer') return 'number'
120
- if (type === 'array') return 'tags'
155
+ if (type === 'array') return schema.items?.enum ? 'multiselect' : 'tags'
121
156
  return 'text'
122
157
  }
123
158
 
@@ -125,6 +160,11 @@ function hasOwnDefault(schema: GeneratedJsonSchema): boolean {
125
160
  return Object.prototype.hasOwnProperty.call(schema, 'default')
126
161
  }
127
162
 
163
+ function allowsNull(schema: GeneratedJsonSchema): boolean {
164
+ return schema.type === 'null' || (Array.isArray(schema.type) && schema.type.includes('null'))
165
+ || !!schema.anyOf?.some(item => item.type === 'null')
166
+ }
167
+
128
168
  function humanize(value: string): string {
129
169
  return value.replace(/[_-]+/g, ' ').replace(/^\w/, character => character.toUpperCase())
130
170
  }
package/src/index.ts CHANGED
@@ -1,6 +1,9 @@
1
1
  // Styles (import to ensure bundling)
2
2
  import './styles/index.css'
3
3
 
4
+ // Typed API errors
5
+ export * from './apiErrors'
6
+
4
7
  // Vue Plugin for global component registration
5
8
  export { MINTSdk, default } from './install'
6
9