@morscherlab/mint-sdk 1.0.57 → 1.0.59

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/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { $ as AutoGroupModal_default, A as ScientificNumber_default, At as ColorSlider_default, B as BioTemplateRenderer_default, C as estimateSequenceFinishDate, Ct as AppToastContainer_default, D as sequenceProgressPercent, Dt as DropdownButton_default, E as formatSequenceRemaining, Et as Calendar_default, F as GroupAssigner_default, G as StatusIndicator_default, H as Breadcrumb_default, I as RackEditor_default, J as DoseDesignWorkspaceView_default, K as Divider_default, L as BioTemplatePresetWorkspaceView_default, M as SampleHierarchyTree_default, N as ReagentEditor_default, O as sequenceSamplesRemaining, Ot as SegmentedControl_default, P as SmartGroupModal_default, Q as SampleSelector_default, R as BioTemplatePackWorkspaceView_default, S as SequenceProgressBar_default, St as IconButton_default, T as formatSequenceEta, Tt as DataFrame_default, U as Avatar_default, V as ChartContainer_default, W as ProgressBar_default, X as ComponentBindingRenderer_default, Y as ControlWorkspaceView_default, Z as ScheduleCalendar_default, _ as lcmsWellIdFromPosition, _t as AppPluginSwitcher_default, a as ResourceCard_default, at as SampleLegend_default, b as InstrumentStateBadge_default, bt as CollapsibleCard_default, c as JobsStatusTray_default, ct as DoseCalculator_default, d as LcmsSequenceTable_default, dt as AppLayout_default, et as SmartGroupManual_default, f as DEFAULT_LCMS_SEQUENCE_COLUMNS, ft as AppSidebar_default, g as inferLcmsPlateTypeFromWellIds, gt as AppTopBar_default, h as extractLcmsSampleName, ht as StepWizard_default, i as ArtifactSelectorModal_default, it as PlateMapEditor_default, j as ProtocolStepEditor_default, k as ChemicalFormula_default, kt as BaseTabs_default, l as BatchProgressList_default, lt as PluginWorkspaceView_default, m as extractLcmsCommonPrefix, mt as FormActions_default, n as FitPanel_default, nt as LoadingSpinner_default, o as TimeRangeInput_default, ot as WellPlate_default, p as basenameFromWindowsPath, pt as FormBuilder_default, q as AppContainer_default, r as ArtifactSaveDialog_default, rt as ReagentList_default, s as ExperimentDataViewer_default, st as ExperimentTimeline_default, tt as SmartGroupFieldRecipe_default, u as AuditTrail_default, ut as MobileSupportGate_default, v as reconstructLcmsPlateCellsFromSequenceItems, vt as AppAvatarMenu_default, w as estimateSequenceRemainingSeconds, wt as AlertBox_default, x as InstrumentAlertLog_default, xt as ThemeToggle_default, y as InstrumentStatusCard_default, yt as PluginIcon_default, z as BioTemplateExperimentWorkspaceView_default } from "./components-PC3SVsaa.js";
1
+ import { $ as AutoGroupModal_default, A as ScientificNumber_default, At as ColorSlider_default, B as BioTemplateRenderer_default, C as estimateSequenceFinishDate, Ct as AppToastContainer_default, D as sequenceProgressPercent, Dt as DropdownButton_default, E as formatSequenceRemaining, Et as Calendar_default, F as GroupAssigner_default, G as StatusIndicator_default, H as Breadcrumb_default, I as RackEditor_default, J as DoseDesignWorkspaceView_default, K as Divider_default, L as BioTemplatePresetWorkspaceView_default, M as SampleHierarchyTree_default, N as ReagentEditor_default, O as sequenceSamplesRemaining, Ot as SegmentedControl_default, P as SmartGroupModal_default, Q as SampleSelector_default, R as BioTemplatePackWorkspaceView_default, S as SequenceProgressBar_default, St as IconButton_default, T as formatSequenceEta, Tt as DataFrame_default, U as Avatar_default, V as ChartContainer_default, W as ProgressBar_default, X as ComponentBindingRenderer_default, Y as ControlWorkspaceView_default, Z as ScheduleCalendar_default, _ as lcmsWellIdFromPosition, _t as AppPluginSwitcher_default, a as ResourceCard_default, at as SampleLegend_default, b as InstrumentStateBadge_default, bt as CollapsibleCard_default, c as JobsStatusTray_default, ct as DoseCalculator_default, d as LcmsSequenceTable_default, dt as AppLayout_default, et as SmartGroupManual_default, f as DEFAULT_LCMS_SEQUENCE_COLUMNS, ft as AppSidebar_default, g as inferLcmsPlateTypeFromWellIds, gt as AppTopBar_default, h as extractLcmsSampleName, ht as StepWizard_default, i as ArtifactSelectorModal_default, it as PlateMapEditor_default, j as ProtocolStepEditor_default, k as ChemicalFormula_default, kt as BaseTabs_default, l as BatchProgressList_default, lt as PluginWorkspaceView_default, m as extractLcmsCommonPrefix, mt as FormActions_default, n as FitPanel_default, nt as LoadingSpinner_default, o as TimeRangeInput_default, ot as WellPlate_default, p as basenameFromWindowsPath, pt as FormBuilder_default, q as AppContainer_default, r as ArtifactSaveDialog_default, rt as ReagentList_default, s as ExperimentDataViewer_default, st as ExperimentTimeline_default, tt as SmartGroupFieldRecipe_default, u as AuditTrail_default, ut as MobileSupportGate_default, v as reconstructLcmsPlateCellsFromSequenceItems, vt as AppAvatarMenu_default, w as estimateSequenceRemainingSeconds, wt as AlertBox_default, x as InstrumentAlertLog_default, xt as ThemeToggle_default, y as InstrumentStatusCard_default, yt as PluginIcon_default, z as BioTemplateExperimentWorkspaceView_default } from "./components-CpnOaPbP.js";
2
2
  import { t as BaseButton_default } from "./BaseButton-Dgqrze41.js";
