@morscherlab/mint-sdk 1.4.0-beta.1 → 1.4.0-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ActionMenu-CScINahU.js → AlertBox-B2Bgr3ei.js} +40 -209
- package/dist/AlertBox-B2Bgr3ei.js.map +1 -0
- package/dist/{AppSidebar-BmiiPfz9.js → AppSidebar-mvm--GxN.js} +161 -110
- package/dist/AppSidebar-mvm--GxN.js.map +1 -0
- package/dist/{ConfirmDialog-CJ7hqYUP.js → ConfirmDialog-nbJj51is.js} +2 -2
- package/dist/{ConfirmDialog-CJ7hqYUP.js.map → ConfirmDialog-nbJj51is.js.map} +1 -1
- package/dist/ExperimentPopover-5T1fAq2L.js +337 -0
- package/dist/ExperimentPopover-5T1fAq2L.js.map +1 -0
- package/dist/{ExperimentSelectorModal-D-9z7cNx.js → ExperimentSelectorModal-65peG4Up.js} +198 -181
- package/dist/ExperimentSelectorModal-65peG4Up.js.map +1 -0
- package/dist/{SegmentedControl-DERBN-Xd.js → SegmentedControl-BtnnjWzs.js} +2 -2
- package/dist/{SegmentedControl-DERBN-Xd.js.map → SegmentedControl-BtnnjWzs.js.map} +1 -1
- package/dist/{SettingsModal-CSLE31yg.js → SettingsModal-DO3IZ7qz.js} +9 -9
- package/dist/SettingsModal-DO3IZ7qz.js.map +1 -0
- package/dist/components/AppSidebar.vue.d.ts +1 -1
- package/dist/components/ArtifactSaveDialog.vue.d.ts +9 -1
- package/dist/components/AuditTrail.vue.d.ts +1 -1
- package/dist/components/BaseSelect.vue.d.ts +2 -2
- package/dist/components/ChartContainer.vue.d.ts +2 -0
- package/dist/components/DatePicker.vue.d.ts +1 -1
- package/dist/components/ExperimentPopover.vue.d.ts +18 -0
- package/dist/components/ExperimentSelectorModal.vue.d.ts +0 -4
- package/dist/components/FilePicker.vue.d.ts +1 -1
- package/dist/components/FileUploader.vue.d.ts +2 -2
- package/dist/components/PluginIcon.vue.d.ts +2 -2
- package/dist/components/SampleSelector.vue.d.ts +6 -6
- package/dist/components/SequenceProgressBar.vue.d.ts +3 -0
- package/dist/components/StepWizard.vue.d.ts +3 -0
- package/dist/components/internal/AutoGroupModalInternal.vue.d.ts +1 -1
- package/dist/components/internal/DatePickerTimeInternal.vue.d.ts +1 -1
- package/dist/components/internal/ResourcePickerListInternal.vue.d.ts +2 -7
- package/dist/components/internal/SelectMultipleInternal.vue.d.ts +4 -0
- package/dist/composables/createClient.d.ts +2 -0
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/pluginIdentity.d.ts +24 -0
- package/dist/composables/pluginWorkspaceModel.d.ts +2 -2
- package/dist/composables/useAppExperiment.d.ts +8 -0
- package/dist/composables/useCalendarGrid.d.ts +1 -1
- package/dist/composables/useExperimentSelector.d.ts +6 -2
- package/dist/composables/usePlatform.d.ts +1 -0
- package/dist/{controlSchemaModel-CP5-_nAj.js → controlSchemaModel-B8CTXagP.js} +2 -2
- package/dist/{controlSchemaModel-CP5-_nAj.js.map → controlSchemaModel-B8CTXagP.js.map} +1 -1
- package/dist/{data-C2PwI_dn.js → data-CgILxDLW.js} +178 -124
- package/dist/data-CgILxDLW.js.map +1 -0
- package/dist/data.js +8 -8
- package/dist/{experimentStatus-BZmU4czV.js → experimentStatus-Cqp6qNrR.js} +3 -3
- package/dist/{experimentStatus-BZmU4czV.js.map → experimentStatus-Cqp6qNrR.js.map} +1 -1
- package/dist/index.js +19 -19
- package/dist/{install-DAID6c2-.js → install-CsPFJSx3.js} +15 -14
- package/dist/{install-DAID6c2-.js.map → install-CsPFJSx3.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-DLwRl_Ql.js → lab-MHzp-_J8.js} +128 -147
- package/dist/lab-MHzp-_J8.js.map +1 -0
- package/dist/lab.js +4 -3
- package/dist/{records-BzTcxekG.js → records-B6gpM_3e.js} +472 -274
- package/dist/records-B6gpM_3e.js.map +1 -0
- package/dist/{runtime-D6gVat6r.js → runtime-8bs8EogX.js} +27 -31
- package/dist/runtime-8bs8EogX.js.map +1 -0
- package/dist/runtime.js +6 -6
- package/dist/{shell-5ju97nOS.js → shell-CywENwBT.js} +5 -5
- package/dist/{shell-5ju97nOS.js.map → shell-CywENwBT.js.map} +1 -1
- package/dist/shell.js +5 -5
- package/dist/styles.css +1606 -1666
- package/dist/types/componentWorkflowTypes.d.ts +2 -0
- package/dist/types/generated/platformContext.d.ts +1 -0
- package/dist/types/generated-ui.d.ts +4 -0
- package/dist/{BaseModal-DfRIuVoj.js → useDropdownState-8yWMu-7_.js} +60 -2
- package/dist/useDropdownState-8yWMu-7_.js.map +1 -0
- package/dist/{useExperimentSamples-CxcnxZ8h.js → useExperimentSamples-DHnxKFAF.js} +3 -3
- package/dist/{useExperimentSamples-CxcnxZ8h.js.map → useExperimentSamples-DHnxKFAF.js.map} +1 -1
- package/dist/{useListReorder-CcbEYxX-.js → useListReorder-BitdUhxn.js} +109 -277
- package/dist/useListReorder-BitdUhxn.js.map +1 -0
- package/dist/{useRackEditor-BD5NIo5s.js → useRackEditor-DiwCt77M.js} +71 -57
- package/dist/useRackEditor-DiwCt77M.js.map +1 -0
- package/dist/{useRealtime-PalKue84.js → useRealtime-C3iy9QkU.js} +7 -4
- package/dist/useRealtime-C3iy9QkU.js.map +1 -0
- package/dist/useRequest-D506eHi8.js +244 -0
- package/dist/useRequest-D506eHi8.js.map +1 -0
- package/dist/utils/time.d.ts +7 -0
- package/package.json +1 -1
- package/src/__tests__/components/AppTopBar.test.ts +51 -2
- package/src/__tests__/components/AppTopBarPillNav.test.ts +35 -0
- package/src/__tests__/components/ArtifactSaveDialog.test.ts +78 -0
- package/src/__tests__/components/BaseSelect.multiple.test.ts +87 -32
- package/src/__tests__/components/BaseSelect.test.ts +2 -1
- package/src/__tests__/components/BatchProgressList.test.ts +16 -0
- package/src/__tests__/components/CalendarGridPanel.test.ts +2 -2
- package/src/__tests__/components/ChartContainer.test.ts +11 -0
- package/src/__tests__/components/ChemicalFormula.test.ts +6 -3
- package/src/__tests__/components/DatePicker.datetime.test.ts +6 -0
- package/src/__tests__/components/DatePicker.test.ts +21 -0
- package/src/__tests__/components/DatePicker.time.test.ts +2 -2
- package/src/__tests__/components/EmptyState.test.ts +1 -1
- package/src/__tests__/components/ExperimentPopover.test.ts +125 -16
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +119 -13
- package/src/__tests__/components/GeneratedPluginView.test.ts +22 -1
- package/src/__tests__/components/InstrumentStateBadge.test.ts +4 -4
- package/src/__tests__/components/PluginWorkspaceView.test.ts +7 -0
- package/src/__tests__/components/ResourcePickerListInternal.test.ts +0 -20
- package/src/__tests__/components/SampleSelector.test.ts +13 -8
- package/src/__tests__/components/SequenceProgressBar.test.ts +12 -0
- package/src/__tests__/components/StepWizard.test.ts +11 -0
- package/src/__tests__/components/WellPlate.decoration.test.ts +11 -4
- package/src/__tests__/components/deprecationTags.test.ts +1 -1
- package/src/__tests__/composables/useAppExperiment.test.ts +27 -1
- package/src/__tests__/composables/useCalendarGrid.test.ts +4 -2
- package/src/__tests__/composables/useExperimentSelector.test.ts +111 -0
- package/src/__tests__/styles/baseControlsRules.test.ts +6 -2
- package/src/__tests__/styles/controlRefreshRules.test.ts +0 -6
- package/src/__tests__/styles/cssTokens.test.ts +2 -2
- package/src/__tests__/styles/menusShellRefresh.test.ts +14 -10
- package/src/__tests__/utils/plotlyTemplate.test.ts +4 -4
- package/src/__tests__/utils/time.test.ts +18 -1
- package/src/components/AdductText.story.vue +7 -0
- package/src/components/AdductText.vue +1 -1
- package/src/components/AlertBox.story.vue +2 -2
- package/src/components/AlertBox.vue +18 -16
- package/src/components/AppSidebar.story.vue +0 -28
- package/src/components/AppTopBar.story.vue +47 -12
- package/src/components/AppTopBar.vue +118 -114
- package/src/components/ArtifactSaveDialog.story.vue +23 -0
- package/src/components/ArtifactSaveDialog.vue +102 -12
- package/src/components/AuditTrail.story.vue +2 -1
- package/src/components/AuditTrail.vue +2 -21
- package/src/components/Avatar.vue +10 -14
- package/src/components/BaseSelect.multiple.story.vue +13 -2
- package/src/components/BaseSelect.vue +4 -3
- package/src/components/BatchProgressList.vue +35 -25
- package/src/components/ChartContainer.story.vue +2 -1
- package/src/components/ChartContainer.vue +8 -6
- package/src/components/ConfirmDialog.story.vue +1 -1
- package/src/components/EmptyState.vue +1 -1
- package/src/components/ExperimentPopover.story.vue +165 -53
- package/src/components/ExperimentPopover.vue +169 -187
- package/src/components/ExperimentSelectorModal.story.vue +40 -36
- package/src/components/ExperimentSelectorModal.vue +124 -110
- package/src/components/FormField.story.vue +8 -48
- package/src/components/GeneratedPluginView.vue +7 -1
- package/src/components/InstrumentStateBadge.vue +1 -1
- package/src/components/JobsStatusTray.vue +1 -1
- package/src/components/LoadingSpinner.vue +1 -1
- package/src/components/NumberInput.vue +3 -6
- package/src/components/PluginIcon.story.vue +2 -10
- package/src/components/PluginIcon.vue +2 -2
- package/src/components/PluginWorkspaceView.story.vue +1 -1
- package/src/components/RuntimeUpdateNotice.vue +1 -1
- package/src/components/SampleSelector.story.vue +1 -1
- package/src/components/SampleSelector.vue +27 -37
- package/src/components/SampleSelectorGroupNode.vue +1 -1
- package/src/components/SectionCard.story.vue +2 -2
- package/src/components/SequenceProgressBar.story.vue +9 -0
- package/src/components/SequenceProgressBar.vue +6 -1
- package/src/components/SettingsModal.schema.ts +1 -1
- package/src/components/SettingsModal.vue +2 -2
- package/src/components/SmartGroupFieldRecipe.story.vue +1 -1
- package/src/components/SmartGroupManual.story.vue +1 -1
- package/src/components/StepWizard.story.vue +33 -0
- package/src/components/StepWizard.vue +8 -1
- package/src/components/WellPlate.story.vue +3 -1
- package/src/components/WellPlate.vue +7 -0
- package/src/components/internal/AppTopBarPillNavInternal.vue +33 -2
- package/src/components/internal/DatePickerDateInternal.vue +16 -40
- package/src/components/internal/DatePickerDateTimeInternal.vue +3 -13
- package/src/components/internal/DatePickerTimeInternal.vue +5 -8
- package/src/components/internal/ResourcePickerListInternal.vue +45 -85
- package/src/components/internal/SelectMultipleInternal.vue +182 -65
- package/src/components/internal/SelectSearchableInternal.vue +2 -3
- package/src/composables/createClient.ts +2 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/pluginIdentity.ts +31 -0
- package/src/composables/pluginWorkspaceModel.ts +2 -2
- package/src/composables/useAppExperiment.ts +20 -0
- package/src/composables/useCalendarGrid.ts +4 -3
- package/src/composables/useExperimentSelector.ts +77 -31
- package/src/composables/usePluginWorkspace.ts +1 -1
- package/src/styles/components/adduct-text.css +13 -5
- package/src/styles/components/alert-box.css +39 -58
- package/src/styles/components/app-layout.css +6 -5
- package/src/styles/components/app-page-selector.css +3 -1
- package/src/styles/components/app-pill-nav.css +11 -12
- package/src/styles/components/app-sidebar.css +9 -18
- package/src/styles/components/app-top-bar.css +57 -55
- package/src/styles/components/artifact-save-dialog.css +153 -2
- package/src/styles/components/audit-trail.css +16 -19
- package/src/styles/components/avatar.css +6 -4
- package/src/styles/components/batch-progress-list.css +38 -36
- package/src/styles/components/button.css +28 -59
- package/src/styles/components/calendar.css +12 -12
- package/src/styles/components/chart-container.css +34 -23
- package/src/styles/components/checkbox.css +2 -12
- package/src/styles/components/collapsible-card.css +27 -29
- package/src/styles/components/confirm-dialog.css +13 -14
- package/src/styles/components/dataframe.css +12 -4
- package/src/styles/components/date-picker.css +80 -92
- package/src/styles/components/datetime-picker.css +31 -43
- package/src/styles/components/divider.css +20 -20
- package/src/styles/components/dose-calculator.css +3 -3
- package/src/styles/components/empty-state.css +20 -17
- package/src/styles/components/experiment-code-badge.css +2 -0
- package/src/styles/components/experiment-popover.css +174 -329
- package/src/styles/components/experiment-selector-modal.css +75 -56
- package/src/styles/components/file-picker.css +3 -3
- package/src/styles/components/file-uploader.css +38 -49
- package/src/styles/components/form-builder.css +3 -3
- package/src/styles/components/formula-input.css +1 -1
- package/src/styles/components/icon-button.css +18 -24
- package/src/styles/components/input.css +5 -4
- package/src/styles/components/instrument-monitor.css +13 -15
- package/src/styles/components/jobs-status-tray.css +31 -25
- package/src/styles/components/modal.css +2 -4
- package/src/styles/components/molecule-input.css +2 -2
- package/src/styles/components/multi-select.css +65 -117
- package/src/styles/components/number-input.css +26 -19
- package/src/styles/components/pill.css +3 -2
- package/src/styles/components/plate-editor.css +0 -1
- package/src/styles/components/plugin-icon.css +26 -23
- package/src/styles/components/popover.css +2 -2
- package/src/styles/components/presence-avatars.css +1 -1
- package/src/styles/components/protocol-step-editor.css +1 -3
- package/src/styles/components/resource-card.css +2 -2
- package/src/styles/components/resource-picker-list.css +6 -57
- package/src/styles/components/sample-hierarchy-tree.css +4 -4
- package/src/styles/components/sample-selector.css +95 -79
- package/src/styles/components/schedule-calendar.css +8 -8
- package/src/styles/components/searchable-select.css +12 -12
- package/src/styles/components/section-card.css +28 -47
- package/src/styles/components/segmented-control.css +18 -15
- package/src/styles/components/select.css +3 -3
- package/src/styles/components/sequence-input.css +2 -2
- package/src/styles/components/sequence-progress-bar.css +17 -8
- package/src/styles/components/smart-group.css +18 -18
- package/src/styles/components/step-wizard.css +52 -16
- package/src/styles/components/tabs.css +2 -2
- package/src/styles/components/textarea.css +3 -3
- package/src/styles/components/time-picker.css +36 -21
- package/src/styles/components/toast.css +12 -10
- package/src/styles/components/toggle.css +1 -11
- package/src/styles/components/tooltip.css +10 -9
- package/src/styles/components/unit-input.css +8 -7
- package/src/styles/components/well-plate.css +9 -2
- package/src/styles/variables.css +106 -75
- package/src/types/componentWorkflowTypes.ts +2 -0
- package/src/types/generated/platformContext.ts +1 -0
- package/src/types/generated-ui.ts +4 -0
- package/src/utils/time.ts +21 -0
- package/dist/ActionMenu-CScINahU.js.map +0 -1
- package/dist/AppSidebar-BmiiPfz9.js.map +0 -1
- package/dist/BaseModal-DfRIuVoj.js.map +0 -1
- package/dist/ExperimentPopover-C4sGw-is.js +0 -359
- package/dist/ExperimentPopover-C4sGw-is.js.map +0 -1
- package/dist/ExperimentSelectorModal-D-9z7cNx.js.map +0 -1
- package/dist/SettingsModal-CSLE31yg.js.map +0 -1
- package/dist/data-C2PwI_dn.js.map +0 -1
- package/dist/lab-DLwRl_Ql.js.map +0 -1
- package/dist/records-BzTcxekG.js.map +0 -1
- package/dist/runtime-D6gVat6r.js.map +0 -1
- package/dist/useListReorder-CcbEYxX-.js.map +0 -1
- package/dist/useRackEditor-BD5NIo5s.js.map +0 -1
- package/dist/useRealtime-PalKue84.js.map +0 -1
- package/dist/useRequest-D5JDfsgZ.js +0 -76
- package/dist/useRequest-D5JDfsgZ.js.map +0 -1
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Full application top bar with brand logo, static plugin identity, page selector, centered pill nav, experiment popover, and an account slot.
|
|
4
|
+
*
|
|
5
|
+
* The `#icon` slot applies in standalone mode only; integrated branding comes from `Metadata.icon`.
|
|
6
|
+
*/
|
|
7
|
+
import { ref, computed, inject, defineAsyncComponent, onMounted, onBeforeUnmount, toValue, useSlots, watch } from 'vue'
|
|
4
8
|
import type {
|
|
5
9
|
PillNavOption,
|
|
6
10
|
PillNavItem,
|
|
@@ -18,6 +22,7 @@ import AppTopBarPillNavInternal from './internal/AppTopBarPillNavInternal.vue'
|
|
|
18
22
|
import PluginIcon from './PluginIcon.vue'
|
|
19
23
|
import { usePlatform } from '../composables/usePlatform'
|
|
20
24
|
import { APP_EXPERIMENT_KEY } from '../composables/useAppExperiment'
|
|
25
|
+
import { pluginIdentity } from '../composables/pluginIdentity'
|
|
21
26
|
import { integrationHost } from '../utils/integrationHost'
|
|
22
27
|
import { trackTopBarForToasts } from './AppTopBar.toastOffset'
|
|
23
28
|
|
|
@@ -75,12 +80,12 @@ const isStandalone = computed(() => !isIntegrated.value)
|
|
|
75
80
|
const appExperiment = inject(APP_EXPERIMENT_KEY, null)
|
|
76
81
|
const canBindExperiment = computed(() => plugin.value?.plugin_type !== 'static')
|
|
77
82
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
83
|
+
const slots = useSlots()
|
|
84
|
+
// Standalone: the seeded identity (providePluginIdentity) stands in for a missing icon and title.
|
|
85
|
+
const pluginIcon = computed(() => plugin.value?.icon || (slots.logo ? undefined : pluginIdentity.icon))
|
|
86
|
+
const seeded = computed(() => isStandalone.value && !props.title && !props.subtitle)
|
|
87
|
+
const titleText = computed(() => props.title || (seeded.value ? pluginIdentity.name : undefined))
|
|
88
|
+
const subtitleText = computed(() => props.subtitle || (seeded.value ? pluginIdentity.subtitle : undefined))
|
|
84
89
|
|
|
85
90
|
const hasPlatformPageSelector = computed(() =>
|
|
86
91
|
props.pageSelector === undefined && (plugin.value?.nav_items?.length ?? 0) > 1,
|
|
@@ -98,20 +103,22 @@ watch(platformPageSelectorEl, (element, _previous, onCleanup) => {
|
|
|
98
103
|
const identityName = computed(() =>
|
|
99
104
|
isIntegrated.value && !hasPageSelector.value ? plugin.value?.display_name || plugin.value?.name : undefined,
|
|
100
105
|
)
|
|
106
|
+
// Platforms before 1.4 send no subtitle; the description fills line 2.
|
|
107
|
+
const identitySubtitle = computed(() => plugin.value?.subtitle || plugin.value?.description)
|
|
101
108
|
const hasPillNav = computed(() => !!config.value.pills?.length)
|
|
102
109
|
const hasTitleGroup = computed(
|
|
103
110
|
() =>
|
|
104
111
|
!hasPageSelector.value &&
|
|
105
112
|
!identityName.value &&
|
|
106
|
-
!!
|
|
107
|
-
!!
|
|
113
|
+
!!titleText.value &&
|
|
114
|
+
!!subtitleText.value
|
|
108
115
|
)
|
|
109
116
|
const hasTitleOnly = computed(
|
|
110
117
|
() =>
|
|
111
118
|
!hasPageSelector.value &&
|
|
112
119
|
!identityName.value &&
|
|
113
120
|
!hasTitleGroup.value &&
|
|
114
|
-
!!
|
|
121
|
+
!!titleText.value,
|
|
115
122
|
)
|
|
116
123
|
const normalizedPageSelector = computed<PageSelectorItem[]>(() =>
|
|
117
124
|
props.pageSelector?.map(normalizeItemInput) ?? [],
|
|
@@ -137,135 +144,132 @@ const normalizedSettingsTabs = computed<SettingsTab[]>(() =>
|
|
|
137
144
|
>
|
|
138
145
|
<div class="mint-topbar__container">
|
|
139
146
|
<!-- Left: brand -->
|
|
140
|
-
<div class="mint-
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
<
|
|
148
|
-
v-if="isIntegrated"
|
|
149
|
-
class="mint-topbar__plugin-icon"
|
|
150
|
-
:icon="plugin?.icon"
|
|
151
|
-
:color="plugin?.color"
|
|
152
|
-
size="topbar"
|
|
153
|
-
variant="solid"
|
|
154
|
-
/>
|
|
155
|
-
<slot v-else name="icon">
|
|
147
|
+
<div class="mint-topbar__start">
|
|
148
|
+
<div class="mint-topbar__brand">
|
|
149
|
+
<a
|
|
150
|
+
v-if="config.homePath && (config.homePath.startsWith('http') || config.homePath.startsWith('/'))"
|
|
151
|
+
:href="config.homePath"
|
|
152
|
+
class="mint-topbar-home-link"
|
|
153
|
+
>
|
|
154
|
+
<span v-if="isIntegrated || $slots.icon || $slots.logo || pluginIcon || config.logo" class="mint-topbar__identity">
|
|
156
155
|
<PluginIcon
|
|
157
|
-
v-if="
|
|
156
|
+
v-if="isIntegrated"
|
|
158
157
|
class="mint-topbar__plugin-icon"
|
|
159
|
-
:icon="
|
|
160
|
-
:color="pluginIcon.color"
|
|
158
|
+
:icon="plugin?.icon"
|
|
161
159
|
size="topbar"
|
|
162
160
|
variant="solid"
|
|
163
161
|
/>
|
|
164
|
-
<slot v-else name="
|
|
165
|
-
<
|
|
166
|
-
|
|
167
|
-
|
|
162
|
+
<slot v-else name="icon">
|
|
163
|
+
<PluginIcon
|
|
164
|
+
v-if="pluginIcon"
|
|
165
|
+
class="mint-topbar__plugin-icon"
|
|
166
|
+
:icon="pluginIcon"
|
|
167
|
+
size="topbar"
|
|
168
|
+
variant="solid"
|
|
169
|
+
/>
|
|
170
|
+
<slot v-else name="logo">
|
|
171
|
+
<div v-if="config.logo" class="mint-topbar__logo">
|
|
172
|
+
<div class="mint-topbar__logo-icon">
|
|
173
|
+
<span class="mint-topbar__logo-text">M</span>
|
|
174
|
+
</div>
|
|
168
175
|
</div>
|
|
169
|
-
</
|
|
176
|
+
</slot>
|
|
170
177
|
</slot>
|
|
171
|
-
</
|
|
172
|
-
</
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
<span v-if="isIntegrated || $slots.icon || $slots.logo || pluginIcon || config.logo" class="mint-topbar__identity">
|
|
180
|
-
<PluginIcon
|
|
181
|
-
v-if="isIntegrated"
|
|
182
|
-
class="mint-topbar__plugin-icon"
|
|
183
|
-
:icon="plugin?.icon"
|
|
184
|
-
:color="plugin?.color"
|
|
185
|
-
size="topbar"
|
|
186
|
-
variant="solid"
|
|
187
|
-
/>
|
|
188
|
-
<slot v-else name="icon">
|
|
178
|
+
</span>
|
|
179
|
+
</a>
|
|
180
|
+
<router-link
|
|
181
|
+
v-else-if="config.homePath"
|
|
182
|
+
:to="config.homePath"
|
|
183
|
+
class="mint-topbar-home-link"
|
|
184
|
+
>
|
|
185
|
+
<span v-if="isIntegrated || $slots.icon || $slots.logo || pluginIcon || config.logo" class="mint-topbar__identity">
|
|
189
186
|
<PluginIcon
|
|
190
|
-
v-if="
|
|
187
|
+
v-if="isIntegrated"
|
|
191
188
|
class="mint-topbar__plugin-icon"
|
|
192
|
-
:icon="
|
|
193
|
-
:color="pluginIcon.color"
|
|
189
|
+
:icon="plugin?.icon"
|
|
194
190
|
size="topbar"
|
|
195
191
|
variant="solid"
|
|
196
192
|
/>
|
|
197
|
-
<slot v-else name="
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
193
|
+
<slot v-else name="icon">
|
|
194
|
+
<PluginIcon
|
|
195
|
+
v-if="pluginIcon"
|
|
196
|
+
class="mint-topbar__plugin-icon"
|
|
197
|
+
:icon="pluginIcon"
|
|
198
|
+
size="topbar"
|
|
199
|
+
variant="solid"
|
|
200
|
+
/>
|
|
201
|
+
<slot v-else name="logo">
|
|
202
|
+
<div v-if="config.logo" class="mint-topbar__logo">
|
|
203
|
+
<div class="mint-topbar__logo-icon">
|
|
204
|
+
<span class="mint-topbar__logo-text">M</span>
|
|
205
|
+
</div>
|
|
201
206
|
</div>
|
|
202
|
-
</
|
|
207
|
+
</slot>
|
|
203
208
|
</slot>
|
|
204
|
-
</
|
|
205
|
-
</
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
<span v-if="isIntegrated || $slots.icon || $slots.logo || pluginIcon || config.logo" class="mint-topbar__identity">
|
|
209
|
-
<PluginIcon
|
|
210
|
-
v-if="isIntegrated"
|
|
211
|
-
class="mint-topbar__plugin-icon"
|
|
212
|
-
:icon="plugin?.icon"
|
|
213
|
-
:color="plugin?.color"
|
|
214
|
-
size="topbar"
|
|
215
|
-
variant="solid"
|
|
216
|
-
/>
|
|
217
|
-
<slot v-else name="icon">
|
|
209
|
+
</span>
|
|
210
|
+
</router-link>
|
|
211
|
+
<template v-else>
|
|
212
|
+
<span v-if="isIntegrated || $slots.icon || $slots.logo || pluginIcon || config.logo" class="mint-topbar__identity">
|
|
218
213
|
<PluginIcon
|
|
219
|
-
v-if="
|
|
214
|
+
v-if="isIntegrated"
|
|
220
215
|
class="mint-topbar__plugin-icon"
|
|
221
|
-
:icon="
|
|
222
|
-
:color="pluginIcon.color"
|
|
216
|
+
:icon="plugin?.icon"
|
|
223
217
|
size="topbar"
|
|
224
218
|
variant="solid"
|
|
225
219
|
/>
|
|
226
|
-
<slot v-else name="
|
|
227
|
-
<
|
|
228
|
-
|
|
229
|
-
|
|
220
|
+
<slot v-else name="icon">
|
|
221
|
+
<PluginIcon
|
|
222
|
+
v-if="pluginIcon"
|
|
223
|
+
class="mint-topbar__plugin-icon"
|
|
224
|
+
:icon="pluginIcon"
|
|
225
|
+
size="topbar"
|
|
226
|
+
variant="solid"
|
|
227
|
+
/>
|
|
228
|
+
<slot v-else name="logo">
|
|
229
|
+
<div v-if="config.logo" class="mint-topbar__logo">
|
|
230
|
+
<div class="mint-topbar__logo-icon">
|
|
231
|
+
<span class="mint-topbar__logo-text">M</span>
|
|
232
|
+
</div>
|
|
230
233
|
</div>
|
|
231
|
-
</
|
|
234
|
+
</slot>
|
|
232
235
|
</slot>
|
|
233
|
-
</
|
|
234
|
-
</
|
|
235
|
-
</
|
|
236
|
-
</div>
|
|
236
|
+
</span>
|
|
237
|
+
</template>
|
|
238
|
+
</div>
|
|
237
239
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
240
|
+
<!-- Left: static plugin identity (not a control) -->
|
|
241
|
+
<span v-if="identityName" class="mint-topbar__plugin-identity">
|
|
242
|
+
<span class="mint-topbar__plugin-name">{{ identityName }}</span>
|
|
243
|
+
<span v-if="identitySubtitle" class="mint-topbar__plugin-subtitle">{{ identitySubtitle }}</span>
|
|
244
|
+
</span>
|
|
242
245
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
246
|
+
<!-- Left: page selector (the platform integration script renders the nav_items one) -->
|
|
247
|
+
<span v-if="hasPlatformPageSelector" ref="platformPageSelectorEl" class="mint-topbar__platform-page-selector" />
|
|
248
|
+
<AppTopBarPageSelectorInternal
|
|
249
|
+
v-else-if="hasPropPageSelector"
|
|
250
|
+
:pages="normalizedPageSelector"
|
|
251
|
+
:current-page-id="currentPageSelectorId"
|
|
252
|
+
@select="emit('page-selector-select', $event)"
|
|
253
|
+
>
|
|
254
|
+
<template v-if="$slots['page-selector-icon']" #icon="slotProps">
|
|
255
|
+
<slot name="page-selector-icon" v-bind="slotProps" />
|
|
256
|
+
</template>
|
|
257
|
+
<template v-if="$slots['page-selector-item-icon']" #item-icon="slotProps">
|
|
258
|
+
<slot name="page-selector-item-icon" v-bind="slotProps" />
|
|
259
|
+
</template>
|
|
260
|
+
</AppTopBarPageSelectorInternal>
|
|
258
261
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
262
|
+
<!-- Left: title -->
|
|
263
|
+
<div v-if="hasTitleGroup" class="mint-topbar-title-group">
|
|
264
|
+
<span class="mint-topbar-title">{{ titleText }}</span>
|
|
265
|
+
<span class="mint-topbar-subtitle">{{ subtitleText }}</span>
|
|
266
|
+
</div>
|
|
264
267
|
|
|
265
|
-
|
|
268
|
+
<span v-else-if="hasTitleOnly" class="mint-topbar__title-only">{{ titleText }}</span>
|
|
266
269
|
|
|
267
|
-
|
|
268
|
-
|
|
270
|
+
<!-- Nav slot (inline, after brand/selector) -->
|
|
271
|
+
<slot name="nav" />
|
|
272
|
+
</div>
|
|
269
273
|
|
|
270
274
|
<!-- Center: pill nav -->
|
|
271
275
|
<div v-if="hasPillNav || $slots.center" class="mint-topbar__center">
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type AnalysisArtifactDetail,
|
|
7
7
|
type ArtifactSaveDialogPayload,
|
|
8
8
|
} from '../types/analysisArtifactTypes'
|
|
9
|
+
import type { ExperimentSummary } from '../types'
|
|
9
10
|
|
|
10
11
|
// -- Mock JSON result a plugin would hand the dialog --
|
|
11
12
|
const mockResult: Record<string, unknown> = {
|
|
@@ -47,6 +48,19 @@ const existingJsonArtifact: AnalysisArtifactDetail = {
|
|
|
47
48
|
result: mockResult,
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
// -- Experiment shown in the strip at the top of the dialog --
|
|
52
|
+
const mockExperiment: ExperimentSummary = {
|
|
53
|
+
id: 42,
|
|
54
|
+
experiment_code: 'EXP-042',
|
|
55
|
+
name: 'Doxorubicin dose response, HeLa',
|
|
56
|
+
status: 'ongoing',
|
|
57
|
+
experiment_type: 'dose_response',
|
|
58
|
+
created_at: '2026-07-01T09:00:00Z',
|
|
59
|
+
updated_at: '2026-07-02T14:00:00Z',
|
|
60
|
+
has_design_data: true,
|
|
61
|
+
}
|
|
62
|
+
const changeCount = ref(0)
|
|
63
|
+
|
|
50
64
|
// -- Mock handlers standing in for a generated plugin-client call --
|
|
51
65
|
const lastPayload = ref<ArtifactSaveDialogPayload | null>(null)
|
|
52
66
|
|
|
@@ -103,10 +117,15 @@ const isOpenError = ref(false)
|
|
|
103
117
|
v-if="lastPayload"
|
|
104
118
|
style="margin-top: 1rem; padding: 0.75rem 1rem; background: var(--bg-secondary, #f8fafc); border: 1px solid var(--border-color, #e2e8f0); border-radius: 6px; font-size: 0.75rem; overflow-x: auto;"
|
|
105
119
|
>{{ formatPayload(lastPayload) }}</pre>
|
|
120
|
+
<p v-if="changeCount" style="margin-top: 0.5rem; font-size: 0.75rem;">
|
|
121
|
+
change-experiment emitted {{ changeCount }}×
|
|
122
|
+
</p>
|
|
106
123
|
|
|
107
124
|
<ArtifactSaveDialog
|
|
108
125
|
v-model:open="isOpenFile"
|
|
109
126
|
:experiment-id="42"
|
|
127
|
+
:experiment="mockExperiment"
|
|
128
|
+
@change-experiment="changeCount++"
|
|
110
129
|
:mode="state.mode"
|
|
111
130
|
:result="mockResult"
|
|
112
131
|
:default-artifact-key="state.defaultArtifactKey"
|
|
@@ -190,6 +209,8 @@ const isOpenError = ref(false)
|
|
|
190
209
|
<ArtifactSaveDialog
|
|
191
210
|
v-model:open="isOpenUpdateFile"
|
|
192
211
|
:experiment-id="42"
|
|
212
|
+
:experiment="mockExperiment"
|
|
213
|
+
@change-experiment="changeCount++"
|
|
193
214
|
:existing-artifact="existingFileArtifact"
|
|
194
215
|
accept=".pdf"
|
|
195
216
|
:save-handler="saveHandler"
|
|
@@ -237,6 +258,8 @@ const isOpenError = ref(false)
|
|
|
237
258
|
<ArtifactSaveDialog
|
|
238
259
|
v-model:open="isOpenUpdateJson"
|
|
239
260
|
:experiment-id="42"
|
|
261
|
+
:experiment="mockExperiment"
|
|
262
|
+
@change-experiment="changeCount++"
|
|
240
263
|
:existing-artifact="existingJsonArtifact"
|
|
241
264
|
:result="mockResult"
|
|
242
265
|
:save-handler="saveHandler"
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/** Save/update dialog for analysis artifacts; delegates the write to a plugin-supplied handler. */
|
|
3
|
-
import { computed, ref, shallowRef, watch } from 'vue'
|
|
3
|
+
import { computed, getCurrentInstance, ref, shallowRef, watch } from 'vue'
|
|
4
|
+
import { CloudUpload, File as FileIcon } from 'lucide-vue-next'
|
|
4
5
|
import BaseModal from './BaseModal.vue'
|
|
5
6
|
import BaseInput from './BaseInput.vue'
|
|
6
7
|
import BaseButton from './BaseButton.vue'
|
|
@@ -10,6 +11,7 @@ import FileUploader from './FileUploader.vue'
|
|
|
10
11
|
import SegmentedControl from './SegmentedControl.vue'
|
|
11
12
|
import { useRequest } from '../composables/useRequest'
|
|
12
13
|
import { useExperimentStore } from '../stores/experiment'
|
|
14
|
+
import { formatExperimentDate } from '../composables/experiment-utils'
|
|
13
15
|
import {
|
|
14
16
|
ANALYSIS_FILE_ARTIFACT_SCHEMA,
|
|
15
17
|
type AnalysisArtifactDetail,
|
|
@@ -20,7 +22,7 @@ import {
|
|
|
20
22
|
type ArtifactSaveResult,
|
|
21
23
|
type HandlerArtifactSaveHandler,
|
|
22
24
|
} from '../types/analysisArtifactTypes'
|
|
23
|
-
import type { ModalSize } from '../types'
|
|
25
|
+
import type { ExperimentSummary, ModalSize } from '../types'
|
|
24
26
|
|
|
25
27
|
interface Props {
|
|
26
28
|
open: boolean
|
|
@@ -34,6 +36,11 @@ interface Props {
|
|
|
34
36
|
result?: Record<string, unknown>
|
|
35
37
|
/** Experiment id override; defaults to the current experiment (`useExperimentStore`). */
|
|
36
38
|
experimentId?: number | null
|
|
39
|
+
/**
|
|
40
|
+
* Experiment shown in the strip at the top. Defaults to the store's current
|
|
41
|
+
* experiment when it is the one being saved to; no strip when unknown.
|
|
42
|
+
*/
|
|
43
|
+
experiment?: ExperimentSummary | null
|
|
37
44
|
/** Existing artifact detail; when set the dialog updates instead of creating. */
|
|
38
45
|
existingArtifact?: AnalysisArtifactDetail | null
|
|
39
46
|
/** Known artifact keys, used to warn (json) or block (file) on collisions. */
|
|
@@ -60,6 +67,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
60
67
|
mode: 'file',
|
|
61
68
|
fileSource: 'upload',
|
|
62
69
|
experimentId: null,
|
|
70
|
+
experiment: null,
|
|
63
71
|
existingArtifact: null,
|
|
64
72
|
size: 'md',
|
|
65
73
|
defaultArtifactKey: 'default',
|
|
@@ -71,14 +79,25 @@ const emit = defineEmits<{
|
|
|
71
79
|
saved: [result: ArtifactSaveResult & { payload: ArtifactSaveDialogPayload }]
|
|
72
80
|
error: [message: string]
|
|
73
81
|
cancel: []
|
|
82
|
+
/** The "Change" link in the experiment strip; shown only when a listener is bound. */
|
|
83
|
+
'change-experiment': []
|
|
74
84
|
}>()
|
|
75
85
|
|
|
86
|
+
// Declared emits leave $attrs, so the bound listener is read off the vnode.
|
|
87
|
+
const canChangeExperiment = Boolean(getCurrentInstance()?.vnode.props?.onChangeExperiment)
|
|
88
|
+
|
|
76
89
|
const ARTIFACT_KEY_PATTERN = /^[a-z0-9][a-z0-9._-]*$/
|
|
77
90
|
|
|
78
91
|
const experimentStore = useExperimentStore()
|
|
79
92
|
const request = useRequest('Failed to save analysis artifact.')
|
|
80
93
|
|
|
81
|
-
|
|
94
|
+
/** With an existing artifact the user picks: replace it, or save a separate one. */
|
|
95
|
+
const saveMode = ref<'replace' | 'new'>('replace')
|
|
96
|
+
const isUpdate = computed(() => props.existingArtifact !== null && saveMode.value === 'replace')
|
|
97
|
+
const SAVE_MODES = [
|
|
98
|
+
{ value: 'replace', label: 'Replace an existing result', description: 'Keeps the key; collaborators see the new version.' },
|
|
99
|
+
{ value: 'new', label: 'Save as a new result', description: 'Adds a separate artifact next to the others.' },
|
|
100
|
+
] as const
|
|
82
101
|
const existingResult = computed(
|
|
83
102
|
() => (props.existingArtifact?.result ?? {}) as Record<string, unknown>,
|
|
84
103
|
)
|
|
@@ -104,7 +123,13 @@ const selectedFile = shallowRef<File | null>(null)
|
|
|
104
123
|
let initialNote = ''
|
|
105
124
|
|
|
106
125
|
function resetForm(): void {
|
|
107
|
-
|
|
126
|
+
saveMode.value = 'replace'
|
|
127
|
+
syncFields()
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Fills the fields for the current save mode; a mode switch calls only this. */
|
|
131
|
+
function syncFields(): void {
|
|
132
|
+
const existing = isUpdate.value ? props.existingArtifact : null
|
|
108
133
|
activeKind.value = existing
|
|
109
134
|
? (existingIsFile.value ? 'file' : 'json')
|
|
110
135
|
: (props.mode === 'json' ? 'json' : 'file')
|
|
@@ -120,6 +145,7 @@ resetForm()
|
|
|
120
145
|
watch(() => props.open, (open) => {
|
|
121
146
|
if (open) resetForm()
|
|
122
147
|
})
|
|
148
|
+
watch(saveMode, syncFields)
|
|
123
149
|
// Key off identity, not object reference: re-fetching the same artifact must not
|
|
124
150
|
// wipe in-progress edits.
|
|
125
151
|
watch(
|
|
@@ -127,8 +153,17 @@ watch(
|
|
|
127
153
|
resetForm,
|
|
128
154
|
)
|
|
129
155
|
|
|
130
|
-
const resolvedTitle = computed(() =>
|
|
131
|
-
|
|
156
|
+
const resolvedTitle = computed(() => props.title ?? 'Save to experiment')
|
|
157
|
+
|
|
158
|
+
const stripExperiment = computed(() => {
|
|
159
|
+
if (props.experiment) return props.experiment
|
|
160
|
+
const current = experimentStore.current
|
|
161
|
+
return current && current.id === resolveExperimentId() ? current : null
|
|
162
|
+
})
|
|
163
|
+
const existingUpdated = computed(() => {
|
|
164
|
+
const date = props.existingArtifact?.updated_at ?? props.existingArtifact?.created_at
|
|
165
|
+
return date ? formatExperimentDate(date) : ''
|
|
166
|
+
})
|
|
132
167
|
const showKindToggle = computed(() => !isUpdate.value && props.mode === 'both')
|
|
133
168
|
const showFileUploader = computed(
|
|
134
169
|
() => activeKind.value === 'file' && props.fileSource === 'upload',
|
|
@@ -143,12 +178,16 @@ const effectiveKey = computed(() => {
|
|
|
143
178
|
return keyInput.value.trim() || props.defaultArtifactKey
|
|
144
179
|
})
|
|
145
180
|
const keyValid = computed(() => ARTIFACT_KEY_PATTERN.test(effectiveKey.value))
|
|
146
|
-
const keyExists = computed(
|
|
147
|
-
(
|
|
148
|
-
)
|
|
181
|
+
const keyExists = computed(() => {
|
|
182
|
+
if (isUpdate.value) return false
|
|
183
|
+
const known = [...(props.existingArtifactKeys ?? []), props.existingArtifact?.artifact_key]
|
|
184
|
+
return known.includes(effectiveKey.value)
|
|
185
|
+
})
|
|
149
186
|
// A file artifact is create-only: reusing a key always fails backend-side, and the
|
|
150
|
-
// dialog has no detail to replace it safely. JSON keys upsert natively, so warn only
|
|
151
|
-
|
|
187
|
+
// dialog has no detail to replace it safely. JSON keys upsert natively, so warn only,
|
|
188
|
+
// unless the user explicitly chose "Save as a new result" over the replace option.
|
|
189
|
+
const keyBlocked = computed(() => keyExists.value
|
|
190
|
+
&& (activeKind.value === 'file' || (props.existingArtifact !== null && saveMode.value === 'new')))
|
|
152
191
|
const keyError = computed(() => {
|
|
153
192
|
if (!keyValid.value) return 'Use lowercase letters, digits, dots, dashes, or underscores.'
|
|
154
193
|
if (keyBlocked.value) return 'This key is already used. Choose another, or replace the existing artifact.'
|
|
@@ -324,6 +363,56 @@ function handleCancel(): void {
|
|
|
324
363
|
@close="emit('cancel')"
|
|
325
364
|
>
|
|
326
365
|
<div class="mint-artifact-save-dialog">
|
|
366
|
+
<div v-if="stripExperiment" class="mint-artifact-save-dialog__experiment">
|
|
367
|
+
<span
|
|
368
|
+
v-if="stripExperiment.experiment_code"
|
|
369
|
+
class="mint-exp-code mint-exp-code--sm"
|
|
370
|
+
>{{ stripExperiment.experiment_code }}</span>
|
|
371
|
+
<span class="mint-artifact-save-dialog__experiment-name">{{ stripExperiment.name }}</span>
|
|
372
|
+
<button
|
|
373
|
+
v-if="canChangeExperiment"
|
|
374
|
+
type="button"
|
|
375
|
+
class="mint-artifact-save-dialog__change"
|
|
376
|
+
@click="emit('change-experiment')"
|
|
377
|
+
>
|
|
378
|
+
Change
|
|
379
|
+
</button>
|
|
380
|
+
</div>
|
|
381
|
+
|
|
382
|
+
<div
|
|
383
|
+
v-if="existingArtifact"
|
|
384
|
+
class="mint-artifact-save-dialog__modes"
|
|
385
|
+
role="radiogroup"
|
|
386
|
+
aria-label="Save mode"
|
|
387
|
+
>
|
|
388
|
+
<button
|
|
389
|
+
v-for="option in SAVE_MODES"
|
|
390
|
+
:key="option.value"
|
|
391
|
+
type="button"
|
|
392
|
+
role="radio"
|
|
393
|
+
:aria-checked="saveMode === option.value"
|
|
394
|
+
class="mint-artifact-save-dialog__mode"
|
|
395
|
+
@click="saveMode = option.value"
|
|
396
|
+
>
|
|
397
|
+
<span class="mint-artifact-save-dialog__mode-dot" aria-hidden="true" />
|
|
398
|
+
<span class="mint-artifact-save-dialog__mode-text">
|
|
399
|
+
<b>{{ option.label }}</b>
|
|
400
|
+
<span>{{ option.description }}</span>
|
|
401
|
+
</span>
|
|
402
|
+
</button>
|
|
403
|
+
</div>
|
|
404
|
+
|
|
405
|
+
<div v-if="isUpdate && existingArtifact" class="mint-artifact-save-dialog__list">
|
|
406
|
+
<div class="mint-artifact-save-dialog__row mint-artifact-save-dialog__row--selected">
|
|
407
|
+
<FileIcon class="mint-artifact-save-dialog__row-icon" :size="15" aria-hidden="true" />
|
|
408
|
+
<span class="mint-artifact-save-dialog__row-name">
|
|
409
|
+
{{ existingArtifact.display_name }}
|
|
410
|
+
<span>{{ existingArtifact.artifact_key }}</span>
|
|
411
|
+
</span>
|
|
412
|
+
<span v-if="existingUpdated" class="mint-artifact-save-dialog__row-meta">{{ existingUpdated }}</span>
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
|
|
327
416
|
<SegmentedControl
|
|
328
417
|
v-if="showKindToggle"
|
|
329
418
|
v-model="activeKind"
|
|
@@ -416,11 +505,12 @@ function handleCancel(): void {
|
|
|
416
505
|
</BaseButton>
|
|
417
506
|
<BaseButton
|
|
418
507
|
class="mint-artifact-save-dialog__submit"
|
|
508
|
+
:icon="CloudUpload"
|
|
419
509
|
:loading="request.isLoading.value"
|
|
420
510
|
:disabled="!canSubmit"
|
|
421
511
|
@click="handleSubmit"
|
|
422
512
|
>
|
|
423
|
-
{{ isUpdate ? '
|
|
513
|
+
{{ isUpdate ? 'Replace result' : 'Save result' }}
|
|
424
514
|
</BaseButton>
|
|
425
515
|
</template>
|
|
426
516
|
</BaseModal>
|
|
@@ -80,7 +80,8 @@ function initState() {
|
|
|
80
80
|
return {
|
|
81
81
|
loading: false,
|
|
82
82
|
error: '',
|
|
83
|
-
|
|
83
|
+
// Histoire serializes state; Date objects come back unparseable, ISO strings survive.
|
|
84
|
+
entries: mockEntries.map((entry) => ({ ...entry, timestamp: entry.timestamp.toISOString() })),
|
|
84
85
|
showFilters: false,
|
|
85
86
|
emptyText: 'No activity yet',
|
|
86
87
|
order: 'newest' as const,
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
/** Filterable, sortable timeline of audit entries for tracking experiment or resource history. */
|
|
3
3
|
import { computed, ref } from 'vue'
|
|
4
4
|
import type { AuditEntry } from '../types'
|
|
5
|
+
import { formatRelativeTime } from '../utils/time'
|
|
5
6
|
import BaseSelect from './BaseSelect.vue'
|
|
6
7
|
import DataStateInternal from './internal/DataStateInternal.vue'
|
|
7
8
|
|
|
@@ -79,26 +80,6 @@ function getInitials(name: string): string {
|
|
|
79
80
|
return (words[0]?.[0] ?? '').toUpperCase()
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
const rtf = new Intl.RelativeTimeFormat('en', { numeric: 'auto' })
|
|
83
|
-
|
|
84
|
-
function formatTimestamp(timestamp: Date | string): string {
|
|
85
|
-
const date = timestamp instanceof Date ? timestamp : new Date(timestamp)
|
|
86
|
-
const now = Date.now()
|
|
87
|
-
const diffMs = date.getTime() - now
|
|
88
|
-
const diffSec = Math.round(diffMs / 1000)
|
|
89
|
-
const diffMin = Math.round(diffSec / 60)
|
|
90
|
-
const diffHr = Math.round(diffMin / 60)
|
|
91
|
-
const diffDay = Math.round(diffHr / 24)
|
|
92
|
-
const diffWeek = Math.round(diffDay / 7)
|
|
93
|
-
|
|
94
|
-
if (Math.abs(diffSec) < 60) return rtf.format(diffSec, 'second')
|
|
95
|
-
if (Math.abs(diffMin) < 60) return rtf.format(diffMin, 'minute')
|
|
96
|
-
if (Math.abs(diffHr) < 24) return rtf.format(diffHr, 'hour')
|
|
97
|
-
if (Math.abs(diffDay) < 7) return rtf.format(diffDay, 'day')
|
|
98
|
-
if (Math.abs(diffWeek) < 5) return rtf.format(diffWeek, 'week')
|
|
99
|
-
return date.toLocaleDateString()
|
|
100
|
-
}
|
|
101
|
-
|
|
102
83
|
function handleEntryClick(entry: AuditEntry) {
|
|
103
84
|
emit('entry-click', entry)
|
|
104
85
|
}
|
|
@@ -146,7 +127,7 @@ function handleEntryClick(entry: AuditEntry) {
|
|
|
146
127
|
<span class="mint-audit-trail__sentence">
|
|
147
128
|
<span v-if="entry.user" class="mint-audit-trail__user">{{ entry.user }}</span> <span class="mint-audit-trail__action">{{ entry.action }}</span>
|
|
148
129
|
</span>
|
|
149
|
-
<span class="mint-audit-trail__timestamp">{{
|
|
130
|
+
<span class="mint-audit-trail__timestamp">{{ formatRelativeTime(entry.timestamp, { weeks: true }) }}</span>
|
|
150
131
|
</div>
|
|
151
132
|
<div v-if="entry.detail" class="mint-audit-trail__detail">{{ entry.detail }}</div>
|
|
152
133
|
<div v-if="entry.metadata && Object.keys(entry.metadata).length" class="mint-audit-trail__metadata">
|