@morscherlab/mint-sdk 1.1.0-beta.3 → 1.1.0-beta.5
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.
- package/dist/{EmptyState-D1c4RdC7.js → EmptyState-DgBRoYAy.js} +22 -13
- package/dist/EmptyState-DgBRoYAy.js.map +1 -0
- package/dist/{ExperimentSelectorModal-B7w6k3Y8.js → ExperimentSelectorModal-BBWXC7KW.js} +3 -3
- package/dist/{ExperimentSelectorModal-B7w6k3Y8.js.map → ExperimentSelectorModal-BBWXC7KW.js.map} +1 -1
- package/dist/{SettingsModal-DPDwCbXK.js → SettingsModal-DDaq294M.js} +3 -3
- package/dist/{SettingsModal-DPDwCbXK.js.map → SettingsModal-DDaq294M.js.map} +1 -1
- package/dist/__tests__/components/BaseToggle.test.d.ts +1 -0
- package/dist/__tests__/composables/analysisArtifactTarget.test.d.ts +1 -0
- package/dist/analysisArtifactTypes-ejqR8MX3.js.map +1 -1
- package/dist/components/BaseSelect.vue.d.ts +1 -1
- package/dist/components/BaseToggle.vue.d.ts +2 -0
- package/dist/components/FitPanel.vue.d.ts +1 -1
- package/dist/components/index.js +5 -5
- package/dist/{components-DFH5whuh.js → components-t0iBwn5R.js} +10 -10
- package/dist/{components-DFH5whuh.js.map → components-t0iBwn5R.js.map} +1 -1
- package/dist/composables/analysisArtifactTarget.d.ts +5 -0
- package/dist/composables/index.d.ts +2 -0
- package/dist/composables/index.js +8 -8
- package/dist/composables/platformContextHelpers.d.ts +2 -0
- package/dist/composables/useAnalysisArtifacts.d.ts +3 -1
- package/dist/{composables-B512bWJP.js → composables-CqjA4Gth.js} +40 -5
- package/dist/composables-CqjA4Gth.js.map +1 -0
- package/dist/index.js +11 -11
- package/dist/install.js +5 -5
- package/dist/templates/index.js +3 -3
- package/dist/{templates-Cttbjbyc.js → templates-BdArs8LL.js} +2 -2
- package/dist/{templates-Cttbjbyc.js.map → templates-BdArs8LL.js.map} +1 -1
- package/dist/types/analysisArtifactTypes.d.ts +8 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/{useControlSchema-Dallso-L.js → useControlSchema-BAqu3kqE.js} +33 -6
- package/dist/useControlSchema-BAqu3kqE.js.map +1 -0
- package/dist/{useExperimentSelector-BMjzNcf2.js → useExperimentSelector-BuQZRgMt.js} +2 -2
- package/dist/{useExperimentSelector-BMjzNcf2.js.map → useExperimentSelector-BuQZRgMt.js.map} +1 -1
- package/dist/{useFormBuilder-BXaaDI41.js → useFormBuilder-DvO-TrfX.js} +2 -2
- package/dist/{useFormBuilder-BXaaDI41.js.map → useFormBuilder-DvO-TrfX.js.map} +1 -1
- package/dist/{useProtocolTemplates-BP7_4RGQ.js → useProtocolTemplates-BuY_MlBP.js} +4 -4
- package/dist/{useProtocolTemplates-BP7_4RGQ.js.map → useProtocolTemplates-BuY_MlBP.js.map} +1 -1
- package/package.json +1 -1
- package/src/__tests__/components/BaseSelect.test.ts +52 -0
- package/src/__tests__/components/BaseToggle.test.ts +40 -0
- package/src/__tests__/composables/analysisArtifactTarget.test.ts +77 -0
- package/src/__tests__/composables/useAnalysisArtifacts.test.ts +74 -1
- package/src/components/BaseSelect.vue +16 -2
- package/src/components/BaseToggle.vue +3 -1
- package/src/composables/analysisArtifactTarget.ts +53 -0
- package/src/composables/index.ts +5 -0
- package/src/composables/platformContextHelpers.ts +10 -3
- package/src/composables/useAnalysisArtifacts.ts +48 -0
- package/src/types/analysisArtifactTypes.ts +9 -0
- package/src/types/index.ts +1 -0
- package/dist/EmptyState-D1c4RdC7.js.map +0 -1
- package/dist/composables-B512bWJP.js.map +0 -1
- package/dist/useControlSchema-Dallso-L.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morscherlab/mint-sdk",
|
|
3
|
-
"version": "1.1.0-beta.
|
|
3
|
+
"version": "1.1.0-beta.5",
|
|
4
4
|
"description": "MINT Platform SDK — Vue 3 components, composables, and types for plugin development. MINT = Mass-spec INtegrated Toolkit.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -3,6 +3,58 @@ import { describe, expect, it } from 'vitest'
|
|
|
3
3
|
import BaseSelect from '../../components/BaseSelect.vue'
|
|
4
4
|
|
|
5
5
|
describe('BaseSelect', () => {
|
|
6
|
+
it('forwards native attributes to the select instead of the wrapper', () => {
|
|
7
|
+
const wrapper = mount(BaseSelect, {
|
|
8
|
+
props: {
|
|
9
|
+
options: ['linear'],
|
|
10
|
+
},
|
|
11
|
+
attrs: {
|
|
12
|
+
id: 'curve-model',
|
|
13
|
+
name: 'curve-model',
|
|
14
|
+
'aria-label': 'Curve model',
|
|
15
|
+
'data-testid': 'curve-model-select',
|
|
16
|
+
class: 'curve-model-layout',
|
|
17
|
+
style: 'width: 12rem;',
|
|
18
|
+
},
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
const select = wrapper.get('select')
|
|
22
|
+
|
|
23
|
+
expect({
|
|
24
|
+
select: {
|
|
25
|
+
id: select.attributes('id'),
|
|
26
|
+
name: select.attributes('name'),
|
|
27
|
+
ariaLabel: select.attributes('aria-label'),
|
|
28
|
+
testId: select.attributes('data-testid'),
|
|
29
|
+
layoutClass: select.classes().includes('curve-model-layout'),
|
|
30
|
+
},
|
|
31
|
+
wrapper: {
|
|
32
|
+
id: wrapper.attributes('id'),
|
|
33
|
+
name: wrapper.attributes('name'),
|
|
34
|
+
ariaLabel: wrapper.attributes('aria-label'),
|
|
35
|
+
testId: wrapper.attributes('data-testid'),
|
|
36
|
+
layoutClass: wrapper.classes().includes('curve-model-layout'),
|
|
37
|
+
style: wrapper.attributes('style'),
|
|
38
|
+
},
|
|
39
|
+
}).toEqual({
|
|
40
|
+
select: {
|
|
41
|
+
id: 'curve-model',
|
|
42
|
+
name: 'curve-model',
|
|
43
|
+
ariaLabel: 'Curve model',
|
|
44
|
+
testId: 'curve-model-select',
|
|
45
|
+
layoutClass: false,
|
|
46
|
+
},
|
|
47
|
+
wrapper: {
|
|
48
|
+
id: undefined,
|
|
49
|
+
name: undefined,
|
|
50
|
+
ariaLabel: undefined,
|
|
51
|
+
testId: undefined,
|
|
52
|
+
layoutClass: true,
|
|
53
|
+
style: 'width: 12rem;',
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
6
58
|
it('accepts string and number shorthand options', async () => {
|
|
7
59
|
const wrapper = mount(BaseSelect, {
|
|
8
60
|
props: {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { mount } from '@vue/test-utils'
|
|
2
|
+
import { describe, expect, it } from 'vitest'
|
|
3
|
+
import BaseToggle from '../../components/BaseToggle.vue'
|
|
4
|
+
|
|
5
|
+
describe('BaseToggle', () => {
|
|
6
|
+
it('uses ariaLabel for the switch name without changing the visual label', () => {
|
|
7
|
+
const wrapper = mount(BaseToggle, {
|
|
8
|
+
props: {
|
|
9
|
+
label: 'Enabled',
|
|
10
|
+
ariaLabel: 'Email notifications',
|
|
11
|
+
},
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
expect({
|
|
15
|
+
accessibleName: wrapper.get('[role="switch"]').attributes('aria-label'),
|
|
16
|
+
visualText: wrapper.text(),
|
|
17
|
+
wrapperAriaLabel: wrapper.attributes('aria-label'),
|
|
18
|
+
}).toEqual({
|
|
19
|
+
accessibleName: 'Email notifications',
|
|
20
|
+
visualText: 'Enabled',
|
|
21
|
+
wrapperAriaLabel: undefined,
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('keeps using the visual label as the default switch name', () => {
|
|
26
|
+
const wrapper = mount(BaseToggle, {
|
|
27
|
+
props: {
|
|
28
|
+
label: 'Enabled',
|
|
29
|
+
},
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
expect(wrapper.get('[role="switch"]').attributes('aria-label')).toBe('Enabled')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('keeps the existing fallback name when no labels are provided', () => {
|
|
36
|
+
const wrapper = mount(BaseToggle)
|
|
37
|
+
|
|
38
|
+
expect(wrapper.get('[role="switch"]').attributes('aria-label')).toBe('Toggle')
|
|
39
|
+
})
|
|
40
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
buildAnalysisArtifactTargetQuery,
|
|
5
|
+
parseAnalysisArtifactTarget,
|
|
6
|
+
} from '../../composables/analysisArtifactTarget'
|
|
7
|
+
import {
|
|
8
|
+
currentExperimentIdFromUrl,
|
|
9
|
+
resolveCurrentPluginId,
|
|
10
|
+
} from '../../composables/platformContextHelpers'
|
|
11
|
+
|
|
12
|
+
describe('analysis artifact target', () => {
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
delete (window as unknown as { __MINT_PLATFORM__?: unknown }).__MINT_PLATFORM__
|
|
15
|
+
vi.unstubAllGlobals()
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should resolve the canonical injected plugin id', () => {
|
|
19
|
+
;(window as unknown as { __MINT_PLATFORM__: unknown }).__MINT_PLATFORM__ = {
|
|
20
|
+
isIntegrated: true,
|
|
21
|
+
plugin: { id: 'LEAF', name: 'Leaf UI' },
|
|
22
|
+
theme: 'system',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
expect(resolveCurrentPluginId()).toBe('LEAF')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should tolerate a host that provides a partial location object', () => {
|
|
29
|
+
vi.stubGlobal('window', {
|
|
30
|
+
location: { search: '' },
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
expect(currentExperimentIdFromUrl()).toBeUndefined()
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('should build the canonical readable query when an artifact id hint is present', () => {
|
|
37
|
+
const query = buildAnalysisArtifactTargetQuery({
|
|
38
|
+
experimentId: 66,
|
|
39
|
+
artifactPluginId: 'LEAF',
|
|
40
|
+
artifactKey: 'exp316_260703_ys_rna_ide_scr',
|
|
41
|
+
artifactId: 206,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
expect(query.toString()).toBe(
|
|
45
|
+
'experiment_id=66&artifact_plugin_id=LEAF'
|
|
46
|
+
+ '&artifact_key=exp316_260703_ys_rna_ide_scr&artifact_id=206',
|
|
47
|
+
)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('should parse the canonical identity without requiring an artifact id hint', () => {
|
|
51
|
+
const target = parseAnalysisArtifactTarget(new URLSearchParams(
|
|
52
|
+
'experiment_id=66&artifact_plugin_id=LEAF&artifact_key=result',
|
|
53
|
+
))
|
|
54
|
+
|
|
55
|
+
expect(target).toEqual({
|
|
56
|
+
experimentId: 66,
|
|
57
|
+
artifactPluginId: 'LEAF',
|
|
58
|
+
artifactKey: 'result',
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should reject an incomplete target instead of guessing another artifact', () => {
|
|
63
|
+
const target = parseAnalysisArtifactTarget(new URLSearchParams(
|
|
64
|
+
'experiment_id=66&artifact_key=result',
|
|
65
|
+
))
|
|
66
|
+
|
|
67
|
+
expect(target).toBeNull()
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should reject non-positive numeric identifiers', () => {
|
|
71
|
+
const target = parseAnalysisArtifactTarget(new URLSearchParams(
|
|
72
|
+
'experiment_id=0&artifact_plugin_id=LEAF&artifact_key=result&artifact_id=-1',
|
|
73
|
+
))
|
|
74
|
+
|
|
75
|
+
expect(target).toBeNull()
|
|
76
|
+
})
|
|
77
|
+
})
|
|
@@ -19,7 +19,10 @@ vi.mock('../../composables/useApi', () => ({
|
|
|
19
19
|
}))
|
|
20
20
|
|
|
21
21
|
import { useAnalysisArtifacts } from '../../composables/useAnalysisArtifacts'
|
|
22
|
-
import type {
|
|
22
|
+
import type {
|
|
23
|
+
AnalysisArtifactDetail,
|
|
24
|
+
AnalysisArtifactSummary,
|
|
25
|
+
} from '../../types/analysisArtifactTypes'
|
|
23
26
|
|
|
24
27
|
// ---------------------------------------------------------------------------
|
|
25
28
|
// Fixtures
|
|
@@ -177,6 +180,76 @@ describe('useAnalysisArtifacts', () => {
|
|
|
177
180
|
})
|
|
178
181
|
})
|
|
179
182
|
|
|
183
|
+
describe('resolveTarget', () => {
|
|
184
|
+
it('should resolve the exact stable identity when the numeric hint is stale', async () => {
|
|
185
|
+
const detail = {
|
|
186
|
+
...makeArtifact({
|
|
187
|
+
id: 2,
|
|
188
|
+
experiment_id: 42,
|
|
189
|
+
plugin_id: 'plugin-b',
|
|
190
|
+
artifact_key: 'peaks',
|
|
191
|
+
}),
|
|
192
|
+
result: { schema_version: 'mint.analysis_file.v1' },
|
|
193
|
+
tree: [],
|
|
194
|
+
summary: null,
|
|
195
|
+
} satisfies AnalysisArtifactDetail
|
|
196
|
+
mockGet
|
|
197
|
+
.mockResolvedValueOnce({ artifacts: ARTIFACTS })
|
|
198
|
+
.mockResolvedValueOnce(detail)
|
|
199
|
+
const { resolveTarget } = setup()
|
|
200
|
+
|
|
201
|
+
const resolved = await resolveTarget({
|
|
202
|
+
experimentId: 42,
|
|
203
|
+
artifactPluginId: 'plugin-b',
|
|
204
|
+
artifactKey: 'peaks',
|
|
205
|
+
artifactId: 999,
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
expect({
|
|
209
|
+
resolvedId: resolved?.id,
|
|
210
|
+
detailPath: mockGet.mock.calls[1]?.[0],
|
|
211
|
+
}).toEqual({
|
|
212
|
+
resolvedId: 2,
|
|
213
|
+
detailPath: '/experiments/42/analysis-artifacts/2',
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it('should fail explicitly when the stable identity is not available', async () => {
|
|
218
|
+
const { resolveTarget, error } = setup()
|
|
219
|
+
|
|
220
|
+
const resolved = await resolveTarget({
|
|
221
|
+
experimentId: 42,
|
|
222
|
+
artifactPluginId: 'LEAF',
|
|
223
|
+
artifactKey: 'missing',
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
expect({ resolved, error: error.value }).toEqual({
|
|
227
|
+
resolved: null,
|
|
228
|
+
error: 'Analysis artifact LEAF/missing is not available.',
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
it('should reject a target from another experiment before making a request', async () => {
|
|
233
|
+
const { resolveTarget, error } = setup()
|
|
234
|
+
|
|
235
|
+
const resolved = await resolveTarget({
|
|
236
|
+
experimentId: 66,
|
|
237
|
+
artifactPluginId: 'LEAF',
|
|
238
|
+
artifactKey: 'result',
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
expect({
|
|
242
|
+
resolved,
|
|
243
|
+
error: error.value,
|
|
244
|
+
requests: mockGet.mock.calls,
|
|
245
|
+
}).toEqual({
|
|
246
|
+
resolved: null,
|
|
247
|
+
error: 'Analysis artifact target belongs to experiment 66, not 42.',
|
|
248
|
+
requests: [],
|
|
249
|
+
})
|
|
250
|
+
})
|
|
251
|
+
})
|
|
252
|
+
|
|
180
253
|
// -------------------------------------------------------------------------
|
|
181
254
|
// experiment id resolution
|
|
182
255
|
// -------------------------------------------------------------------------
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/** Renders a native `<select>` element with size, error, and disabled states. */
|
|
3
|
-
import { computed } from 'vue'
|
|
3
|
+
import { computed, useAttrs } from 'vue'
|
|
4
|
+
import type { HTMLAttributes } from 'vue'
|
|
4
5
|
import type { SelectOption, SelectOptionInput } from '../types'
|
|
5
6
|
import { findOptionByDomValue, normalizeOptionInput } from '../utils/options'
|
|
6
7
|
|
|
8
|
+
defineOptions({ inheritAttrs: false })
|
|
9
|
+
|
|
7
10
|
interface Props {
|
|
8
11
|
modelValue?: string | number
|
|
9
12
|
options: SelectOptionInput<string | number>[]
|
|
@@ -19,6 +22,16 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
19
22
|
error: false,
|
|
20
23
|
size: 'md',
|
|
21
24
|
})
|
|
25
|
+
const attrs = useAttrs()
|
|
26
|
+
|
|
27
|
+
const controlAttrs = computed(() => {
|
|
28
|
+
const { class: _class, style: _style, ...nativeAttrs } = attrs
|
|
29
|
+
return nativeAttrs
|
|
30
|
+
})
|
|
31
|
+
const wrapperAttrs = computed<Pick<HTMLAttributes, 'class' | 'style'>>(() => ({
|
|
32
|
+
class: attrs.class as HTMLAttributes['class'],
|
|
33
|
+
style: attrs.style as HTMLAttributes['style'],
|
|
34
|
+
}))
|
|
22
35
|
|
|
23
36
|
const emit = defineEmits<{
|
|
24
37
|
'update:modelValue': [value: string | number]
|
|
@@ -36,8 +49,9 @@ function handleChange(event: Event) {
|
|
|
36
49
|
</script>
|
|
37
50
|
|
|
38
51
|
<template>
|
|
39
|
-
<div class="mint-select">
|
|
52
|
+
<div v-bind="wrapperAttrs" class="mint-select">
|
|
40
53
|
<select
|
|
54
|
+
v-bind="controlAttrs"
|
|
41
55
|
:value="modelValue"
|
|
42
56
|
:disabled="disabled"
|
|
43
57
|
:aria-invalid="error || undefined"
|
|
@@ -5,6 +5,8 @@ import Tooltip from './Tooltip.vue'
|
|
|
5
5
|
interface Props {
|
|
6
6
|
modelValue?: boolean
|
|
7
7
|
label?: string
|
|
8
|
+
/** Accessible switch name; defaults to the visual label, then "Toggle". */
|
|
9
|
+
ariaLabel?: string
|
|
8
10
|
description?: string
|
|
9
11
|
icon?: string | string[]
|
|
10
12
|
iconColor?: string
|
|
@@ -111,7 +113,7 @@ function iconStyle(): Record<string, string> {
|
|
|
111
113
|
role="switch"
|
|
112
114
|
:tabindex="disabled ? -1 : 0"
|
|
113
115
|
:aria-checked="modelValue"
|
|
114
|
-
:aria-label="label || 'Toggle'"
|
|
116
|
+
:aria-label="ariaLabel || label || 'Toggle'"
|
|
115
117
|
:class="[
|
|
116
118
|
'mint-toggle__track',
|
|
117
119
|
`mint-toggle__track--${size}`,
|
|
@@ -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
|
+
}
|
package/src/composables/index.ts
CHANGED
|
@@ -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,
|
|
@@ -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
|
|
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(
|
|
44
|
-
?? experimentIdFromPath(
|
|
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,
|
|
@@ -38,6 +38,15 @@ export interface AnalysisArtifactListResponse {
|
|
|
38
38
|
artifacts: AnalysisArtifactSummary[]
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
/** Stable identity used when one plugin opens another plugin's artifact. */
|
|
42
|
+
export interface AnalysisArtifactTarget {
|
|
43
|
+
experimentId: number
|
|
44
|
+
artifactPluginId: string
|
|
45
|
+
artifactKey: string
|
|
46
|
+
/** Optional lookup hint. The stable identity remains the three fields above. */
|
|
47
|
+
artifactId?: number
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
/** Body of PATCH /experiments/{id}/analysis-artifacts/{artifactId}. */
|
|
42
51
|
export interface AnalysisArtifactMetadataUpdate {
|
|
43
52
|
display_name?: string
|
package/src/types/index.ts
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"EmptyState-D1c4RdC7.js","names":[],"sources":["../src/components/BaseInput.vue","../src/components/BaseInput.vue","../src/utils/options.ts","../src/components/BaseSelect.vue","../src/components/BaseSelect.vue","../src/components/BasePill.vue","../src/components/BasePill.vue","../src/composables/useApi.ts","../src/composables/useRequestSyncState.ts","../src/components/Skeleton.vue","../src/components/Skeleton.vue","../src/components/EmptyState.vue","../src/components/EmptyState.vue"],"sourcesContent":["<script setup lang=\"ts\">\n/** Renders a styled `<input>` supporting text/number/email types, size, error, and readonly states. */\nimport type { InputType } from '../types'\n\ninterface Props {\n modelValue?: string | number\n type?: InputType\n placeholder?: string\n disabled?: boolean\n readonly?: boolean\n error?: boolean\n size?: 'sm' | 'md' | 'lg'\n autocomplete?: string\n autofocus?: boolean\n min?: number\n max?: number\n step?: number\n list?: string\n ariaDescribedby?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n type: 'text',\n disabled: false,\n readonly: false,\n error: false,\n size: 'md',\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string | number | null]\n focus: [event: FocusEvent]\n blur: [event: FocusEvent]\n keydown: [event: KeyboardEvent]\n}>()\n\nfunction handleInput(event: Event) {\n const target = event.target as HTMLInputElement\n const value = props.type === 'number'\n ? (target.value === '' ? null : Number(target.value))\n : target.value\n emit('update:modelValue', value as string | number)\n}\n</script>\n\n<template>\n <input\n :type=\"type\"\n :value=\"modelValue\"\n :placeholder=\"placeholder\"\n :disabled=\"disabled\"\n :readonly=\"readonly\"\n :autocomplete=\"autocomplete\"\n :autofocus=\"autofocus\"\n :min=\"min\"\n :max=\"max\"\n :step=\"step\"\n :list=\"list\"\n :aria-invalid=\"error || undefined\"\n :aria-describedby=\"ariaDescribedby || undefined\"\n :class=\"[\n 'mint-input',\n `mint-input--${size}`,\n error ? 'mint-input--error' : '',\n disabled ? 'mint-input--disabled' : '',\n ]\"\n @input=\"handleInput\"\n @focus=\"emit('focus', $event)\"\n @blur=\"emit('blur', $event)\"\n @keydown=\"emit('keydown', $event)\"\n />\n</template>\n\n<style>\n@import '../styles/components/input.css';\n</style>\n","<script setup lang=\"ts\">\n/** Renders a styled `<input>` supporting text/number/email types, size, error, and readonly states. */\nimport type { InputType } from '../types'\n\ninterface Props {\n modelValue?: string | number\n type?: InputType\n placeholder?: string\n disabled?: boolean\n readonly?: boolean\n error?: boolean\n size?: 'sm' | 'md' | 'lg'\n autocomplete?: string\n autofocus?: boolean\n min?: number\n max?: number\n step?: number\n list?: string\n ariaDescribedby?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n type: 'text',\n disabled: false,\n readonly: false,\n error: false,\n size: 'md',\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string | number | null]\n focus: [event: FocusEvent]\n blur: [event: FocusEvent]\n keydown: [event: KeyboardEvent]\n}>()\n\nfunction handleInput(event: Event) {\n const target = event.target as HTMLInputElement\n const value = props.type === 'number'\n ? (target.value === '' ? null : Number(target.value))\n : target.value\n emit('update:modelValue', value as string | number)\n}\n</script>\n\n<template>\n <input\n :type=\"type\"\n :value=\"modelValue\"\n :placeholder=\"placeholder\"\n :disabled=\"disabled\"\n :readonly=\"readonly\"\n :autocomplete=\"autocomplete\"\n :autofocus=\"autofocus\"\n :min=\"min\"\n :max=\"max\"\n :step=\"step\"\n :list=\"list\"\n :aria-invalid=\"error || undefined\"\n :aria-describedby=\"ariaDescribedby || undefined\"\n :class=\"[\n 'mint-input',\n `mint-input--${size}`,\n error ? 'mint-input--error' : '',\n disabled ? 'mint-input--disabled' : '',\n ]\"\n @input=\"handleInput\"\n @focus=\"emit('focus', $event)\"\n @blur=\"emit('blur', $event)\"\n @keydown=\"emit('keydown', $event)\"\n />\n</template>\n\n<style>\n@import '../styles/components/input.css';\n</style>\n","export type OptionLike<TValue extends string | number> = {\n value: TValue\n label: string\n}\n\nexport function normalizeOptionInput<\n TValue extends string | number,\n TOption extends OptionLike<TValue>,\n>(option: TOption | TValue): TOption {\n if (typeof option === 'object') return option\n\n return {\n value: option,\n label: String(option),\n } as TOption\n}\n\nexport function findOptionByDomValue<\n TValue extends string | number,\n TOption extends OptionLike<TValue>,\n>(options: TOption[], value: string): TOption | undefined {\n return options.find(option => String(option.value) === value)\n}\n","<script setup lang=\"ts\">\n/** Renders a native `<select>` element with size, error, and disabled states. */\nimport { computed } from 'vue'\nimport type { SelectOption, SelectOptionInput } from '../types'\nimport { findOptionByDomValue, normalizeOptionInput } from '../utils/options'\n\ninterface Props {\n modelValue?: string | number\n options: SelectOptionInput<string | number>[]\n placeholder?: string\n disabled?: boolean\n error?: boolean\n size?: 'sm' | 'md' | 'lg'\n ariaDescribedby?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n disabled: false,\n error: false,\n size: 'md',\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string | number]\n}>()\n\nconst normalizedOptions = computed<SelectOption<string | number>[]>(() =>\n props.options.map(normalizeOptionInput)\n)\n\nfunction handleChange(event: Event) {\n const target = event.target as HTMLSelectElement\n const option = findOptionByDomValue(normalizedOptions.value, target.value)\n emit('update:modelValue', option?.value ?? target.value)\n}\n</script>\n\n<template>\n <div class=\"mint-select\">\n <select\n :value=\"modelValue\"\n :disabled=\"disabled\"\n :aria-invalid=\"error || undefined\"\n :aria-describedby=\"ariaDescribedby || undefined\"\n :class=\"[\n 'mint-select__control',\n `mint-select__control--${size}`,\n error ? 'mint-select__control--error' : '',\n disabled ? 'mint-select__control--disabled' : '',\n ]\"\n @change=\"handleChange\"\n >\n <option v-if=\"placeholder\" value=\"\" disabled>\n {{ placeholder }}\n </option>\n <option\n v-for=\"option in normalizedOptions\"\n :key=\"String(option.value)\"\n :value=\"option.value\"\n :disabled=\"option.disabled\"\n >\n {{ option.label }}\n </option>\n </select>\n <div class=\"mint-select__icon\">\n <svg fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" viewBox=\"0 0 24 24\">\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </div>\n </div>\n</template>\n\n<style>\n@import '../styles/components/select.css';\n</style>\n","<script setup lang=\"ts\">\n/** Renders a native `<select>` element with size, error, and disabled states. */\nimport { computed } from 'vue'\nimport type { SelectOption, SelectOptionInput } from '../types'\nimport { findOptionByDomValue, normalizeOptionInput } from '../utils/options'\n\ninterface Props {\n modelValue?: string | number\n options: SelectOptionInput<string | number>[]\n placeholder?: string\n disabled?: boolean\n error?: boolean\n size?: 'sm' | 'md' | 'lg'\n ariaDescribedby?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n disabled: false,\n error: false,\n size: 'md',\n})\n\nconst emit = defineEmits<{\n 'update:modelValue': [value: string | number]\n}>()\n\nconst normalizedOptions = computed<SelectOption<string | number>[]>(() =>\n props.options.map(normalizeOptionInput)\n)\n\nfunction handleChange(event: Event) {\n const target = event.target as HTMLSelectElement\n const option = findOptionByDomValue(normalizedOptions.value, target.value)\n emit('update:modelValue', option?.value ?? target.value)\n}\n</script>\n\n<template>\n <div class=\"mint-select\">\n <select\n :value=\"modelValue\"\n :disabled=\"disabled\"\n :aria-invalid=\"error || undefined\"\n :aria-describedby=\"ariaDescribedby || undefined\"\n :class=\"[\n 'mint-select__control',\n `mint-select__control--${size}`,\n error ? 'mint-select__control--error' : '',\n disabled ? 'mint-select__control--disabled' : '',\n ]\"\n @change=\"handleChange\"\n >\n <option v-if=\"placeholder\" value=\"\" disabled>\n {{ placeholder }}\n </option>\n <option\n v-for=\"option in normalizedOptions\"\n :key=\"String(option.value)\"\n :value=\"option.value\"\n :disabled=\"option.disabled\"\n >\n {{ option.label }}\n </option>\n </select>\n <div class=\"mint-select__icon\">\n <svg fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" viewBox=\"0 0 24 24\">\n <path d=\"m6 9 6 6 6-6\" />\n </svg>\n </div>\n </div>\n</template>\n\n<style>\n@import '../styles/components/select.css';\n</style>\n","<script setup lang=\"ts\">\n/** Compact label for tags, status indicators, and badges; supports removable and icon slots. */\nimport type { PillVariant, PillColor, PillSize } from '../types'\n\n/**\n * BasePill - Compact label component for tags, status indicators, and badges.\n *\n * @example\n * ```vue\n * <BasePill variant=\"success\">Active</BasePill>\n * <BasePill variant=\"warning\" removable @remove=\"handleRemove\">Tag</BasePill>\n * <BasePill :icon=\"true\">\n * <template #icon><CheckIcon /></template>\n * Verified\n * </BasePill>\n * ```\n */\ninterface Props {\n /** Visual style variant */\n variant?: PillVariant\n /** Semantic color modifier — use with variant=\"outline\" for colored outlines */\n color?: PillColor\n /** Size of the pill */\n size?: PillSize\n /** Show remove button */\n removable?: boolean\n /** Disable interaction */\n disabled?: boolean\n /** Show icon slot */\n icon?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n variant: 'default',\n color: undefined,\n size: 'md',\n removable: false,\n disabled: false,\n icon: false,\n})\n\n/**\n * @event remove - Emitted when the remove button is clicked\n */\nconst emit = defineEmits<{\n remove: []\n}>()\n\nfunction handleRemove(event: MouseEvent) {\n event.stopPropagation()\n if (!props.disabled) {\n emit('remove')\n }\n}\n</script>\n\n<template>\n <span\n :class=\"[\n 'mint-pill',\n `mint-pill--${variant}`,\n color && `mint-pill--${color}`,\n `mint-pill--${size}`,\n { 'mint-pill--disabled': disabled, 'mint-pill--with-icon': icon },\n ]\"\n >\n <span v-if=\"icon\" class=\"mint-pill__icon\">\n <slot name=\"icon\" />\n </span>\n <span class=\"mint-pill__label\">\n <slot />\n </span>\n <button\n v-if=\"removable && !disabled\"\n type=\"button\"\n class=\"mint-pill__remove\"\n aria-label=\"Remove\"\n @click=\"handleRemove\"\n >\n <svg class=\"mint-pill__remove-icon\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" viewBox=\"0 0 24 24\">\n <path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />\n </svg>\n </button>\n </span>\n</template>\n\n<style>\n@import '../styles/components/pill.css';\n</style>\n","<script setup lang=\"ts\">\n/** Compact label for tags, status indicators, and badges; supports removable and icon slots. */\nimport type { PillVariant, PillColor, PillSize } from '../types'\n\n/**\n * BasePill - Compact label component for tags, status indicators, and badges.\n *\n * @example\n * ```vue\n * <BasePill variant=\"success\">Active</BasePill>\n * <BasePill variant=\"warning\" removable @remove=\"handleRemove\">Tag</BasePill>\n * <BasePill :icon=\"true\">\n * <template #icon><CheckIcon /></template>\n * Verified\n * </BasePill>\n * ```\n */\ninterface Props {\n /** Visual style variant */\n variant?: PillVariant\n /** Semantic color modifier — use with variant=\"outline\" for colored outlines */\n color?: PillColor\n /** Size of the pill */\n size?: PillSize\n /** Show remove button */\n removable?: boolean\n /** Disable interaction */\n disabled?: boolean\n /** Show icon slot */\n icon?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n variant: 'default',\n color: undefined,\n size: 'md',\n removable: false,\n disabled: false,\n icon: false,\n})\n\n/**\n * @event remove - Emitted when the remove button is clicked\n */\nconst emit = defineEmits<{\n remove: []\n}>()\n\nfunction handleRemove(event: MouseEvent) {\n event.stopPropagation()\n if (!props.disabled) {\n emit('remove')\n }\n}\n</script>\n\n<template>\n <span\n :class=\"[\n 'mint-pill',\n `mint-pill--${variant}`,\n color && `mint-pill--${color}`,\n `mint-pill--${size}`,\n { 'mint-pill--disabled': disabled, 'mint-pill--with-icon': icon },\n ]\"\n >\n <span v-if=\"icon\" class=\"mint-pill__icon\">\n <slot name=\"icon\" />\n </span>\n <span class=\"mint-pill__label\">\n <slot />\n </span>\n <button\n v-if=\"removable && !disabled\"\n type=\"button\"\n class=\"mint-pill__remove\"\n aria-label=\"Remove\"\n @click=\"handleRemove\"\n >\n <svg class=\"mint-pill__remove-icon\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" viewBox=\"0 0 24 24\">\n <path d=\"M18 6 6 18\" /><path d=\"m6 6 12 12\" />\n </svg>\n </button>\n </span>\n</template>\n\n<style>\n@import '../styles/components/pill.css';\n</style>\n","import axios, { type AxiosInstance, type AxiosRequestConfig, type InternalAxiosRequestConfig } from 'axios'\nimport { useSettingsStore } from '../stores/settings'\nimport { useAuthStore } from '../stores/auth'\n\nlet apiClientInstance: AxiosInstance | null = null\nlet interceptorAttached = false\n\ninterface MintAxiosRequestConfig extends AxiosRequestConfig {\n _mintSkipAuth?: boolean\n _mintAuthToken?: string\n}\n\ninterface MintInternalAxiosRequestConfig extends InternalAxiosRequestConfig {\n _mintSkipAuth?: boolean\n _mintAuthToken?: string\n}\n\ntype MutableHeaders = Record<string, unknown> & {\n has?: (header: string) => boolean\n set?: (header: string, value: string) => void\n delete?: (header: string) => void\n}\n\nfunction joinUrlPath(baseUrl: string, path: string): string {\n if (/^https?:\\/\\//i.test(path)) return path\n if (!path) return baseUrl\n if (path.startsWith('?') || path.startsWith('#')) return `${baseUrl.replace(/\\/+$/, '')}${path}`\n const normalizedBase = baseUrl.replace(/\\/+$/, '')\n const normalizedPath = path.replace(/^\\/+/, '/')\n return `${normalizedBase}${normalizedPath.startsWith('/') ? normalizedPath : `/${normalizedPath}`}`\n}\n\nfunction getBasePath(baseUrl: string): string {\n if (!baseUrl) return '/'\n\n try {\n const origin = typeof window !== 'undefined' ? window.location.origin : 'http://localhost'\n return new URL(baseUrl, origin).pathname.replace(/\\/+$/, '') || '/'\n } catch {\n const path = baseUrl.replace(/^https?:\\/\\/[^/]+/i, '')\n return path.replace(/\\/+$/, '') || '/'\n }\n}\n\nfunction normalizeRequestUrl(baseUrl: string, url: string): string {\n if (!url || /^https?:\\/\\//.test(url)) return url\n\n const basePath = getBasePath(baseUrl)\n if (basePath === '/') return url\n\n const normalizedUrl = url.startsWith('/') ? url : `/${url}`\n if (\n normalizedUrl === basePath\n || normalizedUrl.startsWith(`${basePath}?`)\n || normalizedUrl.startsWith(`${basePath}#`)\n ) {\n return normalizedUrl.slice(basePath.length)\n }\n if (normalizedUrl.startsWith(`${basePath}/`)) {\n return normalizedUrl.slice(basePath.length) || ''\n }\n return url\n}\n\nfunction asMutableHeaders(headers: AxiosRequestConfig['headers']): MutableHeaders | null {\n return headers ? (headers as MutableHeaders) : null\n}\n\nfunction hasAuthorizationHeader(headers: AxiosRequestConfig['headers']): boolean {\n const bag = asMutableHeaders(headers)\n if (!bag) return false\n if (typeof bag.has === 'function') return bag.has('Authorization')\n return Object.keys(bag).some((key) => key.toLowerCase() === 'authorization')\n}\n\nfunction setAuthorizationHeader(headers: AxiosRequestConfig['headers'], value: string): void {\n const bag = asMutableHeaders(headers)\n if (!bag) return\n if (typeof bag.set === 'function') {\n bag.set('Authorization', value)\n return\n }\n bag.Authorization = value\n}\n\nfunction deleteAuthorizationHeader(headers: AxiosRequestConfig['headers']): void {\n const bag = asMutableHeaders(headers)\n if (!bag) return\n if (typeof bag.delete === 'function') {\n bag.delete('Authorization')\n return\n }\n for (const key of Object.keys(bag)) {\n if (key.toLowerCase() === 'authorization') delete bag[key]\n }\n}\n\nfunction getAuthorizationHeader(headers: AxiosRequestConfig['headers']): unknown {\n const bag = asMutableHeaders(headers) as (MutableHeaders & { get?: (header: string) => unknown }) | null\n if (!bag) return undefined\n if (typeof bag.get === 'function') return bag.get('Authorization')\n const key = Object.keys(bag).find((candidate) => candidate.toLowerCase() === 'authorization')\n return key ? bag[key] : undefined\n}\n\n/** Raised when an authenticated SDK request is rejected because the login is no longer valid. */\nexport class AuthenticationRequiredError extends Error {\n readonly code = 'AUTHENTICATION_REQUIRED'\n readonly status = 401\n\n constructor(readonly cause?: unknown) {\n super('Authentication required. Please sign in again.')\n this.name = 'AuthenticationRequiredError'\n }\n}\n\n/** @internal Convert a rejected authenticated request into the SDK's public auth error. */\nexport function authenticationRequiredError(\n cause: unknown,\n rejectedToken?: string,\n): AuthenticationRequiredError {\n const currentAuthStore = useAuthStore()\n if (rejectedToken && currentAuthStore.token === rejectedToken) {\n currentAuthStore.clearToken()\n }\n return new AuthenticationRequiredError(cause)\n}\n\nfunction getApiClient(): AxiosInstance {\n if (!apiClientInstance) {\n apiClientInstance = axios.create({\n headers: {\n 'Content-Type': 'application/json',\n },\n })\n }\n return apiClientInstance\n}\n\nexport interface ApiClientOptions {\n baseUrl?: string\n timeout?: number\n withAuth?: boolean\n}\n\nexport interface UseApiReturn {\n client: AxiosInstance\n get: <T>(url: string, config?: AxiosRequestConfig) => Promise<T>\n post: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) => Promise<T>\n put: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) => Promise<T>\n patch: <T>(url: string, data?: unknown, config?: AxiosRequestConfig) => Promise<T>\n delete: <T>(url: string, config?: AxiosRequestConfig) => Promise<T>\n upload: <T>(url: string, file: File, fieldName?: string, additionalData?: Record<string, unknown>) => Promise<T>\n download: (url: string, filename?: string) => Promise<string>\n buildUrl: (path: string) => string\n buildWsUrl: (path: string) => string\n}\n\n/** Axios-based API client that injects the plugin base URL and JWT auth header on every request. */\nexport function useApi(options: ApiClientOptions = {}): UseApiReturn {\n const settingsStore = useSettingsStore()\n const authStore = useAuthStore()\n const apiClient = getApiClient()\n\n // Ensure auth store is initialized (reads token from localStorage)\n if (!authStore.isInitialized) {\n authStore.initialize()\n }\n\n // Attach auth interceptor only once (reads token dynamically, not from closure)\n if (!interceptorAttached) {\n apiClient.interceptors.request.use((config) => {\n const request = config as MintInternalAxiosRequestConfig\n if (request._mintSkipAuth) {\n deleteAuthorizationHeader(request.headers)\n return request\n }\n\n const currentAuthStore = useAuthStore()\n if (currentAuthStore.token && config.headers && !hasAuthorizationHeader(config.headers)) {\n setAuthorizationHeader(config.headers, `Bearer ${currentAuthStore.token}`)\n }\n if (getAuthorizationHeader(config.headers) === `Bearer ${currentAuthStore.token}`) {\n request._mintAuthToken = currentAuthStore.token ?? undefined\n }\n return config\n })\n apiClient.interceptors.response.use(\n (response) => response,\n (error: unknown) => {\n if (axios.isAxiosError(error) && error.response?.status === 401) {\n const request = error.config as MintInternalAxiosRequestConfig | undefined\n if (request?._mintSkipAuth !== true) {\n return Promise.reject(authenticationRequiredError(error, request?._mintAuthToken))\n }\n }\n return Promise.reject(error)\n },\n )\n interceptorAttached = true\n }\n\n function getBaseUrl(): string {\n return options.baseUrl ?? settingsStore.getApiBaseUrl()\n }\n\n function normalizeUrl(url: string): string {\n return normalizeRequestUrl(getBaseUrl(), url)\n }\n\n // Build per-request config that applies this caller's options\n function requestConfig(config?: AxiosRequestConfig): MintAxiosRequestConfig {\n const base: MintAxiosRequestConfig = {\n baseURL: getBaseUrl(),\n timeout: options.timeout ?? settingsStore.requestTimeout,\n ...config,\n }\n // Strip auth header if explicitly disabled\n if (options.withAuth === false) {\n base._mintSkipAuth = true\n deleteAuthorizationHeader(base.headers)\n }\n return base\n }\n\n // Generic request methods\n async function get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {\n const response = await apiClient.get<T>(normalizeUrl(url), requestConfig(config))\n return response.data\n }\n\n async function post<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {\n const response = await apiClient.post<T>(normalizeUrl(url), data, requestConfig(config))\n return response.data\n }\n\n async function put<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {\n const response = await apiClient.put<T>(normalizeUrl(url), data, requestConfig(config))\n return response.data\n }\n\n async function patch<T>(url: string, data?: unknown, config?: AxiosRequestConfig): Promise<T> {\n const response = await apiClient.patch<T>(normalizeUrl(url), data, requestConfig(config))\n return response.data\n }\n\n async function del<T>(url: string, config?: AxiosRequestConfig): Promise<T> {\n const response = await apiClient.delete<T>(normalizeUrl(url), requestConfig(config))\n return response.data\n }\n\n // File upload helper\n async function upload<T>(url: string, file: File, fieldName = 'file', additionalData?: Record<string, unknown>): Promise<T> {\n const formData = new FormData()\n formData.append(fieldName, file)\n\n if (additionalData) {\n Object.entries(additionalData).forEach(([key, value]) => {\n if (value !== undefined && value !== null) {\n formData.append(key, typeof value === 'object' ? JSON.stringify(value) : String(value))\n }\n })\n }\n\n const response = await apiClient.post<T>(normalizeUrl(url), formData, requestConfig({\n // Let Axios set Content-Type with the correct multipart boundary\n headers: { 'Content-Type': undefined },\n }))\n return response.data\n }\n\n // Download helper - returns blob URL\n async function download(url: string, filename?: string): Promise<string> {\n const response = await apiClient.get(normalizeUrl(url), requestConfig({ responseType: 'blob' }))\n const blob = new Blob([response.data])\n const blobUrl = URL.createObjectURL(blob)\n\n // Optionally trigger download\n if (filename) {\n const link = document.createElement('a')\n link.href = blobUrl\n link.download = filename\n document.body.appendChild(link)\n link.click()\n document.body.removeChild(link)\n // Revoke after a short delay to allow download to start;\n // return empty string since the URL will be invalidated\n setTimeout(() => URL.revokeObjectURL(blobUrl), 100)\n return ''\n }\n\n // Caller is responsible for revoking when no filename is provided\n return blobUrl\n }\n\n // Build full URL for external use (e.g., <a href=\"...\">)\n function buildUrl(path: string): string {\n const baseUrl = getBaseUrl()\n return joinUrlPath(baseUrl, normalizeRequestUrl(baseUrl, path))\n }\n\n // WebSocket URL builder\n function buildWsUrl(path: string): string {\n return joinUrlPath(settingsStore.getWsBaseUrl(), path)\n }\n\n return {\n client: apiClient,\n get,\n post,\n put,\n patch,\n delete: del,\n upload,\n download,\n buildUrl,\n buildWsUrl,\n }\n}\n","import { ref, type Ref } from 'vue'\n\nexport type RequestSyncSuccessKind = 'load' | 'save' | 'run'\n\nexport interface RequestSyncRunOptions {\n success?: RequestSyncSuccessKind\n errorMessage?: string\n}\n\nexport interface UseRequestSyncStateReturn {\n /** Whether a request is in progress. */\n loading: Ref<boolean>\n /** Message from the last failed request, or null. */\n error: Ref<string | null>\n /** Timestamp of the last successful load operation, or null. */\n lastLoadedAt: Ref<Date | null>\n /** Timestamp of the last successful save operation, or null. */\n lastSavedAt: Ref<Date | null>\n /** Timestamp of the last successful run operation, or null. */\n lastRunAt: Ref<Date | null>\n /** Clear the current error without changing loading or timestamps. */\n clearError: () => void\n /** Convert thrown values into a developer-facing error message. */\n readErrorMessage: (value: unknown, fallback?: string) => string\n /** Store and return a normalized error message. */\n setError: (value: unknown, fallback?: string) => string\n /** Mark the resource as successfully loaded. */\n markLoaded: (date?: Date) => void\n /** Mark the resource as successfully saved. */\n markSaved: (date?: Date) => void\n /** Mark the operation as successfully run. */\n markRun: (date?: Date) => void\n /** Run a request with shared loading/error handling and optional sync marking. */\n run: <T>(operation: () => Promise<T>, options?: RequestSyncRunOptions) => Promise<T>\n}\n\n/** Shared loading/error/timestamp state for generated plugin request helpers. */\nexport function useRequestSyncState(\n defaultErrorMessage = 'Request failed.',\n): UseRequestSyncStateReturn {\n const loading = ref(false)\n const error = ref<string | null>(null)\n const lastLoadedAt = ref<Date | null>(null)\n const lastSavedAt = ref<Date | null>(null)\n const lastRunAt = ref<Date | null>(null)\n\n function clearError(): void {\n error.value = null\n }\n\n function readErrorMessage(value: unknown, fallback = defaultErrorMessage): string {\n if (value instanceof Error && value.message) {\n return value.message\n }\n if (typeof value === 'string' && value.trim()) {\n return value\n }\n if (\n typeof value === 'object'\n && value !== null\n && 'message' in value\n && typeof (value as { message?: unknown }).message === 'string'\n && (value as { message: string }).message\n ) {\n return (value as { message: string }).message\n }\n return fallback\n }\n\n function setError(value: unknown, fallback?: string): string {\n const message = readErrorMessage(value, fallback)\n error.value = message\n return message\n }\n\n function markLoaded(date = new Date()): void {\n lastLoadedAt.value = date\n }\n\n function markSaved(date = new Date()): void {\n lastSavedAt.value = date\n }\n\n function markRun(date = new Date()): void {\n lastRunAt.value = date\n }\n\n async function run<T>(\n operation: () => Promise<T>,\n options: RequestSyncRunOptions = {},\n ): Promise<T> {\n loading.value = true\n clearError()\n try {\n const response = await operation()\n if (options.success === 'load') {\n markLoaded()\n } else if (options.success === 'save') {\n markSaved()\n } else if (options.success === 'run') {\n markRun()\n }\n return response\n } catch (err) {\n setError(err, options.errorMessage)\n throw err\n } finally {\n loading.value = false\n }\n }\n\n return {\n loading,\n error,\n lastLoadedAt,\n lastSavedAt,\n lastRunAt,\n clearError,\n readErrorMessage,\n setError,\n markLoaded,\n markSaved,\n markRun,\n run,\n }\n}\n","<script setup lang=\"ts\">\n/** Animated loading placeholder in text, circular, rectangular, or rounded variants with pulse or wave animation. */\nimport { computed } from 'vue'\n\ninterface Props {\n variant?: 'text' | 'circular' | 'rectangular' | 'rounded'\n width?: string | number\n height?: string | number\n animation?: 'pulse' | 'wave' | 'none'\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n variant: 'text',\n animation: 'wave',\n})\n\nconst style = computed(() => {\n const s: Record<string, string> = {}\n\n if (props.width) {\n s.width = typeof props.width === 'number' ? `${props.width}px` : props.width\n }\n\n if (props.height) {\n s.height = typeof props.height === 'number' ? `${props.height}px` : props.height\n }\n\n return s\n})\n\nconst classes = computed(() => {\n const base = ['bg-bg-hover']\n\n switch (props.variant) {\n case 'circular':\n base.push('rounded-full')\n if (!props.width && !props.height) {\n base.push('w-10 h-10')\n }\n break\n case 'rectangular':\n base.push('rounded-none')\n if (!props.height) base.push('h-24')\n break\n case 'rounded':\n base.push('rounded-mint')\n if (!props.height) base.push('h-24')\n break\n default:\n base.push('rounded')\n if (!props.height) base.push('h-4')\n if (!props.width) base.push('w-full')\n }\n\n switch (props.animation) {\n case 'pulse':\n base.push('animate-pulse')\n break\n case 'wave':\n base.push('skeleton-wave')\n break\n }\n\n return base\n})\n</script>\n\n<template>\n <div :class=\"classes\" :style=\"style\" />\n</template>\n\n<style>\n@import '../styles/components/skeleton.css';\n</style>\n","<script setup lang=\"ts\">\n/** Animated loading placeholder in text, circular, rectangular, or rounded variants with pulse or wave animation. */\nimport { computed } from 'vue'\n\ninterface Props {\n variant?: 'text' | 'circular' | 'rectangular' | 'rounded'\n width?: string | number\n height?: string | number\n animation?: 'pulse' | 'wave' | 'none'\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n variant: 'text',\n animation: 'wave',\n})\n\nconst style = computed(() => {\n const s: Record<string, string> = {}\n\n if (props.width) {\n s.width = typeof props.width === 'number' ? `${props.width}px` : props.width\n }\n\n if (props.height) {\n s.height = typeof props.height === 'number' ? `${props.height}px` : props.height\n }\n\n return s\n})\n\nconst classes = computed(() => {\n const base = ['bg-bg-hover']\n\n switch (props.variant) {\n case 'circular':\n base.push('rounded-full')\n if (!props.width && !props.height) {\n base.push('w-10 h-10')\n }\n break\n case 'rectangular':\n base.push('rounded-none')\n if (!props.height) base.push('h-24')\n break\n case 'rounded':\n base.push('rounded-mint')\n if (!props.height) base.push('h-24')\n break\n default:\n base.push('rounded')\n if (!props.height) base.push('h-4')\n if (!props.width) base.push('w-full')\n }\n\n switch (props.animation) {\n case 'pulse':\n base.push('animate-pulse')\n break\n case 'wave':\n base.push('skeleton-wave')\n break\n }\n\n return base\n})\n</script>\n\n<template>\n <div :class=\"classes\" :style=\"style\" />\n</template>\n\n<style>\n@import '../styles/components/skeleton.css';\n</style>\n","<script setup lang=\"ts\">\n/** Empty-state placeholder with icon badge, headline, description, default slot, and optional CTA button. */\nimport BaseButton from './BaseButton.vue'\n\ninterface Props {\n title?: string\n description?: string\n iconPath?: string\n color?: 'primary' | 'cta' | 'success' | 'warning' | 'error' | 'muted'\n size?: 'sm' | 'md' | 'lg'\n variant?: 'illustrated' | 'inline'\n actionLabel?: string\n}\n\nwithDefaults(defineProps<Props>(), {\n color: 'primary',\n size: 'md',\n variant: 'illustrated',\n})\n\nconst emit = defineEmits<{\n action: []\n}>()\n\nconst defaultIconPaths = [\n 'M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z',\n 'M7 11h10',\n 'M7 15h6',\n 'M7 7h8',\n]\n</script>\n\n<template>\n <div :class=\"['mint-empty-state', `mint-empty-state--${variant}`, `mint-empty-state--${size}`]\">\n <div :class=\"['mint-empty-state__icon-wrapper', `mint-empty-state__icon-wrapper--${color}`]\">\n <slot name=\"icon\">\n <svg\n class=\"mint-empty-state__icon\"\n :class=\"`mint-empty-state__icon--${color}`\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <template v-if=\"iconPath\">\n <path :d=\"iconPath\" />\n </template>\n <template v-else>\n <path v-for=\"(d, i) in defaultIconPaths\" :key=\"i\" :d=\"d\" />\n </template>\n </svg>\n </slot>\n </div>\n <div class=\"mint-empty-state__body\">\n <h3 v-if=\"title\" class=\"mint-empty-state__title\">{{ title }}</h3>\n <p v-if=\"description\" class=\"mint-empty-state__description\">{{ description }}</p>\n <slot />\n </div>\n <div v-if=\"actionLabel\" class=\"mint-empty-state__action\">\n <BaseButton @click=\"emit('action')\">\n {{ actionLabel }}\n </BaseButton>\n </div>\n </div>\n</template>\n\n<style>\n@import '../styles/components/empty-state.css';\n</style>\n","<script setup lang=\"ts\">\n/** Empty-state placeholder with icon badge, headline, description, default slot, and optional CTA button. */\nimport BaseButton from './BaseButton.vue'\n\ninterface Props {\n title?: string\n description?: string\n iconPath?: string\n color?: 'primary' | 'cta' | 'success' | 'warning' | 'error' | 'muted'\n size?: 'sm' | 'md' | 'lg'\n variant?: 'illustrated' | 'inline'\n actionLabel?: string\n}\n\nwithDefaults(defineProps<Props>(), {\n color: 'primary',\n size: 'md',\n variant: 'illustrated',\n})\n\nconst emit = defineEmits<{\n action: []\n}>()\n\nconst defaultIconPaths = [\n 'M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z',\n 'M7 11h10',\n 'M7 15h6',\n 'M7 7h8',\n]\n</script>\n\n<template>\n <div :class=\"['mint-empty-state', `mint-empty-state--${variant}`, `mint-empty-state--${size}`]\">\n <div :class=\"['mint-empty-state__icon-wrapper', `mint-empty-state__icon-wrapper--${color}`]\">\n <slot name=\"icon\">\n <svg\n class=\"mint-empty-state__icon\"\n :class=\"`mint-empty-state__icon--${color}`\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n >\n <template v-if=\"iconPath\">\n <path :d=\"iconPath\" />\n </template>\n <template v-else>\n <path v-for=\"(d, i) in defaultIconPaths\" :key=\"i\" :d=\"d\" />\n </template>\n </svg>\n </slot>\n </div>\n <div class=\"mint-empty-state__body\">\n <h3 v-if=\"title\" class=\"mint-empty-state__title\">{{ title }}</h3>\n <p v-if=\"description\" class=\"mint-empty-state__description\">{{ description }}</p>\n <slot />\n </div>\n <div v-if=\"actionLabel\" class=\"mint-empty-state__action\">\n <BaseButton @click=\"emit('action')\">\n {{ actionLabel }}\n </BaseButton>\n </div>\n </div>\n</template>\n\n<style>\n@import '../styles/components/empty-state.css';\n</style>\n"],"mappings":"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"}
|