@morscherlab/mint-sdk 1.0.56 → 1.0.58
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/__tests__/components/ArtifactSaveDialog.test.d.ts +1 -0
- package/dist/__tests__/components/ArtifactSelectorModal.test.d.ts +1 -0
- package/dist/__tests__/composables/useAnalysisArtifacts.test.d.ts +1 -0
- package/dist/analysisArtifactTypes-Cu40dzSG.js +7 -0
- package/dist/analysisArtifactTypes-Cu40dzSG.js.map +1 -0
- package/dist/components/ArtifactSaveDialog.vue.d.ts +59 -0
- package/dist/components/ArtifactSelectorModal.vue.d.ts +41 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/index.js +2 -2
- package/dist/{components-BQwOeyiy.js → components-CpnOaPbP.js} +1774 -1144
- package/dist/components-CpnOaPbP.js.map +1 -0
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/index.js +4 -3
- package/dist/composables/useAnalysisArtifacts.d.ts +59 -0
- package/dist/{composables-TpXyhRZZ.js → composables-OYTD0Y3r.js} +2 -2
- package/dist/{composables-TpXyhRZZ.js.map → composables-OYTD0Y3r.js.map} +1 -1
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/install.js +1 -1
- package/dist/styles.css +412 -0
- package/dist/types/analysisArtifactTypes.d.ts +133 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +2 -0
- package/dist/{useJobsStatusTray-CVecN6Ao.js → useAnalysisArtifacts-Ob8UtVwl.js} +157 -2
- package/dist/useAnalysisArtifacts-Ob8UtVwl.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/components/ArtifactSaveDialog.test.ts +566 -0
- package/src/__tests__/components/ArtifactSelectorModal.test.ts +178 -0
- package/src/__tests__/composables/useAnalysisArtifacts.test.ts +667 -0
- package/src/components/ArtifactSaveDialog.story.vue +319 -0
- package/src/components/ArtifactSaveDialog.vue +434 -0
- package/src/components/ArtifactSelectorModal.story.vue +295 -0
- package/src/components/ArtifactSelectorModal.vue +326 -0
- package/src/components/index.ts +2 -0
- package/src/composables/index.ts +6 -0
- package/src/composables/useAnalysisArtifacts.ts +305 -0
- package/src/styles/components/artifact-save-dialog.css +17 -0
- package/src/styles/components/artifact-selector-modal.css +216 -0
- package/src/styles/index.css +2 -0
- package/src/types/analysisArtifactTypes.ts +167 -0
- package/src/types/index.ts +24 -0
- package/dist/components-BQwOeyiy.js.map +0 -1
- package/dist/useJobsStatusTray-CVecN6Ao.js.map +0 -1
- /package/dist/{ExperimentPopover-8A4Rhffp.js → ExperimentPopover-Ryusjrhv.js} +0 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/** Save/update dialog for analysis artifacts; delegates the write to a plugin-supplied handler. */
|
|
3
|
+
import { computed, ref, shallowRef, watch } from 'vue'
|
|
4
|
+
import BaseModal from './BaseModal.vue'
|
|
5
|
+
import BaseInput from './BaseInput.vue'
|
|
6
|
+
import BaseTextarea from './BaseTextarea.vue'
|
|
7
|
+
import BaseButton from './BaseButton.vue'
|
|
8
|
+
import AlertBox from './AlertBox.vue'
|
|
9
|
+
import FormField from './FormField.vue'
|
|
10
|
+
import FileUploader from './FileUploader.vue'
|
|
11
|
+
import SegmentedControl from './SegmentedControl.vue'
|
|
12
|
+
import { useRequestSyncState } from '../composables/useRequestSyncState'
|
|
13
|
+
import {
|
|
14
|
+
getInjectedPlatformContext,
|
|
15
|
+
resolveCurrentExperimentId,
|
|
16
|
+
} from '../composables/platformContextHelpers'
|
|
17
|
+
import {
|
|
18
|
+
ANALYSIS_FILE_ARTIFACT_SCHEMA,
|
|
19
|
+
type AnalysisArtifactDetail,
|
|
20
|
+
type ArtifactFileSource,
|
|
21
|
+
type ArtifactSaveDialogHandler,
|
|
22
|
+
type ArtifactSaveDialogPayload,
|
|
23
|
+
type ArtifactSaveHandler,
|
|
24
|
+
type ArtifactSaveResult,
|
|
25
|
+
type HandlerArtifactSaveHandler,
|
|
26
|
+
} from '../types/analysisArtifactTypes'
|
|
27
|
+
import type { ModalSize } from '../types'
|
|
28
|
+
|
|
29
|
+
interface Props {
|
|
30
|
+
modelValue: boolean
|
|
31
|
+
/** Performs the backend write; the dialog never calls the platform itself. */
|
|
32
|
+
saveHandler: ArtifactSaveDialogHandler
|
|
33
|
+
/** Which artifact kinds the form offers. */
|
|
34
|
+
mode?: 'json' | 'file' | 'both'
|
|
35
|
+
/** Use `handler` when the plugin backend serializes the file content itself. */
|
|
36
|
+
fileSource?: ArtifactFileSource
|
|
37
|
+
/** JSON result payload supplied programmatically by the host. */
|
|
38
|
+
result?: Record<string, unknown>
|
|
39
|
+
/** Experiment id override; defaults to platform injection or URL. */
|
|
40
|
+
experimentId?: number | null
|
|
41
|
+
/** Existing artifact detail; when set the dialog updates instead of creating. */
|
|
42
|
+
existingArtifact?: AnalysisArtifactDetail | null
|
|
43
|
+
/** Known artifact keys, used to warn (json) or block (file) on collisions. */
|
|
44
|
+
existingArtifactKeys?: string[]
|
|
45
|
+
title?: string
|
|
46
|
+
size?: ModalSize
|
|
47
|
+
defaultArtifactKey?: string
|
|
48
|
+
defaultDisplayName?: string
|
|
49
|
+
/** Maps to the Python `kind=` argument for new file artifacts. */
|
|
50
|
+
defaultFileKind?: string
|
|
51
|
+
/** Forwarded to the file uploader. */
|
|
52
|
+
accept?: string
|
|
53
|
+
/** Forwarded to the file uploader (bytes). */
|
|
54
|
+
maxFileSize?: number
|
|
55
|
+
/**
|
|
56
|
+
* Result metadata for file artifacts. Backend-side this REPLACES the previous
|
|
57
|
+
* metadata; on replacement the existing artifact's metadata is carried through
|
|
58
|
+
* unless this prop supplies a new map.
|
|
59
|
+
*/
|
|
60
|
+
metadata?: Record<string, unknown>
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
64
|
+
mode: 'file',
|
|
65
|
+
fileSource: 'upload',
|
|
66
|
+
experimentId: null,
|
|
67
|
+
existingArtifact: null,
|
|
68
|
+
size: 'md',
|
|
69
|
+
defaultArtifactKey: 'default',
|
|
70
|
+
defaultFileKind: 'file',
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
const emit = defineEmits<{
|
|
74
|
+
'update:modelValue': [value: boolean]
|
|
75
|
+
saved: [result: ArtifactSaveResult & { payload: ArtifactSaveDialogPayload }]
|
|
76
|
+
error: [message: string]
|
|
77
|
+
cancel: []
|
|
78
|
+
}>()
|
|
79
|
+
|
|
80
|
+
const ARTIFACT_KEY_PATTERN = /^[a-z0-9][a-z0-9._-]*$/
|
|
81
|
+
|
|
82
|
+
const injectedContext = getInjectedPlatformContext()
|
|
83
|
+
const request = useRequestSyncState('Failed to save analysis artifact.')
|
|
84
|
+
|
|
85
|
+
const isUpdate = computed(() => props.existingArtifact !== null)
|
|
86
|
+
const existingResult = computed(
|
|
87
|
+
() => (props.existingArtifact?.result ?? {}) as Record<string, unknown>,
|
|
88
|
+
)
|
|
89
|
+
const existingIsFile = computed(
|
|
90
|
+
() => existingResult.value.schema_version === ANALYSIS_FILE_ARTIFACT_SCHEMA,
|
|
91
|
+
)
|
|
92
|
+
const existingFileKind = computed(() =>
|
|
93
|
+
typeof existingResult.value.kind === 'string' ? existingResult.value.kind : undefined)
|
|
94
|
+
const existingFilename = computed(() =>
|
|
95
|
+
typeof existingResult.value.filename === 'string' ? existingResult.value.filename : undefined)
|
|
96
|
+
const existingMetadata = computed(() => {
|
|
97
|
+
const value = existingResult.value.metadata
|
|
98
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
99
|
+
? value as Record<string, unknown>
|
|
100
|
+
: undefined
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
const activeKind = ref<'json' | 'file'>('file')
|
|
104
|
+
const keyInput = ref('')
|
|
105
|
+
const displayNameInput = ref('')
|
|
106
|
+
const noteInput = ref('')
|
|
107
|
+
const selectedFile = shallowRef<File | null>(null)
|
|
108
|
+
let initialNote = ''
|
|
109
|
+
|
|
110
|
+
function resetForm(): void {
|
|
111
|
+
const existing = props.existingArtifact
|
|
112
|
+
activeKind.value = existing
|
|
113
|
+
? (existingIsFile.value ? 'file' : 'json')
|
|
114
|
+
: (props.mode === 'json' ? 'json' : 'file')
|
|
115
|
+
keyInput.value = existing?.artifact_key ?? props.defaultArtifactKey
|
|
116
|
+
displayNameInput.value = existing?.display_name ?? props.defaultDisplayName ?? ''
|
|
117
|
+
initialNote = existing?.note ?? ''
|
|
118
|
+
noteInput.value = initialNote
|
|
119
|
+
selectedFile.value = null
|
|
120
|
+
request.clearError()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
resetForm()
|
|
124
|
+
watch(() => props.modelValue, (open) => {
|
|
125
|
+
if (open) resetForm()
|
|
126
|
+
})
|
|
127
|
+
// Key off identity, not object reference: re-fetching the same artifact must not
|
|
128
|
+
// wipe in-progress edits.
|
|
129
|
+
watch(
|
|
130
|
+
() => (props.existingArtifact ? `${props.existingArtifact.id}:${props.existingArtifact.artifact_key}` : null),
|
|
131
|
+
resetForm,
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
const resolvedTitle = computed(() =>
|
|
135
|
+
props.title ?? (isUpdate.value ? 'Update Analysis Artifact' : 'Save Analysis Artifact'))
|
|
136
|
+
const showKindToggle = computed(() => !isUpdate.value && props.mode === 'both')
|
|
137
|
+
const showFileUploader = computed(
|
|
138
|
+
() => activeKind.value === 'file' && props.fileSource === 'upload',
|
|
139
|
+
)
|
|
140
|
+
// save_analysis_artifact ignores note=None (it preserves), so a JSON artifact's
|
|
141
|
+
// note cannot be cleared through the save path — that belongs to updateMetadata.
|
|
142
|
+
// update_analysis_file_artifact does honour the not-provided/null/value sentinel.
|
|
143
|
+
const showNoteField = computed(() => !isUpdate.value || existingIsFile.value)
|
|
144
|
+
|
|
145
|
+
const effectiveKey = computed(() => {
|
|
146
|
+
if (isUpdate.value) return props.existingArtifact?.artifact_key ?? ''
|
|
147
|
+
return keyInput.value.trim() || props.defaultArtifactKey
|
|
148
|
+
})
|
|
149
|
+
const keyValid = computed(() => ARTIFACT_KEY_PATTERN.test(effectiveKey.value))
|
|
150
|
+
const keyExists = computed(
|
|
151
|
+
() => !isUpdate.value && (props.existingArtifactKeys ?? []).includes(effectiveKey.value),
|
|
152
|
+
)
|
|
153
|
+
// A file artifact is create-only: reusing a key always fails backend-side, and the
|
|
154
|
+
// dialog has no detail to replace it safely. JSON keys upsert natively, so warn only.
|
|
155
|
+
const keyBlocked = computed(() => keyExists.value && activeKind.value === 'file')
|
|
156
|
+
const keyError = computed(() => {
|
|
157
|
+
if (!keyValid.value) return 'Use lowercase letters, digits, dots, dashes, or underscores.'
|
|
158
|
+
if (keyBlocked.value) return 'This key is already used. Choose another, or replace the existing artifact.'
|
|
159
|
+
return undefined
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
const canSubmit = computed(() => {
|
|
163
|
+
if (request.loading.value || !keyValid.value || keyBlocked.value) return false
|
|
164
|
+
if (activeKind.value === 'file') {
|
|
165
|
+
return props.fileSource === 'handler' || selectedFile.value !== null
|
|
166
|
+
}
|
|
167
|
+
return props.result !== undefined
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
function resolveExperimentId(): number | undefined {
|
|
171
|
+
return props.experimentId ?? resolveCurrentExperimentId(injectedContext)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Set-only note for creates and JSON upserts: an empty field just sends nothing. */
|
|
175
|
+
function createNotePayload(): { note?: string } {
|
|
176
|
+
const value = noteInput.value.trim()
|
|
177
|
+
return showNoteField.value && value ? { note: value } : {}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Three-state note, honoured only by update_analysis_file_artifact:
|
|
182
|
+
* omitted = preserve, null = clear, string = set.
|
|
183
|
+
*/
|
|
184
|
+
function updateNotePayload(): { note?: string | null } {
|
|
185
|
+
if (noteInput.value === initialNote) return {}
|
|
186
|
+
const value = noteInput.value.trim()
|
|
187
|
+
return value ? { note: value } : { note: null }
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function buildPayload(experimentId: number): ArtifactSaveDialogPayload | null {
|
|
191
|
+
const base = {
|
|
192
|
+
experimentId,
|
|
193
|
+
artifactKey: effectiveKey.value,
|
|
194
|
+
}
|
|
195
|
+
if (activeKind.value === 'json') {
|
|
196
|
+
if (props.result === undefined) return null
|
|
197
|
+
// Upsert only sets display_name when provided; pass the existing one through
|
|
198
|
+
// on update so a re-save never blanks it.
|
|
199
|
+
const displayName = isUpdate.value
|
|
200
|
+
? props.existingArtifact?.display_name
|
|
201
|
+
: displayNameInput.value.trim() || undefined
|
|
202
|
+
return {
|
|
203
|
+
kind: 'json',
|
|
204
|
+
mode: isUpdate.value || keyExists.value ? 'upsert' : 'create',
|
|
205
|
+
...base,
|
|
206
|
+
...createNotePayload(),
|
|
207
|
+
...(displayName ? { displayName } : {}),
|
|
208
|
+
result: props.result,
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (isUpdate.value) {
|
|
212
|
+
// Metadata replaces rather than merges backend-side, so send the full intended
|
|
213
|
+
// map: the caller's override, else whatever the artifact already carries.
|
|
214
|
+
const metadata = props.metadata ?? existingMetadata.value
|
|
215
|
+
if (props.fileSource === 'handler') {
|
|
216
|
+
return {
|
|
217
|
+
kind: 'file',
|
|
218
|
+
source: 'handler',
|
|
219
|
+
mode: 'update',
|
|
220
|
+
...base,
|
|
221
|
+
...updateNotePayload(),
|
|
222
|
+
fileKind: existingFileKind.value ?? props.defaultFileKind,
|
|
223
|
+
...(metadata ? { metadata } : {}),
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const file = selectedFile.value
|
|
227
|
+
if (!file) return null
|
|
228
|
+
return {
|
|
229
|
+
kind: 'file',
|
|
230
|
+
mode: 'update',
|
|
231
|
+
...base,
|
|
232
|
+
...updateNotePayload(),
|
|
233
|
+
file,
|
|
234
|
+
// kind is immutable backend-side; always echo the artifact's own kind.
|
|
235
|
+
fileKind: existingFileKind.value ?? props.defaultFileKind,
|
|
236
|
+
...(metadata ? { metadata } : {}),
|
|
237
|
+
...(file.type ? { contentType: file.type } : {}),
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
const displayName = displayNameInput.value.trim()
|
|
241
|
+
if (props.fileSource === 'handler') {
|
|
242
|
+
return {
|
|
243
|
+
kind: 'file',
|
|
244
|
+
source: 'handler',
|
|
245
|
+
mode: 'create',
|
|
246
|
+
...base,
|
|
247
|
+
...createNotePayload(),
|
|
248
|
+
fileKind: props.defaultFileKind,
|
|
249
|
+
...(displayName ? { displayName } : {}),
|
|
250
|
+
...(props.metadata ? { metadata: props.metadata } : {}),
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const file = selectedFile.value
|
|
254
|
+
if (!file) return null
|
|
255
|
+
return {
|
|
256
|
+
kind: 'file',
|
|
257
|
+
mode: 'create',
|
|
258
|
+
...base,
|
|
259
|
+
...createNotePayload(),
|
|
260
|
+
file,
|
|
261
|
+
fileKind: props.defaultFileKind,
|
|
262
|
+
filename: file.name,
|
|
263
|
+
...(displayName ? { displayName } : {}),
|
|
264
|
+
...(props.metadata ? { metadata: props.metadata } : {}),
|
|
265
|
+
...(file.type ? { contentType: file.type } : {}),
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function runSaveHandler(
|
|
270
|
+
payload: ArtifactSaveDialogPayload,
|
|
271
|
+
): Promise<ArtifactSaveResult | void> {
|
|
272
|
+
if (props.fileSource === 'handler') {
|
|
273
|
+
if (payload.kind === 'file' && !('source' in payload)) {
|
|
274
|
+
throw new Error('ArtifactSaveDialog built an upload payload for a handler-owned file.')
|
|
275
|
+
}
|
|
276
|
+
return (props.saveHandler as HandlerArtifactSaveHandler)(payload)
|
|
277
|
+
}
|
|
278
|
+
if (payload.kind === 'file' && 'source' in payload) {
|
|
279
|
+
throw new Error('ArtifactSaveDialog built a handler-owned payload for an uploaded file.')
|
|
280
|
+
}
|
|
281
|
+
return (props.saveHandler as ArtifactSaveHandler)(payload)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function handleFileUpload(files: File[]): void {
|
|
285
|
+
selectedFile.value = files[0] ?? null
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function handleFileError(message: string): void {
|
|
289
|
+
request.setError(message)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function handleSubmit(): Promise<void> {
|
|
293
|
+
if (!canSubmit.value) return
|
|
294
|
+
const experimentId = resolveExperimentId()
|
|
295
|
+
if (experimentId === undefined) {
|
|
296
|
+
const message = request.setError('No experiment is available for saving an artifact.')
|
|
297
|
+
emit('error', message)
|
|
298
|
+
return
|
|
299
|
+
}
|
|
300
|
+
const payload = buildPayload(experimentId)
|
|
301
|
+
if (!payload) return
|
|
302
|
+
try {
|
|
303
|
+
const result = await request.run(
|
|
304
|
+
() => runSaveHandler(payload),
|
|
305
|
+
{ success: 'save', errorMessage: 'Failed to save analysis artifact.' },
|
|
306
|
+
)
|
|
307
|
+
emit('saved', { ...(result ?? {}), payload })
|
|
308
|
+
emit('update:modelValue', false)
|
|
309
|
+
resetForm()
|
|
310
|
+
} catch {
|
|
311
|
+
emit('error', request.error.value ?? 'Failed to save analysis artifact.')
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function handleCancel(): void {
|
|
316
|
+
emit('cancel')
|
|
317
|
+
emit('update:modelValue', false)
|
|
318
|
+
}
|
|
319
|
+
</script>
|
|
320
|
+
|
|
321
|
+
<template>
|
|
322
|
+
<BaseModal
|
|
323
|
+
:model-value="modelValue"
|
|
324
|
+
:title="resolvedTitle"
|
|
325
|
+
:size="size"
|
|
326
|
+
:closable="!request.loading.value"
|
|
327
|
+
@update:model-value="emit('update:modelValue', $event)"
|
|
328
|
+
@close="emit('cancel')"
|
|
329
|
+
>
|
|
330
|
+
<div class="mint-artifact-save-dialog">
|
|
331
|
+
<SegmentedControl
|
|
332
|
+
v-if="showKindToggle"
|
|
333
|
+
v-model="activeKind"
|
|
334
|
+
class="mint-artifact-save-dialog__kind-toggle"
|
|
335
|
+
:options="[
|
|
336
|
+
{ value: 'file', label: 'File' },
|
|
337
|
+
{ value: 'json', label: 'JSON' },
|
|
338
|
+
]"
|
|
339
|
+
full-width
|
|
340
|
+
/>
|
|
341
|
+
|
|
342
|
+
<AlertBox
|
|
343
|
+
v-if="keyExists && !keyBlocked"
|
|
344
|
+
type="warning"
|
|
345
|
+
class="mint-artifact-save-dialog__warning"
|
|
346
|
+
>
|
|
347
|
+
An artifact with key "{{ effectiveKey }}" already exists. Its results will be
|
|
348
|
+
replaced; its name and note are kept unless you set new ones.
|
|
349
|
+
</AlertBox>
|
|
350
|
+
|
|
351
|
+
<FormField
|
|
352
|
+
label="Artifact key"
|
|
353
|
+
class="mint-artifact-save-dialog__key"
|
|
354
|
+
:error="keyError"
|
|
355
|
+
:hint="isUpdate ? 'Keys are fixed once an artifact exists.' : undefined"
|
|
356
|
+
>
|
|
357
|
+
<BaseInput
|
|
358
|
+
v-model="keyInput"
|
|
359
|
+
:readonly="isUpdate"
|
|
360
|
+
:error="Boolean(keyError)"
|
|
361
|
+
:placeholder="defaultArtifactKey"
|
|
362
|
+
/>
|
|
363
|
+
</FormField>
|
|
364
|
+
|
|
365
|
+
<FormField
|
|
366
|
+
v-if="!isUpdate"
|
|
367
|
+
label="Display name"
|
|
368
|
+
class="mint-artifact-save-dialog__name"
|
|
369
|
+
>
|
|
370
|
+
<BaseInput
|
|
371
|
+
v-model="displayNameInput"
|
|
372
|
+
placeholder="Shown in artifact lists"
|
|
373
|
+
/>
|
|
374
|
+
</FormField>
|
|
375
|
+
|
|
376
|
+
<FormField
|
|
377
|
+
v-if="showNoteField"
|
|
378
|
+
label="Note"
|
|
379
|
+
class="mint-artifact-save-dialog__note"
|
|
380
|
+
>
|
|
381
|
+
<BaseTextarea
|
|
382
|
+
v-model="noteInput"
|
|
383
|
+
placeholder="Optional context for this artifact"
|
|
384
|
+
/>
|
|
385
|
+
</FormField>
|
|
386
|
+
|
|
387
|
+
<template v-if="activeKind === 'file'">
|
|
388
|
+
<FileUploader
|
|
389
|
+
v-if="showFileUploader"
|
|
390
|
+
class="mint-artifact-save-dialog__file"
|
|
391
|
+
:accept="accept"
|
|
392
|
+
:max-size="maxFileSize"
|
|
393
|
+
:multiple="false"
|
|
394
|
+
@upload="handleFileUpload"
|
|
395
|
+
@error="handleFileError"
|
|
396
|
+
/>
|
|
397
|
+
<p
|
|
398
|
+
v-if="isUpdate && existingFilename"
|
|
399
|
+
class="mint-artifact-save-dialog__immutable"
|
|
400
|
+
>
|
|
401
|
+
Replaces the content of <code>{{ existingFilename }}</code> (kind
|
|
402
|
+
<code>{{ existingFileKind ?? defaultFileKind }}</code>); filename and kind stay unchanged.
|
|
403
|
+
</p>
|
|
404
|
+
</template>
|
|
405
|
+
|
|
406
|
+
<AlertBox
|
|
407
|
+
v-if="request.error.value"
|
|
408
|
+
type="error"
|
|
409
|
+
class="mint-artifact-save-dialog__error"
|
|
410
|
+
role="alert"
|
|
411
|
+
>
|
|
412
|
+
{{ request.error.value }}
|
|
413
|
+
</AlertBox>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
416
|
+
<template #footer>
|
|
417
|
+
<BaseButton variant="secondary" :disabled="request.loading.value" @click="handleCancel">
|
|
418
|
+
Cancel
|
|
419
|
+
</BaseButton>
|
|
420
|
+
<BaseButton
|
|
421
|
+
class="mint-artifact-save-dialog__submit"
|
|
422
|
+
:loading="request.loading.value"
|
|
423
|
+
:disabled="!canSubmit"
|
|
424
|
+
@click="handleSubmit"
|
|
425
|
+
>
|
|
426
|
+
{{ isUpdate ? 'Update Artifact' : 'Save Artifact' }}
|
|
427
|
+
</BaseButton>
|
|
428
|
+
</template>
|
|
429
|
+
</BaseModal>
|
|
430
|
+
</template>
|
|
431
|
+
|
|
432
|
+
<style>
|
|
433
|
+
@import '../styles/components/artifact-save-dialog.css';
|
|
434
|
+
</style>
|