@morscherlab/mint-sdk 1.1.0-beta.1 → 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 (94) hide show
  1. package/README.md +18 -4
  2. package/dist/{EmptyState-D1c4RdC7.js → EmptyState-DgBRoYAy.js} +22 -13
  3. package/dist/EmptyState-DgBRoYAy.js.map +1 -0
  4. package/dist/{ExperimentSelectorModal-B7w6k3Y8.js → ExperimentSelectorModal-BBWXC7KW.js} +3 -3
  5. package/dist/{ExperimentSelectorModal-B7w6k3Y8.js.map → ExperimentSelectorModal-BBWXC7KW.js.map} +1 -1
  6. package/dist/{SettingsModal-DPDwCbXK.js → SettingsModal-Dpg-X1OM.js} +3 -3
  7. package/dist/{SettingsModal-DPDwCbXK.js.map → SettingsModal-Dpg-X1OM.js.map} +1 -1
  8. package/dist/__tests__/components/BaseToggle.test.d.ts +1 -0
  9. package/dist/__tests__/components/SectionCard.test.d.ts +1 -0
  10. package/dist/__tests__/composables/analysisArtifactTarget.test.d.ts +1 -0
  11. package/dist/__tests__/types/GeneratedJsonSchema.test.d.ts +1 -0
  12. package/dist/analysisArtifactTypes-ejqR8MX3.js.map +1 -1
  13. package/dist/components/BaseSelect.vue.d.ts +1 -1
  14. package/dist/components/BaseToggle.vue.d.ts +2 -0
  15. package/dist/components/FitPanel.vue.d.ts +1 -1
  16. package/dist/components/PluginWorkspaceView.props.d.ts +1 -1
  17. package/dist/components/SectionCard.vue.d.ts +47 -0
  18. package/dist/components/index.d.ts +1 -0
  19. package/dist/components/index.js +6 -6
  20. package/dist/{components-UfkPN1bK.js → components-Ci_BTx00.js} +711 -633
  21. package/dist/components-Ci_BTx00.js.map +1 -0
  22. package/dist/composables/analysisArtifactTarget.d.ts +5 -0
  23. package/dist/composables/index.d.ts +3 -1
  24. package/dist/composables/index.js +8 -8
  25. package/dist/composables/platformContextHelpers.d.ts +2 -0
  26. package/dist/composables/useAnalysisArtifacts.d.ts +3 -1
  27. package/dist/composables/useGeneratedAnalysis.d.ts +1 -1
  28. package/dist/composables/usePlatformContext.d.ts +3 -0
  29. package/dist/composables/usePluginClient.d.ts +11 -5
  30. package/dist/{composables-F4JFK9K1.js → composables-C0VEJ73O.js} +172 -30
  31. package/dist/composables-C0VEJ73O.js.map +1 -0
  32. package/dist/index.js +11 -11
  33. package/dist/install.js +5 -5
  34. package/dist/styles.css +312 -11
  35. package/dist/tailwind.preset.d.ts +5 -0
  36. package/dist/tailwind.preset.js +7 -2
  37. package/dist/tailwind.preset.js.map +1 -1
  38. package/dist/templates/index.js +3 -3
  39. package/dist/{templates-Cttbjbyc.js → templates-Cdj93S9C.js} +2 -2
  40. package/dist/{templates-Cttbjbyc.js.map → templates-Cdj93S9C.js.map} +1 -1
  41. package/dist/types/analysisArtifactTypes.d.ts +8 -0
  42. package/dist/types/components.d.ts +2 -0
  43. package/dist/types/generated-ui.d.ts +13 -0
  44. package/dist/types/index.d.ts +2 -2
  45. package/dist/types/platform.d.ts +2 -0
  46. package/dist/{useControlSchema-Dallso-L.js → useControlSchema-B_qgU4rW.js} +48 -19
  47. package/dist/useControlSchema-B_qgU4rW.js.map +1 -0
  48. package/dist/{useExperimentSelector-BMjzNcf2.js → useExperimentSelector-BuQZRgMt.js} +2 -2
  49. package/dist/{useExperimentSelector-BMjzNcf2.js.map → useExperimentSelector-BuQZRgMt.js.map} +1 -1
  50. package/dist/{useFormBuilder-BXaaDI41.js → useFormBuilder-D1G5J5Z5.js} +2 -2
  51. package/dist/{useFormBuilder-BXaaDI41.js.map → useFormBuilder-D1G5J5Z5.js.map} +1 -1
  52. package/dist/{useProtocolTemplates-BP7_4RGQ.js → useProtocolTemplates-DeaOmMaw.js} +4 -4
  53. package/dist/{useProtocolTemplates-BP7_4RGQ.js.map → useProtocolTemplates-DeaOmMaw.js.map} +1 -1
  54. package/package.json +1 -1
  55. package/src/__tests__/components/BaseSelect.test.ts +52 -0
  56. package/src/__tests__/components/BaseToggle.test.ts +40 -0
  57. package/src/__tests__/components/GeneratedPluginView.test.ts +10 -0
  58. package/src/__tests__/components/PluginWorkspaceView.test.ts +68 -14
  59. package/src/__tests__/components/SectionCard.test.ts +181 -0
  60. package/src/__tests__/composables/analysisArtifactTarget.test.ts +77 -0
  61. package/src/__tests__/composables/useAnalysisArtifacts.test.ts +74 -1
  62. package/src/__tests__/composables/useGeneratedAnalysis.test.ts +2 -0
  63. package/src/__tests__/composables/usePlatformContext.test.ts +28 -0
  64. package/src/__tests__/composables/usePluginClient.test.ts +365 -21
  65. package/src/__tests__/composables/usePluginConfig.test.ts +323 -7
  66. package/src/__tests__/types/GeneratedJsonSchema.test.ts +62 -0
  67. package/src/components/BaseSelect.vue +16 -2
  68. package/src/components/BaseToggle.vue +3 -1
  69. package/src/components/GeneratedPluginView.vue +1 -1
  70. package/src/components/PluginWorkspaceView.props.ts +1 -1
  71. package/src/components/PluginWorkspaceView.vue +1 -5
  72. package/src/components/SectionCard.story.vue +163 -0
  73. package/src/components/SectionCard.vue +98 -0
  74. package/src/components/index.ts +1 -0
  75. package/src/composables/analysisArtifactTarget.ts +53 -0
  76. package/src/composables/index.ts +6 -0
  77. package/src/composables/platformContextHelpers.ts +10 -3
  78. package/src/composables/useAnalysisArtifacts.ts +48 -0
  79. package/src/composables/useGeneratedAnalysis.ts +30 -12
  80. package/src/composables/usePluginClient.ts +130 -20
  81. package/src/composables/usePluginConfig.ts +156 -20
  82. package/src/styles/components/section-card.css +161 -0
  83. package/src/styles/index.css +1 -0
  84. package/src/styles/variables.css +25 -0
  85. package/src/tailwind.preset.ts +5 -0
  86. package/src/types/analysisArtifactTypes.ts +9 -0
  87. package/src/types/components.ts +12 -0
  88. package/src/types/generated-ui.ts +13 -0
  89. package/src/types/index.ts +3 -0
  90. package/src/types/platform.ts +2 -0
  91. package/dist/EmptyState-D1c4RdC7.js.map +0 -1
  92. package/dist/components-UfkPN1bK.js.map +0 -1
  93. package/dist/composables-F4JFK9K1.js.map +0 -1
  94. package/dist/useControlSchema-Dallso-L.js.map +0 -1
