@morscherlab/mint-sdk 1.0.57 → 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/analysisArtifactTypes-Cu40dzSG.js.map +1 -1
- package/dist/components/ArtifactSaveDialog.vue.d.ts +7 -4
- package/dist/components/index.js +1 -1
- package/dist/{components-PC3SVsaa.js → components-CpnOaPbP.js} +40 -9
- package/dist/{components-PC3SVsaa.js.map → components-CpnOaPbP.js.map} +1 -1
- package/dist/index.js +1 -1
- package/dist/install.js +1 -1
- package/dist/types/analysisArtifactTypes.d.ts +33 -0
- package/dist/types/index.d.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/components/ArtifactSaveDialog.test.ts +55 -5
- package/src/components/ArtifactSaveDialog.story.vue +31 -5
- package/src/components/ArtifactSaveDialog.vue +61 -10
- package/src/types/analysisArtifactTypes.ts +48 -0
- package/src/types/index.ts +7 -0
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-
|
|
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
|
@@ -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 {};
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "1.0.58",
|
|
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:
|
|
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:
|
|
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).
|
|
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
|
|
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<
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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')
|
|
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):
|
|
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
|
-
() =>
|
|
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="
|
|
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
|
package/src/types/index.ts
CHANGED
|
@@ -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
|
|