3
3
  import { r as BaseInput_default, t as BaseSelect_default } from "./BaseSelect-ekgr9fDo.js";
4
4
  import { $ as parseTime, A as useConcentrationUnits, B as TagsInput_default, C as controlsToViewIds, D as getTypeDefault, E as getFieldRegistryEntry, F as ATOMIC_WEIGHTS, G as findAvailableSlots, H as addMinutes, I as useChemicalFormula, J as formatTime, K as findNearestTimeSlotIndex, L as DateTimePicker_default, M as SequenceInput_default, N as useSequenceUtils, O as ConcentrationInput_default, P as FormulaInput_default, Q as isTimeInRange, R as FileUploader_default, S as controlsToTopBarSettingsConfig, T as getDefaultControlView, U as compareTime, V as TimePicker_default, W as durationMinutes, X as fromMinutes, Y as formatTimeSlot, Z as generateTimeSlots, _ as controlsToFormSchema, a as defineDoseCalculatorControlProps, at as MultiSelect_default, b as controlsToSettingsSchema, c as defineWellPlateDoseComponentBindings, ct as BaseSlider_default, d as defineControlModel, dt as BaseCheckbox_default, et as rangesOverlap, ft as Tooltip_default, h as controlValuesToComponentProps, i as useControlSchema, it as DatePicker_default, j as MoleculeInput_default, k as UnitInput_default, l as defineWellPlateDoseControlProps, lt as BaseRadioGroup_default, m as controlValuesToComponentBindingsById, n as defineControls, nt as toMinutes, o as defineDoseDesignControlModel, ot as useListSelection, p as controlValuesToComponentBindings, pt as BaseTextarea_default, q as formatDuration, r as getControlDefaults, rt as useTimeUtils, s as defineWellPlateControlProps, st as useSelectionLimit, t as defineControlComponentBindings, tt as snapToSlot, u as useControlWorkspace, ut as BaseToggle_default, v as controlsToSectionFormSchema, w as controlsToViewItems, x as controlsToSidebarPanels, y as controlsToSectionFormSchemas, z as NumberInput_default } from "./useControlSchema-BZNdalmL.js";
package/dist/install.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as components_exports } from "./components-PC3SVsaa.js";
1
+ import { t as components_exports } from "./components-CpnOaPbP.js";
2
2
  import "./BaseButton-Dgqrze41.js";
3
3
  import "./BaseSelect-ekgr9fDo.js";
4
4
  import "./useControlSchema-BZNdalmL.js";
@@ -39,6 +39,8 @@ export interface AnalysisArtifactMetadataUpdate {
39
39
  }
40
40
  /** How a save payload should be applied by the plugin backend. */
41
41
  export type ArtifactSaveMode = 'create' | 'upsert' | 'update';