@@ -0,0 +1,163 @@
1
+ <script setup lang="ts">
2
+ import SectionCard from './SectionCard.vue'
3
+ import type { SectionCardAccent, SectionCardDivider } from '../types'
4
+
5
+ const flaskIcon =
6
+ 'M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4'
7
+ const folderIcon = 'M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z'
8
+
9
+ const experiments = [
10
+ { code: 'MM-2026-014', name: '13C glucose tracing — HeLa, 24h', updated: '2h ago' },
11
+ { code: 'MM-2026-013', name: 'Hypoxia timecourse — passage 6', updated: '5h ago' },
12
+ { code: 'MM-2026-011', name: 'Glutamine dropout, triplicate', updated: 'yesterday' },
13
+ ]
14
+
15
+ function initPlayground() {
16
+ return {
17
+ title: 'Experiments',
18
+ count: '14 ongoing · 42 total',
19
+ accent: 'experiment' as SectionCardAccent,
20
+ uppercase: true,
21
+ divider: 'strong' as SectionCardDivider,
22
+ showIcon: true,
23
+ }
24
+ }
25
+ </script>
26
+
27
+ <template>
28
+ <Story title="Layout/SectionCard">
29
+ <Variant title="Playground" :init-state="initPlayground">
30
+ <template #default="{ state }">
31
+ <div style="padding: 2rem; max-width: 600px;">
32
+ <SectionCard
33
+ :title="state.title"
34
+ :count="state.count || undefined"
35
+ :icon="state.showIcon ? flaskIcon : undefined"
36
+ :accent="state.accent"
37
+ :uppercase="state.uppercase"
38
+ :divider="state.divider"
39
+ >
40
+ <div class="mint-section-card__rows">
41
+ <a
42
+ v-for="e in experiments"
43
+ :key="e.code"
44
+ href="#"
45
+ class="mint-section-card__row"
46
+ >
47
+ <span style="flex: 1; min-width: 0">
48
+ <span style="display: block; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">{{ e.name }}</span>
49
+ <span style="display: block; font-family: 'Fira Code', monospace; font-size: 0.65625rem; color: var(--text-muted)">{{ e.code }}</span>
50
+ </span>
51
+ <span style="flex-shrink: 0; font-size: 0.71875rem; color: var(--text-muted)">{{ e.updated }}</span>
52
+ </a>
53
+ </div>
54
+ </SectionCard>
55
+ </div>
56
+ </template>
57
+
58
+ <template #controls="{ state }">
59
+ <HstText v-model="state.title" title="Title" />
60
+ <HstText v-model="state.count" title="Count" />
61
+ <HstCheckbox v-model="state.showIcon" title="Show icon chip" />
62
+ <HstCheckbox v-model="state.uppercase" title="Uppercase title" />
63
+ <HstSelect
64
+ v-model="state.accent"
65
+ title="Accent"
66
+ :options="{
67
+ primary: 'primary',
68
+ experiment: 'experiment',
69
+ project: 'project',
70
+ success: 'success',
71
+ error: 'error',
72
+ warning: 'warning',
73
+ info: 'info',
74
+ }"
75
+ />
76
+ <HstSelect
77
+ v-model="state.divider"
78
+ title="Divider"
79
+ :options="{ strong: 'strong', light: 'light' }"
80
+ />
81
+ </template>
82
+ </Variant>
83
+
84
+ <Variant title="Home category card">
85
+ <div style="padding: 2rem; max-width: 600px;">
86
+ <SectionCard
87
+ title="Projects"
88
+ count="6 active · 9 total"
89
+ :icon="folderIcon"
90
+ accent="project"
91
+ uppercase
92
+ >
93
+ <template #actions>
94
+ <a href="#" style="font-size: 0.75rem; font-weight: 500; color: var(--color-primary-hover)">All →</a>
95
+ </template>
96
+ <div class="mint-section-card__rows">
97
+ <a href="#" class="mint-section-card__row">
98
+ <span style="flex: 1; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">Hypoxia metabolic rewiring</span>
99
+ <span style="font-size: 0.71875rem; color: var(--text-muted)">12 experiments</span>
100
+ </a>
101
+ <a href="#" class="mint-section-card__row">
102
+ <span style="flex: 1; font-size: 0.8125rem; font-weight: 500; color: var(--text-primary)">Serine one-carbon flux</span>
103
+ <span style="font-size: 0.71875rem; color: var(--text-muted)">7 experiments</span>
104
+ </a>
105
+ </div>
106
+ </SectionCard>
107
+ </div>
108
+ </Variant>
109
+
110
+ <Variant title="Detail card with footer">
111
+ <div style="padding: 2rem; max-width: 600px;">
112
+ <SectionCard title="Analysis artifacts" :count="4" divider="light">
113
+ <div class="mint-section-card__rows">
114
+ <div class="mint-section-card__row">
115
+ <span style="flex: 1; font-size: 0.8125rem; color: var(--text-primary)">isotopologue_matrix.csv</span>
116
+ <span style="font-size: 0.71875rem; color: var(--text-secondary)">DRP</span>
117
+ </div>
118
+ <div class="mint-section-card__row">
119
+ <span style="flex: 1; font-size: 0.8125rem; color: var(--text-primary)">natural_abundance_corrected.parquet</span>
120
+ <span style="font-size: 0.71875rem; color: var(--text-secondary)">DRP</span>
121
+ </div>
122
+ </div>
123
+ <template #footer>
124
+ <span style="font-size: 0.71875rem; color: var(--text-muted)">Showing 1–2 of 4</span>
125
+ <span style="font-size: 0.71875rem; color: var(--text-secondary)">1 / 2</span>
126
+ </template>
127
+ </SectionCard>
128
+ </div>
129
+ </Variant>
130
+
131
+ <Variant title="Fill with pinned toolbar">
132
+ <div style="padding: 2rem; height: 420px; display: flex;">
133
+ <SectionCard title="All experiments" :count="42" divider="light" fill>
134
+ <template #toolbar>
135
+ <div style="display: flex; gap: 0.5rem; padding: 0.6875rem 1rem; border-bottom: 1px solid var(--border-color)">
136
+ <span style="font-size: 0.78125rem; color: var(--text-secondary)">Toolbar stays put while the rows scroll</span>
137
+ </div>
138
+ </template>
139
+ <div class="mint-section-card__rows">
140
+ <div v-for="n in 20" :key="n" class="mint-section-card__row">
141
+ <span style="flex: 1; font-size: 0.8125rem; color: var(--text-primary)">Row {{ n }}</span>
142
+ <span style="font-family: 'Fira Code', monospace; font-size: 0.71875rem; color: var(--text-muted)">MM-2026-{{ String(n).padStart(3, '0') }}</span>
143
+ </div>
144
+ </div>
145
+ <template #footer>
146
+ <span style="font-size: 0.71875rem; color: var(--text-muted)">Showing 1–20 of 42</span>
147
+ <span style="font-size: 0.71875rem; color: var(--text-secondary)">1 / 3</span>
148
+ </template>
149
+ </SectionCard>
150
+ </div>
151
+ </Variant>
152
+
153
+ <Variant title="No icon, plain title">
154
+ <div style="padding: 2rem; max-width: 600px;">
155
+ <SectionCard title="Design data" count="96 samples · 4 timepoints" divider="light">
156
+ <div style="padding: 1rem; font-size: 0.8125rem; color: var(--text-secondary)">
157
+ Card body content sits directly on the card surface — no nested card.
158
+ </div>
159
+ </SectionCard>
160
+ </div>
161
+ </Variant>
162
+ </Story>
163
+ </template>
@@ -0,0 +1,98 @@
1
+ <script setup lang="ts">
2
+ /** One-card page region from the MINT 1.1 card system: 12px radius, the
3
+ * single --card-shadow token, a header strip (icon chip, title, mono count,
4
+ * actions), and hairline-separated content — no card-in-card. */
5
+ import { computed } from 'vue'
6
+ import type { SectionCardAccent, SectionCardDivider } from '../types'
7
+
8
+ interface Props {
9
+ title: string
10
+ /** Monospace meta shown next to the title, e.g. "14 ongoing · 42 total". */
11
+ count?: string | number
12
+ /** SVG path(s) for the 22px icon chip; omit to render no chip. */
13
+ icon?: string | string[]
14
+ /** Tint of the icon chip. */
15
+ accent?: SectionCardAccent
16
+ /** Uppercase category heading (home cards) vs plain heading (detail cards). */
17
+ uppercase?: boolean
18
+ /** Header hairline: 'strong' = --border-color, 'light' = --border-light. */
19
+ divider?: SectionCardDivider
20
+ /**
21
+ * Stretch to fill the height its parent offers, scrolling the body and
22
+ * pinning the header and footer. Requires a parent that gives the card a
23
+ * definite height (a flex or grid track).
24
+ */
25
+ fill?: boolean
26
+ }
27
+
28
+ const props = withDefaults(defineProps<Props>(), {
29
+ count: undefined,
30
+ icon: undefined,
31
+ accent: 'primary',
32
+ uppercase: false,
33
+ divider: 'strong',
34
+ fill: false,
35
+ })
36
+
37
+ const iconPaths = computed(() =>
38
+ props.icon === undefined ? [] : Array.isArray(props.icon) ? props.icon : [props.icon],
39
+ )
40
+
41
+ const headerClasses = computed(() => [
42
+ 'mint-section-card__header',
43
+ props.divider === 'light' ? 'mint-section-card__header--divider-light' : '',
44
+ ])
45
+
46
+ const titleClasses = computed(() => [
47
+ 'mint-section-card__title',
48
+ props.uppercase ? 'mint-section-card__title--uppercase' : '',
49
+ ])
50
+
51
+ const cardClasses = computed(() => [
52
+ 'mint-section-card',
53
+ props.fill ? 'mint-section-card--fill' : '',
54
+ ])
55
+ </script>
56
+
57
+ <template>
58
+ <section :class="cardClasses">
59
+ <div :class="headerClasses">
60
+ <span
61
+ v-if="iconPaths.length"
62
+ :class="['mint-section-card__icon-chip', `mint-section-card__icon-chip--${accent}`]"
63
+ >
64
+ <svg
65
+ class="mint-section-card__icon"
66
+ viewBox="0 0 24 24"
67
+ fill="none"
68
+ stroke="currentColor"
69
+ stroke-width="2"
70
+ stroke-linecap="round"
71
+ stroke-linejoin="round"
72
+ aria-hidden="true"
73
+ >
74
+ <path v-for="(d, i) in iconPaths" :key="i" :d="d" />
75
+ </svg>
76
+ </span>
77
+ <h2 :class="titleClasses">{{ title }}</h2>
78
+ <span v-if="count !== undefined" class="mint-section-card__count">{{ count }}</span>
79
+ <div class="mint-section-card__spacer"></div>
80
+ <slot name="actions" />
81
+ </div>
82
+ <!-- Controls belong to the card's chrome, so they sit outside the body and
83
+ stay put while the body scrolls. -->
84
+ <div v-if="$slots.toolbar" class="mint-section-card__toolbar">
85
+ <slot name="toolbar" />
86
+ </div>
87
+ <div class="mint-section-card__body">
88
+ <slot />
89
+ </div>
90
+ <div v-if="$slots.footer" class="mint-section-card__footer">
91
+ <slot name="footer" />
92
+ </div>
93
+ </section>
94
+ </template>
95
+
96
+ <style>
97
+ @import '../styles/components/section-card.css';
98
+ </style>
@@ -126,6 +126,7 @@ export { default as DateTimePicker } from './DateTimePicker.vue'
126
126
  export { default as TimeRangeInput } from './TimeRangeInput.vue'
127
127
  export { default as ScheduleCalendar } from './ScheduleCalendar.vue'
128
128
  export { default as ResourceCard } from './ResourceCard.vue'
129
+ export { default as SectionCard } from './SectionCard.vue'
129
130
 
130
131
  // Experiment / analysis components
131
132
  export { default as ExperimentSelectorModal } from './ExperimentSelectorModal.vue'
@@ -0,0 +1,53 @@
1
+ import type { AnalysisArtifactTarget } from '../types/analysisArtifactTypes'
2
+
3
+ const EXPERIMENT_ID = 'experiment_id'
4
+ const ARTIFACT_PLUGIN_ID = 'artifact_plugin_id'
5
+ const ARTIFACT_KEY = 'artifact_key'
6
+ const ARTIFACT_ID = 'artifact_id'
7
+
8
+ function parsePositiveInteger(value: string | null): number | undefined {
9
+ if (value === null || !/^\d+$/.test(value)) return undefined
10
+ const parsed = Number(value)
11
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined
12
+ }
13
+
14
+ /** Build the canonical query used to open one analysis artifact in a plugin. */
15
+ export function buildAnalysisArtifactTargetQuery(
16
+ target: AnalysisArtifactTarget,
17
+ ): URLSearchParams {
18
+ const query = new URLSearchParams()
19
+ query.set(EXPERIMENT_ID, String(target.experimentId))
20
+ query.set(ARTIFACT_PLUGIN_ID, target.artifactPluginId)
21
+ query.set(ARTIFACT_KEY, target.artifactKey)
22
+ if (target.artifactId !== undefined) {
23
+ query.set(ARTIFACT_ID, String(target.artifactId))
24
+ }
25
+ return query
26
+ }
27
+
28
+ /** Parse a complete canonical target. Incomplete or invalid input is not guessed. */
29
+ export function parseAnalysisArtifactTarget(
30
+ query: URLSearchParams,
31
+ ): AnalysisArtifactTarget | null {
32
+ const experimentId = parsePositiveInteger(query.get(EXPERIMENT_ID))
33
+ const artifactPluginId = query.get(ARTIFACT_PLUGIN_ID)?.trim()
34
+ const artifactKey = query.get(ARTIFACT_KEY)?.trim()
35
+ const rawArtifactId = query.get(ARTIFACT_ID)
36
+ const artifactId = parsePositiveInteger(rawArtifactId)
37
+
38
+ if (
39
+ experimentId === undefined
40
+ || !artifactPluginId
41
+ || !artifactKey
42
+ || (rawArtifactId !== null && artifactId === undefined)
43
+ ) {
44
+ return null
45
+ }
46
+
47
+ return {
48
+ experimentId,
49
+ artifactPluginId,
50
+ artifactKey,
51
+ ...(artifactId === undefined ? {} : { artifactId }),
52
+ }
53
+ }
@@ -159,6 +159,11 @@ export {
159
159
  type UseAnalysisArtifactsOptions,
160
160
  type UseAnalysisArtifactsReturn,
161
161
  } from './useAnalysisArtifacts'