42
+ /** Where file content comes from when a plugin uses ArtifactSaveDialog. */
43
+ export type ArtifactFileSource = 'upload' | 'handler';
42
44
  interface ArtifactSavePayloadBase {
43
45
  experimentId: number;
44
46
  artifactKey: string;
@@ -85,7 +87,34 @@ export interface FileArtifactUpdatePayload extends ArtifactSavePayloadBase {
85
87
  contentType?: string;
86
88
  metadata?: Record<string, unknown>;
87
89
  }
90
+ /**
91
+ * File artifact create where the plugin handler produces the bytes itself.
92
+ * This is intended for server-side serialization of in-memory analysis state;
93
+ * the browser neither fabricates nor uploads a placeholder File.
94
+ */
95
+ export interface HandlerFileArtifactCreatePayload extends ArtifactSavePayloadBase {
96
+ kind: 'file';
97
+ mode: 'create';
98
+ source: 'handler';
99
+ fileKind: string;
100
+ displayName?: string;
101
+ note?: string;
102
+ metadata?: Record<string, unknown>;
103
+ }
104
+ /** File artifact replacement where the plugin handler produces the bytes itself. */
105
+ export interface HandlerFileArtifactUpdatePayload extends ArtifactSavePayloadBase {
106
+ kind: 'file';
107
+ mode: 'update';
108
+ source: 'handler';
109
+ fileKind: string;
110
+ note?: string | null;
111
+ metadata?: Record<string, unknown>;
112
+ }
88
113
  export type ArtifactSavePayload = JsonArtifactSavePayload | FileArtifactCreatePayload | FileArtifactUpdatePayload;
114
+ /** Payloads accepted when `ArtifactSaveDialog.fileSource` is `handler`. */
115
+ export type HandlerArtifactSavePayload = JsonArtifactSavePayload | HandlerFileArtifactCreatePayload | HandlerFileArtifactUpdatePayload;
116
+ /** Every payload variant the dialog can emit across both file sources. */
117
+ export type ArtifactSaveDialogPayload = ArtifactSavePayload | HandlerFileArtifactCreatePayload | HandlerFileArtifactUpdatePayload;
89
118
  /** Optional handler result; cleanupPending surfaces AnalysisFileArtifactUpdate.cleanup_pending. */
90
119
  export interface ArtifactSaveResult {
91
120
  artifact?: AnalysisArtifactSummary;
@@ -97,4 +126,8 @@ export interface ArtifactSaveResult {
97
126
  * save_analysis_artifact / save_analysis_file_artifact / update_analysis_file_artifact.
98
127
  */
99
128
  export type ArtifactSaveHandler = (payload: ArtifactSavePayload) => Promise<ArtifactSaveResult | void>;
129
+ /** Save handler paired with `ArtifactSaveDialog.fileSource="handler"`. */
130
+ export type HandlerArtifactSaveHandler = (payload: HandlerArtifactSavePayload) => Promise<ArtifactSaveResult | void>;
131
+ /** Handler accepted by ArtifactSaveDialog; `fileSource` selects the payload family. */
132
+ export type ArtifactSaveDialogHandler = ArtifactSaveHandler | HandlerArtifactSaveHandler;
100
133
  export {};
@@ -1,5 +1,5 @@
1
1
  export type { ContainerDirection, ButtonVariant, ButtonSize, InputType, ModalSize, ModalVariant, AlertType, Toast, TabItem, TabItemInput, OptionPrimitive, SelectOption, SelectOptionInput, RadioOption, RadioOptionInput, FormFieldProps, SidebarBadgeTone, SidebarToolSection, SidebarToolSectionAction, CollapsibleState, TopBarVariant, TopBarSettingsConfig, PillNavOption, PillNavOptionInput, PillNavItem, PillNavItemInput, PageSelectorItem, PageSelectorItemInput, PluginSwitcherPlugin, PluginSwitcherInfo, AccountMenuItem, WellPlateFormat, WellState, WellPlateSelectionMode, WellPlateSize, WellShape, Well, HeatmapColorScale, HeatmapConfig, SlotPosition, WellExtendedData, WellEditData, WellEditField, WellSampleDropData, WellSampleDropContext, WellSampleDropParser, WellLegendItem, PlateCondition, ColumnCondition, RowCondition, Rack, RackSampleDropContext, RackSampleDropMapper, SampleType, PlateMap, PlateMapEditorState, ProtocolStepType, ProtocolStepStatus, ProtocolStep, SampleGroup, GroupItem, FileUploaderMode, SegmentedOption, SegmentedOptionInput, SegmentedControlVariant, SegmentedControlSize, MultiSelectOption, MultiSelectOptionInput, MultiSelectSize, PillVariant, PillColor, PillSize, CalendarSelectionMode, CalendarMarker, CalendarDayContext, SortDirection, SortState, DataFrameColumn, PaginationState, SpinnerSize, SpinnerVariant, DividerSpacing, StatusType, ProgressVariant, ProgressSize, AvatarSize, EmptyStateColor, EmptyStateSize, BreadcrumbItem, BreadcrumbItemInput, TooltipPosition, ConfirmVariant, SettingsTab, SettingsTabInput, SettingsModalLayout, SettingsGroup, SettingsModalSchema, SettingsUserType, SettingsAudience, SettingsAudienceInput, NumberNotation, UnitOption, WizardStep, WizardStepState, AuditEntryType, AuditEntry, BatchItemStatus, BatchItem, BatchSummary, TimePickerFormat, TimeRange, ScheduleView, ScheduleEventStatus, ScheduleEvent, ScheduleBlockedSlot, ScheduleSlotContext, ScheduleEventCreateContext, ScheduleEventUpdateContext, SummarySectionItem, SummarySection, SummaryData, ResourceStatus, ResourceSpec, MoleculeData, ExperimentStatus, DatePreset, ExperimentSortField, ExperimentTypeOption, ExperimentSummary, ExperimentListResponse, ExperimentFilters, FitState, FitResultSummary, StorageCondition, ReagentColumn, Reagent, TreeNodeType, BadgeVariant, TreeNode, } from './components';
2
- export type { AnalysisArtifactStatus, AnalysisArtifactStatusFilter, AnalysisArtifactSummary, AnalysisArtifactDetail, AnalysisArtifactListResponse, AnalysisArtifactMetadataUpdate, ArtifactSaveMode, JsonArtifactSavePayload, FileArtifactCreatePayload, FileArtifactUpdatePayload, ArtifactSavePayload, ArtifactSaveResult, ArtifactSaveHandler, } from './analysisArtifactTypes';
2
+ export type { AnalysisArtifactStatus, AnalysisArtifactStatusFilter, AnalysisArtifactSummary, AnalysisArtifactDetail, AnalysisArtifactListResponse, AnalysisArtifactMetadataUpdate, ArtifactSaveMode, ArtifactFileSource, JsonArtifactSavePayload, FileArtifactCreatePayload, FileArtifactUpdatePayload, HandlerFileArtifactCreatePayload, HandlerFileArtifactUpdatePayload, ArtifactSavePayload, HandlerArtifactSavePayload, ArtifactSaveDialogPayload, ArtifactSaveResult, ArtifactSaveHandler, HandlerArtifactSaveHandler, ArtifactSaveDialogHandler, } from './analysisArtifactTypes';
3
3
  export { ANALYSIS_FILE_ARTIFACT_SCHEMA } from './analysisArtifactTypes';
4
4
  export type { JobListPayload, JobProgress, JobSnapshotPayload, JobState, JobStateInput, JobStatus, JobStreamData, } from './jobs';
5
5
  export type { InstrumentAlert, InstrumentAlertBody, InstrumentAlertLevel, InstrumentState, InstrumentStatus, SampleInfo, SampleInfo as InstrumentSampleInfo, SequenceProgress, } from './instrument';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@morscherlab/mint-sdk",
3
- "version": "1.0.57",
3
+ "version": "1.0.59",
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",
@@ -1,11 +1,12 @@
1
1
  import { mount, flushPromises } from '@vue/test-utils'
2
- import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { describe, it, expect, expectTypeOf, vi, beforeEach } from 'vitest'
3
3
  import ArtifactSaveDialog from '../../components/ArtifactSaveDialog.vue'
4
4
  import FileUploader from '../../components/FileUploader.vue'
5
5
  import SegmentedControl from '../../components/SegmentedControl.vue'
6
6
  import { ANALYSIS_FILE_ARTIFACT_SCHEMA } from '../../types/analysisArtifactTypes'
7
7
  import type {
8
8
  AnalysisArtifactDetail,
9
+ ArtifactSaveDialogPayload,
9
10
  ArtifactSavePayload,
10
11
  ArtifactSaveResult,
11
12
  } from '../../types/analysisArtifactTypes'
@@ -46,7 +47,7 @@ function makeFileDetail(overrides: Partial<AnalysisArtifactDetail> = {}): Analys
46
47
  }
47
48
 
48
49
  function mountDialog(props: Record<string, unknown> = {}) {
49
- const saveHandler = vi.fn<(payload: ArtifactSavePayload) => Promise<ArtifactSaveResult | void>>()
50
+ const saveHandler = vi.fn<(payload: ArtifactSaveDialogPayload) => Promise<ArtifactSaveResult | void>>()
50
51
  .mockResolvedValue(undefined)
51
52
  const wrapper = mount(ArtifactSaveDialog, {
52
53
  props: {
@@ -82,6 +83,12 @@ describe('ArtifactSaveDialog', () => {
82
83
  window.history.replaceState({}, '', '/')
83
84
  })
84
85
 
86
+ it('should keep browser File required in the existing upload handler contract', () => {
87
+ type UploadFilePayload = Extract<ArtifactSavePayload, { kind: 'file' }>
88
+
89
+ expectTypeOf<UploadFilePayload['file']>().toEqualTypeOf<File>()
90
+ })
91
+
85
92
  // -------------------------------------------------------------------------
86
93
  // JSON create
87
94
  // -------------------------------------------------------------------------
@@ -131,7 +138,7 @@ describe('ArtifactSaveDialog', () => {
131
138
 
132
139
  const saved = wrapper.emitted('saved')
133
140
  expect(saved).toHaveLength(1)
134
- expect((saved![0][0] as { payload: ArtifactSavePayload }).payload.kind).toBe('json')
141
+ expect((saved![0][0] as { payload: ArtifactSaveDialogPayload }).payload.kind).toBe('json')
135
142
  expect(wrapper.emitted('update:modelValue')?.at(-1)).toEqual([false])
136
143
  })
137
144
 
@@ -193,14 +200,16 @@ describe('ArtifactSaveDialog', () => {
193
200
 
194
201
  expect(saveHandler).toHaveBeenCalledOnce()
195
202
  const payload = saveHandler.mock.calls[0][0]
196
- expect(payload).toMatchObject({
203
+ expect(payload).toEqual({
197
204
  kind: 'file',
198
205
  mode: 'create',
199
206
  experimentId: 42,
200
207
  artifactKey: 'default',
208
+ file: TEST_FILE,
201
209
  fileKind: 'file',
210
+ filename: 'plot.png',
211
+ contentType: 'image/png',
202
212
  })
203
- expect((payload as { file: File }).file).toBe(TEST_FILE)
204
213
  })
205
214
 
206
215
  it('should use the defaultFileKind prop', async () => {
@@ -211,6 +220,27 @@ describe('ArtifactSaveDialog', () => {
211
220
 
212
221
  expect((saveHandler.mock.calls[0][0] as { fileKind: string }).fileKind).toBe('plot')
213
222
  })
223
+
224
+ it('should hide the uploader when file content is owned by the handler', () => {
225
+ const { wrapper } = mountDialog({ mode: 'file', fileSource: 'handler' })
226
+
227
+ expect(wrapper.findComponent(FileUploader).exists()).toBe(false)
228
+ })
229
+
230
+ it('should submit a handler-owned file create without a browser File', async () => {
231
+ const { wrapper, saveHandler } = mountDialog({ mode: 'file', fileSource: 'handler' })
232
+
233
+ await submit(wrapper)
234
+
235
+ expect(saveHandler.mock.calls[0][0]).toEqual({
236
+ kind: 'file',
237
+ source: 'handler',
238
+ mode: 'create',
239
+ experimentId: 42,
240
+ artifactKey: 'default',
241
+ fileKind: 'file',
242
+ })
243
+ })
214
244
  })
215
245
 
216
246
  // -------------------------------------------------------------------------
@@ -435,6 +465,26 @@ describe('ArtifactSaveDialog', () => {
435
465
 
436
466
  expect(saveHandler.mock.calls[0][0].note).toBe('re-measured')
437
467
  })
468
+
469
+ it('should submit a handler-owned file update without a browser File', async () => {
470
+ const { wrapper, saveHandler } = mountDialog({
471
+ existingArtifact: makeFileDetail(),
472
+ fileSource: 'handler',
473
+ })
474
+ await wrapper.get('.mint-artifact-save-dialog__note textarea').setValue('')
475
+
476
+ await submit(wrapper)
477
+
478
+ expect(saveHandler.mock.calls[0][0]).toEqual({
479
+ kind: 'file',
480
+ source: 'handler',
481
+ mode: 'update',
482
+ experimentId: 42,
483
+ artifactKey: 'report',
484
+ fileKind: 'report',
485
+ note: null,
486
+ })
487
+ })
438
488
  })
439
489
 
440
490
  // -------------------------------------------------------------------------
@@ -4,7 +4,7 @@ import ArtifactSaveDialog from './ArtifactSaveDialog.vue'
4
4
  import {
5
5
  ANALYSIS_FILE_ARTIFACT_SCHEMA,
6
6
  type AnalysisArtifactDetail,
7
- type ArtifactSavePayload,
7
+ type ArtifactSaveDialogPayload,
8
8
  } from '../types/analysisArtifactTypes'
9
9
 
10
10
  // -- Mock JSON result a plugin would hand the dialog --
@@ -48,10 +48,10 @@ const existingJsonArtifact: AnalysisArtifactDetail = {
48
48
  }
49
49
 
50
50
  // -- Mock handlers standing in for a generated plugin-client call --
51
- const lastPayload = ref<ArtifactSavePayload | null>(null)
51
+ const lastPayload = ref<ArtifactSaveDialogPayload | null>(null)
52
52
 
53
53
  /** Files can't be JSON-serialized; show their name instead. */
54
- function formatPayload(payload: ArtifactSavePayload): string {
54
+ function formatPayload(payload: ArtifactSaveDialogPayload): string {
55
55
  return JSON.stringify(
56
56
  payload,
57
57
  (_key, value) => (value instanceof File ? `File(${value.name})` : value),
@@ -63,13 +63,13 @@ function delay(ms: number) {
63
63
  return new Promise((resolve) => setTimeout(resolve, ms))
64
64
  }
65
65
 
66
- async function saveHandler(payload: ArtifactSavePayload) {
66
+ async function saveHandler(payload: ArtifactSaveDialogPayload) {
67
67
  lastPayload.value = payload
68
68
  await delay(600)
69
69
  return { cleanupPending: payload.mode === 'update' }
70
70
  }
71
71
 
72
- async function failingHandler(payload: ArtifactSavePayload) {
72
+ async function failingHandler(payload: ArtifactSaveDialogPayload) {
73
73
  lastPayload.value = payload
74
74
  await delay(600)
75
75
  throw new Error('Plugin backend unavailable (503)')
@@ -83,6 +83,7 @@ const isOpenUpdateFile = ref(false)
83
83
  const isOpenUpdateJson = ref(false)
84
84
  const isOpenCollision = ref(false)
85
85
  const isOpenFileConflict = ref(false)
86
+ const isOpenHandlerFile = ref(false)
86
87
  const isOpenError = ref(false)
87
88
  </script>
88
89
 
@@ -196,6 +197,31 @@ const isOpenError = ref(false)
196
197
  </div>
197
198
  </Variant>
198
199
 
200
+ <Variant title="Handler-Owned File">
201
+ <div style="padding: 2rem;">
202
+ <p style="font-size: 0.8125rem; color: var(--text-muted, #94a3b8); margin: 0 0 1rem;">
203
+ The plugin backend serializes the file content. No browser file is requested; the handler
204
+ receives <code>source: 'handler'</code> with the artifact metadata.
205
+ </p>
206
+ <button
207
+ style="padding: 0.5rem 1rem; background: var(--color-primary, #6366F1); color: white; border: none; border-radius: 6px; cursor: pointer; font-size: 0.875rem;"
208
+ @click="isOpenHandlerFile = true"
209
+ >
210
+ Save Server-Generated File
211
+ </button>
212
+
213
+ <ArtifactSaveDialog
214
+ v-model="isOpenHandlerFile"
215
+ :experiment-id="42"
216
+ mode="file"
217
+ file-source="handler"
218
+ default-artifact-key="analysis-session"
219
+ default-file-kind="session"
220
+ :save-handler="saveHandler"
221
+ />
222
+ </div>
223
+ </Variant>
224
+
199
225
  <Variant title="Update JSON Artifact">
200
226
  <div style="padding: 2rem;">
201
227
  <p style="font-size: 0.8125rem; color: var(--text-muted, #94a3b8); margin: 0 0 1rem;">
@@ -17,18 +17,23 @@ import {
17
17
  import {
18
18
  ANALYSIS_FILE_ARTIFACT_SCHEMA,
19
19
  type AnalysisArtifactDetail,
20
+ type ArtifactFileSource,
21
+ type ArtifactSaveDialogHandler,
22
+ type ArtifactSaveDialogPayload,
20
23
  type ArtifactSaveHandler,
21
- type ArtifactSavePayload,
22
24
  type ArtifactSaveResult,
25
+ type HandlerArtifactSaveHandler,
23
26
  } from '../types/analysisArtifactTypes'
24
27
  import type { ModalSize } from '../types'
25
28
 
26
29
  interface Props {
27
30
  modelValue: boolean
28
31
  /** Performs the backend write; the dialog never calls the platform itself. */
29
- saveHandler: ArtifactSaveHandler
32
+ saveHandler: ArtifactSaveDialogHandler
30
33
  /** Which artifact kinds the form offers. */
31
34
  mode?: 'json' | 'file' | 'both'
35
+ /** Use `handler` when the plugin backend serializes the file content itself. */
36
+ fileSource?: ArtifactFileSource
32
37
  /** JSON result payload supplied programmatically by the host. */
33
38
  result?: Record<string, unknown>
34
39
  /** Experiment id override; defaults to platform injection or URL. */
@@ -57,6 +62,7 @@ interface Props {
57
62
 
58
63
  const props = withDefaults(defineProps<Props>(), {
59
64
  mode: 'file',
65
+ fileSource: 'upload',
60
66
  experimentId: null,
61
67
  existingArtifact: null,
62
68
  size: 'md',
@@ -66,7 +72,7 @@ const props = withDefaults(defineProps<Props>(), {
66
72
 
67
73
  const emit = defineEmits<{
68
74
  'update:modelValue': [value: boolean]
69
- saved: [result: ArtifactSaveResult & { payload: ArtifactSavePayload }]
75
+ saved: [result: ArtifactSaveResult & { payload: ArtifactSaveDialogPayload }]
70
76
  error: [message: string]
71
77
  cancel: []
72
78
  }>()
@@ -128,7 +134,9 @@ watch(
128
134
  const resolvedTitle = computed(() =>
129
135
  props.title ?? (isUpdate.value ? 'Update Analysis Artifact' : 'Save Analysis Artifact'))
130
136
  const showKindToggle = computed(() => !isUpdate.value && props.mode === 'both')
131
- const showFileInput = computed(() => activeKind.value === 'file')
137
+ const showFileUploader = computed(
138
+ () => activeKind.value === 'file' && props.fileSource === 'upload',
139
+ )
132
140
  // save_analysis_artifact ignores note=None (it preserves), so a JSON artifact's
133
141
  // note cannot be cleared through the save path — that belongs to updateMetadata.
134
142
  // update_analysis_file_artifact does honour the not-provided/null/value sentinel.
@@ -153,7 +161,9 @@ const keyError = computed(() => {
153
161
 
154
162
  const canSubmit = computed(() => {
155
163
  if (request.loading.value || !keyValid.value || keyBlocked.value) return false
156
- if (activeKind.value === 'file') return selectedFile.value !== null
164
+ if (activeKind.value === 'file') {
165
+ return props.fileSource === 'handler' || selectedFile.value !== null
166
+ }
157
167
  return props.result !== undefined
158
168
  })
159
169
 
@@ -177,7 +187,7 @@ function updateNotePayload(): { note?: string | null } {
177
187
  return value ? { note: value } : { note: null }
178
188
  }
179
189
 
180
- function buildPayload(experimentId: number): ArtifactSavePayload | null {
190
+ function buildPayload(experimentId: number): ArtifactSaveDialogPayload | null {
181
191
  const base = {
182
192
  experimentId,
183
193
  artifactKey: effectiveKey.value,
@@ -198,12 +208,23 @@ function buildPayload(experimentId: number): ArtifactSavePayload | null {
198
208
  result: props.result,
199
209
  }
200
210
  }
201
- const file = selectedFile.value
202
- if (!file) return null
203
211
  if (isUpdate.value) {
204
212
  // Metadata replaces rather than merges backend-side, so send the full intended
205
213
  // map: the caller's override, else whatever the artifact already carries.
206
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
207
228
  return {
208
229
  kind: 'file',
209
230
  mode: 'update',
@@ -217,6 +238,20 @@ function buildPayload(experimentId: number): ArtifactSavePayload | null {
217
238
  }
218
239
  }
219
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
220
255
  return {
221
256
  kind: 'file',
222
257
  mode: 'create',
@@ -231,6 +266,21 @@ function buildPayload(experimentId: number): ArtifactSavePayload | null {
231
266
  }
232
267
  }
233
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
+
234
284
  function handleFileUpload(files: File[]): void {
235
285
  selectedFile.value = files[0] ?? null
236
286
  }
@@ -251,7 +301,7 @@ async function handleSubmit(): Promise<void> {
251
301
  if (!payload) return
252
302
  try {
253
303
  const result = await request.run(
254
- () => props.saveHandler(payload),
304
+ () => runSaveHandler(payload),
255
305
  { success: 'save', errorMessage: 'Failed to save analysis artifact.' },
256
306
  )
257
307
  emit('saved', { ...(result ?? {}), payload })
@@ -334,8 +384,9 @@ function handleCancel(): void {
334
384
  />
335
385
  </FormField>
336
386
 
337
- <template v-if="showFileInput">
387
+ <template v-if="activeKind === 'file'">
338
388
  <FileUploader
389
+ v-if="showFileUploader"
339
390
  class="mint-artifact-save-dialog__file"
340
391
  :accept="accept"
341
392
  :max-size="maxFileSize"
@@ -48,6 +48,9 @@ export interface AnalysisArtifactMetadataUpdate {
48
48
  /** How a save payload should be applied by the plugin backend. */
49
49
  export type ArtifactSaveMode = 'create' | 'upsert' | 'update'
50
50
 
51
+ /** Where file content comes from when a plugin uses ArtifactSaveDialog. */
52
+ export type ArtifactFileSource = 'upload' | 'handler'
53
+
51
54
  interface ArtifactSavePayloadBase {
52
55
  experimentId: number
53
56
  artifactKey: string
@@ -98,11 +101,48 @@ export interface FileArtifactUpdatePayload extends ArtifactSavePayloadBase {
98
101
  metadata?: Record<string, unknown>
99
102
  }
100
103
 
104
+ /**
105
+ * File artifact create where the plugin handler produces the bytes itself.
106
+ * This is intended for server-side serialization of in-memory analysis state;
107
+ * the browser neither fabricates nor uploads a placeholder File.
108
+ */
109
+ export interface HandlerFileArtifactCreatePayload extends ArtifactSavePayloadBase {
110
+ kind: 'file'
111
+ mode: 'create'
112
+ source: 'handler'
113
+ fileKind: string
114
+ displayName?: string
115
+ note?: string
116
+ metadata?: Record<string, unknown>
117
+ }
118
+
119
+ /** File artifact replacement where the plugin handler produces the bytes itself. */
120
+ export interface HandlerFileArtifactUpdatePayload extends ArtifactSavePayloadBase {
121
+ kind: 'file'
122
+ mode: 'update'
123
+ source: 'handler'
124
+ fileKind: string
125
+ note?: string | null
126
+ metadata?: Record<string, unknown>
127
+ }
128
+
101
129
  export type ArtifactSavePayload =
102
130
  | JsonArtifactSavePayload
103
131
  | FileArtifactCreatePayload
104
132
  | FileArtifactUpdatePayload
105
133
 
134
+ /** Payloads accepted when `ArtifactSaveDialog.fileSource` is `handler`. */
135
+ export type HandlerArtifactSavePayload =
136
+ | JsonArtifactSavePayload
137
+ | HandlerFileArtifactCreatePayload
138
+ | HandlerFileArtifactUpdatePayload
139
+
140
+ /** Every payload variant the dialog can emit across both file sources. */
141
+ export type ArtifactSaveDialogPayload =
142
+ | ArtifactSavePayload
143
+ | HandlerFileArtifactCreatePayload
144
+ | HandlerFileArtifactUpdatePayload
145
+
106
146
  /** Optional handler result; cleanupPending surfaces AnalysisFileArtifactUpdate.cleanup_pending. */
107
147
  export interface ArtifactSaveResult {
108
148
  artifact?: AnalysisArtifactSummary
@@ -117,3 +157,11 @@ export interface ArtifactSaveResult {
117
157
  export type ArtifactSaveHandler = (
118
158
  payload: ArtifactSavePayload,
119
159
  ) => Promise<ArtifactSaveResult | void>
160
+
161
+ /** Save handler paired with `ArtifactSaveDialog.fileSource="handler"`. */
162
+ export type HandlerArtifactSaveHandler = (
163
+ payload: HandlerArtifactSavePayload,
164
+ ) => Promise<ArtifactSaveResult | void>
165
+
166
+ /** Handler accepted by ArtifactSaveDialog; `fileSource` selects the payload family. */
167
+ export type ArtifactSaveDialogHandler = ArtifactSaveHandler | HandlerArtifactSaveHandler
@@ -184,12 +184,19 @@ export type {
184
184
  AnalysisArtifactListResponse,
185
185
  AnalysisArtifactMetadataUpdate,
186
186
  ArtifactSaveMode,
187
+ ArtifactFileSource,
187
188
  JsonArtifactSavePayload,
188
189
  FileArtifactCreatePayload,
189
190
  FileArtifactUpdatePayload,
191
+ HandlerFileArtifactCreatePayload,
192
+ HandlerFileArtifactUpdatePayload,
190
193
  ArtifactSavePayload,
194
+ HandlerArtifactSavePayload,
195
+ ArtifactSaveDialogPayload,
191
196
  ArtifactSaveResult,
192
197
  ArtifactSaveHandler,
198
+ HandlerArtifactSaveHandler,
199
+ ArtifactSaveDialogHandler,
193
200
  } from './analysisArtifactTypes'
194
201
  export { ANALYSIS_FILE_ARTIFACT_SCHEMA } from './analysisArtifactTypes'
195
202