162
+ export {
163
+ buildAnalysisArtifactTargetQuery,
164
+ parseAnalysisArtifactTarget,
165
+ } from './analysisArtifactTarget'
166
+ export { resolveCurrentPluginId } from './platformContextHelpers'
162
167
  export {
163
168
  formatExperimentDate,
164
169
  formatExperimentStatus,
@@ -476,6 +481,7 @@ export {
476
481
  type DownloadBlobOptions,
477
482
  type DownloadPluginEndpointOptions,
478
483
  type PluginContract,
484
+ type PluginContractSettings,
479
485
  type PluginClientRequestOptions,
480
486
  type PluginEndpointContract,
481
487
  type PluginEndpointDefinition,
@@ -12,6 +12,12 @@ export function getInjectedPlatformContext(): PlatformContext | undefined {
12
12
  return (window as unknown as { __MINT_PLATFORM__?: PlatformContext }).__MINT_PLATFORM__
13
13
  }
14
14
 
15
+ /** Return the platform's canonical plugin identity for the mounted frontend. */
16
+ export function resolveCurrentPluginId(): string | undefined {
17
+ const pluginId = getInjectedPlatformContext()?.plugin?.id
18
+ return typeof pluginId === 'string' && pluginId ? pluginId : undefined
19
+ }
20
+
15
21
  export function getInjectedExperimentContext(): ExperimentAwarePlatformContext | undefined {
16
22
  return getInjectedPlatformContext() as ExperimentAwarePlatformContext | undefined
17
23
  }
@@ -38,10 +44,11 @@ export function currentExperimentIdFromContext(
38
44
 
39
45
  export function currentExperimentIdFromUrl(): number | undefined {
40
46
  if (typeof window === 'undefined') return undefined
41
- const params = new URLSearchParams(window.location.search)
47
+ const location = window.location
48
+ const params = new URLSearchParams(location?.search ?? '')
42
49
  return parseExperimentId(params.get('experimentId') ?? params.get('experiment_id'))
43
- ?? experimentIdFromPath(window.location.pathname)
44
- ?? experimentIdFromPath(window.location.hash.replace(/^#\/?/, '/'))
50
+ ?? experimentIdFromPath(location?.pathname ?? '')
51
+ ?? experimentIdFromPath((location?.hash ?? '').replace(/^#\/?/, '/'))
45
52
  }
46
53
 
47
54
  export function resolveCurrentExperimentId(
@@ -19,6 +19,7 @@ import type {
19
19
  AnalysisArtifactMetadataUpdate,
20
20
  AnalysisArtifactStatusFilter,
21
21
  AnalysisArtifactSummary,
22
+ AnalysisArtifactTarget,
22
23
  } from '../types/analysisArtifactTypes'
23
24
 
24
25
  export { ANALYSIS_FILE_ARTIFACT_SCHEMA } from '../types/analysisArtifactTypes'
@@ -67,6 +68,8 @@ export interface UseAnalysisArtifactsReturn {
67
68
  refresh: () => Promise<AnalysisArtifactSummary[]>
68
69
  /** Fetch one artifact with its result payload; null when it fails or is superseded. */
69
70
  getDetail: (artifactId: number) => Promise<AnalysisArtifactDetail | null>
71
+ /** Resolve and fetch the exact stable artifact identity. */
72
+ resolveTarget: (target: AnalysisArtifactTarget) => Promise<AnalysisArtifactDetail | null>
70
73
  /** Update display_name and/or note; resolves to the updated summary or null. */
71
74
  updateMetadata: (
72
75
  artifactId: number,
@@ -206,6 +209,50 @@ export function useAnalysisArtifacts(
206
209
  }
207
210
  }
208
211
 
212
+ async function resolveTarget(
213
+ target: AnalysisArtifactTarget,
214
+ ): Promise<AnalysisArtifactDetail | null> {
215
+ const experimentId = currentExperimentId.value
216
+ if (experimentId !== target.experimentId) {
217
+ request.setError(
218
+ `Analysis artifact target belongs to experiment ${target.experimentId}, not `
219
+ + `${experimentId ?? 'the current selection'}.`,
220
+ )
221
+ return null
222
+ }
223
+
224
+ const available = await list()
225
+ const summary = available.find((artifact) => (
226
+ artifact.plugin_id === target.artifactPluginId
227
+ && artifact.artifact_key === target.artifactKey
228
+ && artifact.status === 'active'
229
+ ))
230
+ if (!summary) {
231
+ request.setError(
232
+ `Analysis artifact ${target.artifactPluginId}/${target.artifactKey} is not available.`,
233
+ )
234
+ return null
235
+ }
236
+
237
+ const detail = await getDetail(summary.id)
238
+ if (
239
+ detail
240
+ && (
241
+ detail.experiment_id !== target.experimentId
242
+ || detail.plugin_id !== target.artifactPluginId
243
+ || detail.artifact_key !== target.artifactKey
244
+ )
245
+ ) {
246
+ selectedDetail.value = null
247
+ request.setError(
248
+ `Analysis artifact ${target.artifactPluginId}/${target.artifactKey} did not `
249
+ + 'match the requested identity.',
250
+ )
251
+ return null
252
+ }
253
+ return detail
254
+ }
255
+
209
256
  async function mutate(
210
257
  action: string,
211
258
  operation: (experimentId: number) => Promise<AnalysisArtifactSummary>,
@@ -296,6 +343,7 @@ export function useAnalysisArtifacts(
296
343
  list,
297
344
  refresh: list,
298
345
  getDetail,
346
+ resolveTarget,
299
347
  updateMetadata,
300
348
  archive,
301
349
  restore,
@@ -75,7 +75,10 @@ export interface UsePluginJobsReturn extends PluginJobCenterSource {
75
75
  status: ShallowRef<'idle' | 'initializing' | 'ready' | 'paused' | 'error' | 'disposed'>
76
76
  initialize: () => Promise<void>
77
77
  ready: () => Promise<void>
78
- uploadPath: (input: PluginJobPathInput) => Promise<PluginJobPathUpload>
78
+ uploadPath: (
79
+ input: PluginJobPathInput,
80
+ definitionId?: string,
81
+ ) => Promise<PluginJobPathUpload>
79
82
  uploadStagedPart: <Metadata>(
80
83
  input: PluginJobStagedPartInput<Metadata>,
81
84
  ) => Promise<PluginJobStagedPartUploadResult<Metadata>>
@@ -279,7 +282,7 @@ function createPluginJobs(
279
282
  jobInput = await submitToSession(
280
283
  session.value.id,
281
284
  clientRequestId,
282
- await stagePathInputs(inputs, session.value.id),
285
+ await stagePathInputs(inputs, session.value.id, definitionId),
283
286
  )
284
287
  } catch (cause) {
285
288
  if (!isSessionNotFoundError(cause)) throw cause
@@ -287,7 +290,7 @@ function createPluginJobs(
287
290
  jobInput = await submitToSession(
288
291
  session.value.id,
289
292
  clientRequestId,
290
- await stagePathInputs(inputs, session.value.id),
293
+ await stagePathInputs(inputs, session.value.id, definitionId),
291
294
  )
292
295
  }
293
296
  const job = upsertJob(jobInput, definitionId)
@@ -317,9 +320,12 @@ function createPluginJobs(
317
320
  }
318
321
  }
319
322
 
320
- async function uploadPath(input: PluginJobPathInput): Promise<PluginJobPathUpload> {
323
+ async function uploadPath(
324
+ input: PluginJobPathInput,
325
+ definitionId?: string,
326
+ ): Promise<PluginJobPathUpload> {
321
327
  if (!session.value) throw new Error('Generated analysis session is not initialized')
322
- return uploadPathForSession(input, session.value.id)
328
+ return uploadPathForSession(input, session.value.id, definitionId)
323
329
  }
324
330
 
325
331
  async function uploadStagedPart<Metadata>(
@@ -375,6 +381,7 @@ function createPluginJobs(
375
381
  async function uploadPathForSession(
376
382
  input: PluginJobPathInput,
377
383
  sessionId: string,
384
+ definitionId?: string,
378
385
  ): Promise<PluginJobPathUpload> {
379
386
  if (!input.files.length || input.files.length !== input.relativePaths.length) {
380
387
  throw new Error('Path upload requires one relative path for each file')
@@ -386,40 +393,51 @@ function createPluginJobs(
386
393
  body.append('files', input.files[index]!)
387
394
  body.append('relative_paths', input.relativePaths[index]!)
388
395
  }
396
+ const headers: Record<string, string> = {
397
+ 'X-Mint-Job-Session': sessionId,
398
+ }
399
+ if (definitionId) headers['X-Mint-Job-Definition'] = definitionId
389
400
  return request<PluginJobPathUpload>('/jobs/uploads', {
390
401
  method: 'POST',
391
402
  body,
392
403
  signal: input.signal,
393
- headers: { 'X-Mint-Job-Session': sessionId },
404
+ headers,
394
405
  })
395
406
  }
396
407
 
397
408
  async function stagePathInputs(
398
409
  inputs: Record<string, unknown>,
399
410
  sessionId: string,
411
+ definitionId: string,
400
412
  ): Promise<Record<string, unknown>> {
401
- return await stagePathValue(inputs, sessionId) as Record<string, unknown>
413
+ return await stagePathValue(inputs, sessionId, definitionId) as Record<string, unknown>
402
414
  }
403
415
 
404
- async function stagePathValue(value: unknown, sessionId: string): Promise<unknown> {
416
+ async function stagePathValue(
417
+ value: unknown,
418
+ sessionId: string,
419
+ definitionId: string,
420
+ ): Promise<unknown> {
405
421
  if (typeof File !== 'undefined' && value instanceof File) {
406
422
  const upload = await uploadPathForSession({
407
423
  kind: 'path',
408
424
  files: [value],
409
425
  relativePaths: [value.name],
410
- }, sessionId)
426
+ }, sessionId, definitionId)
411
427
  return { token: upload.token }
412
428
  }
413
429
  if (isPluginJobPathInput(value)) {
414
- const upload = await uploadPathForSession(value, sessionId)
430
+ const upload = await uploadPathForSession(value, sessionId, definitionId)
415
431
  return { token: upload.token }
416
432
  }
417
433
  if (Array.isArray(value)) {
418
- return await Promise.all(value.map(item => stagePathValue(item, sessionId)))
434
+ return await Promise.all(value.map(
435
+ item => stagePathValue(item, sessionId, definitionId),
436
+ ))
419
437
  }
420
438
  if (value && typeof value === 'object') {
421
439
  const entries = await Promise.all(Object.entries(value).map(async ([key, child]) => (
422
- [key, await stagePathValue(child, sessionId)] as const
440
+ [key, await stagePathValue(child, sessionId, definitionId)] as const
423
441
  )))
424
442
  return Object.fromEntries(entries)
425
443
  }