@morscherlab/mint-sdk 1.4.0-beta.9 → 1.4.0
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/README.md +14 -3
- package/dist/{AlertBox-B2Bgr3ei.js → AlertBox-DwgNTEqf.js} +3 -3
- package/dist/{AlertBox-B2Bgr3ei.js.map → AlertBox-DwgNTEqf.js.map} +1 -1
- package/dist/{AppSidebar-Dq9W1jE2.js → AppSidebar-CZZaxni0.js} +1103 -144
- package/dist/AppSidebar-CZZaxni0.js.map +1 -0
- package/dist/{ConfirmDialog-nbJj51is.js → ConfirmDialog-CjWoxAD1.js} +2 -2
- package/dist/{ConfirmDialog-nbJj51is.js.map → ConfirmDialog-CjWoxAD1.js.map} +1 -1
- package/dist/{ExperimentPopover-5T1fAq2L.js → ExperimentPopover-DujiuQIO.js} +5 -5
- package/dist/{ExperimentPopover-5T1fAq2L.js.map → ExperimentPopover-DujiuQIO.js.map} +1 -1
- package/dist/{ExperimentSelectorModal-65peG4Up.js → ExperimentSelectorModal-B4j4eUOJ.js} +114 -98
- package/dist/ExperimentSelectorModal-B4j4eUOJ.js.map +1 -0
- package/dist/{SegmentedControl-BtnnjWzs.js → SegmentedControl-DyUPH3I5.js} +2 -2
- package/dist/{SegmentedControl-BtnnjWzs.js.map → SegmentedControl-DyUPH3I5.js.map} +1 -1
- package/dist/{SettingsModal-483nyI6b.js → SettingsModal-D2MOOtuO.js} +26 -25
- package/dist/SettingsModal-D2MOOtuO.js.map +1 -0
- package/dist/__stories__/experiment-helpers.d.ts +10 -0
- package/dist/__tests__/components/AppTopBarOnboarding.test.d.ts +1 -0
- package/dist/__tests__/components/FloatingPanelsInModal.test.d.ts +1 -0
- package/dist/__tests__/composables/useFloatingPanel.test.d.ts +1 -0
- package/dist/__tests__/composables/useOnboarding.test.d.ts +1 -0
- package/dist/__tests__/styles/paletteTokens.test.d.ts +1 -0
- package/dist/__tests__/styles/v2AuditRules.test.d.ts +1 -0
- package/dist/components/AppTopBar.vue.d.ts +20 -5
- package/dist/components/BaseButton.vue.d.ts +4 -2
- package/dist/components/ExperimentSelectorModal.vue.d.ts +15 -1
- package/dist/components/JobsStatusTray.vue.d.ts +15 -4
- package/dist/components/PluginWorkspaceView.props.d.ts +3 -0
- package/dist/components/PluginWorkspaceView.vue.d.ts +3 -0
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +3 -3
- package/dist/components/internal/AppTopBarOnboardingInternal.vue.d.ts +7 -0
- package/dist/components/internal/AppTopBarPageSelectorInternal.vue.d.ts +2 -1
- package/dist/components/internal/AutoGroupModalInternal.vue.d.ts +2 -2
- package/dist/components/internal/OnboardingLayerInternal.vue.d.ts +7 -0
- package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +8 -0
- package/dist/composables/pluginWorkspaceModel.d.ts +2 -2
- package/dist/composables/useFileImport.d.ts +7 -0
- package/dist/composables/useFloatingPanel.d.ts +29 -0
- package/dist/composables/useJobCenter.d.ts +41 -3
- package/dist/composables/useOnboarding.d.ts +119 -0
- package/dist/{controlSchemaModel-B8CTXagP.js → controlSchemaModel-Bsg0t5Ab.js} +2 -2
- package/dist/{controlSchemaModel-B8CTXagP.js.map → controlSchemaModel-Bsg0t5Ab.js.map} +1 -1
- package/dist/{data-CgILxDLW.js → data-Boeh4uPI.js} +13 -12
- package/dist/{data-CgILxDLW.js.map → data-Boeh4uPI.js.map} +1 -1
- package/dist/data.d.ts +1 -1
- package/dist/data.js +9 -9
- package/dist/{experiment-43OJtH7R.js → experiment-DyO54FQB.js} +25 -61
- package/dist/experiment-DyO54FQB.js.map +1 -0
- package/dist/{experimentStatus-Cqp6qNrR.js → experimentStatus-DCn2QKG7.js} +2 -2
- package/dist/{experimentStatus-Cqp6qNrR.js.map → experimentStatus-DCn2QKG7.js.map} +1 -1
- package/dist/index.js +20 -20
- package/dist/{install-BtLWTk6H.js → install-BEHp7Mo-.js} +15 -15
- package/dist/{install-BtLWTk6H.js.map → install-BEHp7Mo-.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-MHzp-_J8.js → lab-D61eaX6f.js} +68 -61
- package/dist/{lab-MHzp-_J8.js.map → lab-D61eaX6f.js.map} +1 -1
- package/dist/lab.js +4 -4
- package/dist/{records-B6gpM_3e.js → records-D4aQXyZu.js} +193 -133
- package/dist/records-D4aQXyZu.js.map +1 -0
- package/dist/{runtime-7vfl_bI3.js → runtime-q-R0CNLh.js} +400 -33
- package/dist/runtime-q-R0CNLh.js.map +1 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +7 -7
- package/dist/{shell-Cs9OJMnm.js → shell-6CGFvUUw.js} +10 -5
- package/dist/{shell-Cs9OJMnm.js.map → shell-6CGFvUUw.js.map} +1 -1
- package/dist/shell.d.ts +1 -0
- package/dist/shell.js +6 -6
- package/dist/stores/index.d.ts +1 -1
- package/dist/stores/index.js +2 -2
- package/dist/stores/settings.d.ts +4 -12
- package/dist/styles.css +1191 -479
- package/dist/types/components.d.ts +2 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/jobs.d.ts +9 -0
- package/dist/types/platform.d.ts +1 -1
- package/dist/{useDropdownState-8yWMu-7_.js → useDropdownState-uFIz6Yv8.js} +16 -4
- package/dist/useDropdownState-uFIz6Yv8.js.map +1 -0
- package/dist/{useExperimentSamples-DHnxKFAF.js → useExperimentSamples-QEFoq4tf.js} +48 -6
- package/dist/useExperimentSamples-QEFoq4tf.js.map +1 -0
- package/dist/{useListReorder-BitdUhxn.js → useListReorder-DsuzwWGS.js} +122 -172
- package/dist/useListReorder-DsuzwWGS.js.map +1 -0
- package/dist/{useRackEditor-DiwCt77M.js → useRackEditor-KpZ4iJ6o.js} +22 -32
- package/dist/useRackEditor-KpZ4iJ6o.js.map +1 -0
- package/dist/useRealtime-B58Lo-Tm.js +508 -0
- package/dist/useRealtime-B58Lo-Tm.js.map +1 -0
- package/dist/{useRequest-D506eHi8.js → useRequest-BuDmW8Jc.js} +8 -37
- package/dist/useRequest-BuDmW8Jc.js.map +1 -0
- package/package.json +3 -2
- package/src/__stories__/experiment-helpers.ts +3 -3
- package/src/__tests__/components/ActionMenu.test.ts +2 -2
- package/src/__tests__/components/AppTopBar.test.ts +206 -1
- package/src/__tests__/components/AppTopBarOnboarding.test.ts +160 -0
- package/src/__tests__/components/AppTopBarPageSelector.test.ts +125 -9
- package/src/__tests__/components/BaseButton.test.ts +19 -0
- package/src/__tests__/components/BaseModal.test.ts +13 -0
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +34 -1
- package/src/__tests__/components/FloatingPanelsInModal.test.ts +94 -0
- package/src/__tests__/components/GeneratedPluginView.test.ts +1 -1
- package/src/__tests__/components/JobsStatusTray.test.ts +373 -71
- package/src/__tests__/components/PluginWorkspaceView.test.ts +63 -1
- package/src/__tests__/components/SampleSelector.test.ts +20 -0
- package/src/__tests__/components/SettingsModal.test.ts +9 -2
- package/src/__tests__/composables/useFileImport.test.ts +36 -0
- package/src/__tests__/composables/useFloatingPanel.test.ts +68 -0
- package/src/__tests__/composables/useJobCenter.test.ts +262 -3
- package/src/__tests__/composables/useOnboarding.test.ts +317 -0
- package/src/__tests__/docs/frontendDocsCatalog.test.ts +4 -3
- package/src/__tests__/fixtures/table-import/cells.xlsx +0 -0
- package/src/__tests__/fixtures/table-import/samples.xlsx +0 -0
- package/src/__tests__/stores/settings.test.ts +21 -21
- package/src/__tests__/styles/cssTokens.test.ts +4 -1
- package/src/__tests__/styles/menusShellRefresh.test.ts +8 -2
- package/src/__tests__/styles/paletteTokens.test.ts +44 -0
- package/src/__tests__/styles/v2AuditRules.test.ts +104 -0
- package/src/components/ActionMenu.vue +6 -37
- package/src/components/AppTopBar.onboarding.story.vue +69 -0
- package/src/components/AppTopBar.story.vue +59 -4
- package/src/components/AppTopBar.vue +132 -10
- package/src/components/BaseButton.vue +16 -2
- package/src/components/BaseModal.vue +2 -1
- package/src/components/ExperimentSelectorModal.story.vue +38 -0
- package/src/components/ExperimentSelectorModal.vue +20 -1
- package/src/components/GeneratedPluginView.vue +4 -9
- package/src/components/GeneratedResultRenderer.vue +3 -4
- package/src/components/JobsStatusTray.story.vue +97 -43
- package/src/components/JobsStatusTray.vue +287 -290
- package/src/components/LayoutResizeHandle.story.vue +65 -4
- package/src/components/PluginWorkspaceView.props.ts +3 -0
- package/src/components/PluginWorkspaceView.vue +17 -0
- package/src/components/SampleSelector.story.vue +21 -6
- package/src/components/SampleSelector.vue +10 -3
- package/src/components/SectionCard.story.vue +33 -0
- package/src/components/SettingsModal.vue +7 -12
- package/src/components/Tooltip.vue +9 -32
- package/src/components/internal/AppTopBarOnboardingInternal.vue +172 -0
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +140 -72
- package/src/components/internal/DatePickerDateInternal.vue +7 -45
- package/src/components/internal/DatePickerDateTimeInternal.vue +7 -1
- package/src/components/internal/DatePickerTimeInternal.vue +8 -1
- package/src/components/internal/NumberUnitInputInternal.vue +4 -18
- package/src/components/internal/OnboardingLayerInternal.vue +173 -0
- package/src/components/internal/SelectMultipleInternal.vue +6 -1
- package/src/components/internal/SelectSearchableInternal.vue +6 -1
- package/src/components/internal/WellEditPopupInternal.vue +11 -22
- package/src/components/internal/WorkspaceShellInternal.vue +12 -0
- package/src/composables/pluginWorkspaceModel.ts +2 -2
- package/src/composables/useFileImport.ts +51 -1
- package/src/composables/useFloatingPanel.ts +107 -0
- package/src/composables/useJobCenter.ts +188 -6
- package/src/composables/useOnboarding.ts +465 -0
- package/src/composables/usePluginWorkspace.ts +1 -1
- package/src/data.ts +1 -0
- package/src/runtime.ts +2 -1
- package/src/shell.ts +17 -0
- package/src/stores/index.ts +0 -2
- package/src/stores/settings.ts +20 -50
- package/src/styles/components/action-menu.css +1 -1
- package/src/styles/components/app-layout.css +6 -3
- package/src/styles/components/app-page-selector.css +120 -45
- package/src/styles/components/app-top-bar.css +165 -3
- package/src/styles/components/button.css +8 -0
- package/src/styles/components/dataframe.css +1 -1
- package/src/styles/components/date-picker.css +4 -2
- package/src/styles/components/datetime-picker.css +4 -4
- package/src/styles/components/experiment-selector-modal.css +13 -0
- package/src/styles/components/jobs-status-tray.css +134 -296
- package/src/styles/components/layout-resize-handle.css +59 -16
- package/src/styles/components/modal.css +3 -3
- package/src/styles/components/number-input.css +1 -1
- package/src/styles/components/onboarding.css +368 -0
- package/src/styles/components/progress-bar.css +3 -3
- package/src/styles/components/sample-selector.css +43 -0
- package/src/styles/components/searchable-select.css +3 -7
- package/src/styles/components/section-card.css +30 -6
- package/src/styles/components/settings-modal.css +7 -2
- package/src/styles/components/time-picker.css +3 -6
- package/src/styles/components/toast.css +1 -1
- package/src/styles/components/tooltip.css +2 -2
- package/src/styles/components/unit-input.css +1 -1
- package/src/styles/components/well-edit-popup.css +2 -2
- package/src/styles/index.css +1 -0
- package/src/styles/variables.css +153 -15
- package/src/types/components.ts +2 -0
- package/src/types/index.ts +1 -0
- package/src/types/jobs.ts +10 -0
- package/src/types/platform.ts +1 -1
- package/dist/AppSidebar-Dq9W1jE2.js.map +0 -1
- package/dist/ExperimentSelectorModal-65peG4Up.js.map +0 -1
- package/dist/SettingsModal-483nyI6b.js.map +0 -1
- package/dist/experiment-43OJtH7R.js.map +0 -1
- package/dist/records-B6gpM_3e.js.map +0 -1
- package/dist/runtime-7vfl_bI3.js.map +0 -1
- package/dist/useDropdownState-8yWMu-7_.js.map +0 -1
- package/dist/useExperimentSamples-DHnxKFAF.js.map +0 -1
- package/dist/useListReorder-BitdUhxn.js.map +0 -1
- package/dist/useRackEditor-DiwCt77M.js.map +0 -1
- package/dist/useRealtime-C3iy9QkU.js +0 -699
- package/dist/useRealtime-C3iy9QkU.js.map +0 -1
- package/dist/useRequest-D506eHi8.js.map +0 -1
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
/** Compact overflow actions with an icon-only BaseButton trigger and a viewport-safe teleported menu. */
|
|
3
|
-
import { computePosition, flip, offset, shift } from '@floating-ui/dom'
|
|
4
3
|
import { Check, Ellipsis } from 'lucide-vue-next'
|
|
5
|
-
import { nextTick, onMounted, ref
|
|
4
|
+
import { nextTick, onMounted, ref } from 'vue'
|
|
6
5
|
import { useDropdownState } from '../composables/useDropdownState'
|
|
6
|
+
import { useFloatingPanel } from '../composables/useFloatingPanel'
|
|
7
7
|
import { useMenuKeyboard } from '../composables/useMenuKeyboard'
|
|
8
8
|
import type { ActionMenuItem } from '../types/components'
|
|
9
9
|
import BaseButton from './BaseButton.vue'
|
|
@@ -30,19 +30,15 @@ defineSlots<{
|
|
|
30
30
|
}>()
|
|
31
31
|
|
|
32
32
|
const panelRef = ref<HTMLElement | null>(null)
|
|
33
|
-
const panelStyle = ref<Record<string, string>>({ visibility: 'hidden' })
|
|
34
33
|
|
|
35
|
-
// Disclosure state (outside-click, Escape,
|
|
36
|
-
//
|
|
34
|
+
// Disclosure state (outside-click, Escape, focus return) and placement come from
|
|
35
|
+
// the shared composables; the open menu follows its trigger on scroll.
|
|
37
36
|
const { isOpen, rootRef, triggerRef, open, close } = useDropdownState({
|
|
38
|
-
closeOnScroll: true,
|
|
39
37
|
insideRefs: [panelRef],
|
|
40
|
-
onOpen: () =>
|
|
41
|
-
panelStyle.value = { visibility: 'hidden' }
|
|
42
|
-
emit('open')
|
|
43
|
-
},
|
|
38
|
+
onOpen: () => emit('open'),
|
|
44
39
|
onClose: () => emit('close'),
|
|
45
40
|
})
|
|
41
|
+
const { style: panelStyle } = useFloatingPanel(triggerRef, panelRef, isOpen, { placement: 'bottom-end' })
|
|
46
42
|
|
|
47
43
|
// BaseButton owns the actual <button>, so the trigger element is resolved from
|
|
48
44
|
// the wrapper once mounted rather than bound with a template ref.
|
|
@@ -57,27 +53,6 @@ function menuItems(): HTMLElement[] {
|
|
|
57
53
|
)
|
|
58
54
|
}
|
|
59
55
|
|
|
60
|
-
async function positionPanel() {
|
|
61
|
-
const trigger = triggerRef.value
|
|
62
|
-
const panel = panelRef.value
|
|
63
|
-
if (!trigger || !panel) return
|
|
64
|
-
|
|
65
|
-
const position = await computePosition(trigger, panel, {
|
|
66
|
-
placement: 'bottom-end',
|
|
67
|
-
strategy: 'fixed',
|
|
68
|
-
middleware: [
|
|
69
|
-
offset(4),
|
|
70
|
-
flip(),
|
|
71
|
-
shift({ padding: 8 }),
|
|
72
|
-
],
|
|
73
|
-
})
|
|
74
|
-
panelStyle.value = {
|
|
75
|
-
left: `${position.x}px`,
|
|
76
|
-
top: `${position.y}px`,
|
|
77
|
-
visibility: 'visible',
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
56
|
/** `focusFirst` is false when the keyboard composable owns the initial focus. */
|
|
82
57
|
function openMenu(focusFirst = true) {
|
|
83
58
|
if (props.disabled || isOpen.value) return
|
|
@@ -105,12 +80,6 @@ function handleSelect(item: ActionMenuItem) {
|
|
|
105
80
|
emit('select', item)
|
|
106
81
|
close()
|
|
107
82
|
}
|
|
108
|
-
|
|
109
|
-
watch(isOpen, async (opened) => {
|
|
110
|
-
if (!opened) return
|
|
111
|
-
await nextTick()
|
|
112
|
-
await positionPanel()
|
|
113
|
-
})
|
|
114
83
|
</script>
|
|
115
84
|
|
|
116
85
|
<template>
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { defineComponent, h, ref } from 'vue'
|
|
3
|
+
import AppTopBar from './AppTopBar.vue'
|
|
4
|
+
import BaseButton from './BaseButton.vue'
|
|
5
|
+
import { defineOnboarding, provideOnboarding, useOnboarding } from '../composables/useOnboarding'
|
|
6
|
+
import type { PillNavItem } from '../types'
|
|
7
|
+
|
|
8
|
+
const pills: PillNavItem[] = [
|
|
9
|
+
{ id: 'home', label: 'Home' },
|
|
10
|
+
{ id: 'experiments', label: 'Experiments' },
|
|
11
|
+
{ id: 'instruments', label: 'Instruments' },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
const onboarding = defineOnboarding({
|
|
15
|
+
tours: [
|
|
16
|
+
{
|
|
17
|
+
id: 'intro',
|
|
18
|
+
title: 'The top bar',
|
|
19
|
+
steps: [
|
|
20
|
+
{ id: 'pages', anchor: 'topbar-pills', title: 'Move between pages', body: 'Home, Experiments and Instruments are one click away on every page.' },
|
|
21
|
+
{ id: 'new', anchor: 'story-new', title: 'Create an experiment', body: 'Click New experiment to go on.', placement: 'bottom-start', advanceOn: 'click' },
|
|
22
|
+
{ id: 'help', anchor: 'topbar-help', title: 'Help', body: 'Open the checklist, play a tour again, or see what is new.', placement: 'bottom-end' },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
checklist: [
|
|
27
|
+
{ id: 'create', title: 'Create an experiment', action: { label: 'Open', run: () => {} } },
|
|
28
|
+
{ id: 'project', title: 'Create a project', action: { label: 'Open', run: () => {} } },
|
|
29
|
+
{ id: 'theme', title: 'Pick a theme' },
|
|
30
|
+
],
|
|
31
|
+
whatsNew: [
|
|
32
|
+
{ id: 'settings', anchor: 'topbar-settings', title: 'Palettes in Settings', body: 'Pick an accent color in Settings → Appearance.', placement: 'bottom-end' },
|
|
33
|
+
],
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// The stories are returning users with no state yet, so the news shows too.
|
|
37
|
+
const OnboardingProvider = defineComponent({
|
|
38
|
+
props: { appId: { type: String, required: true }, news: Boolean },
|
|
39
|
+
setup(props, { slots }) {
|
|
40
|
+
const key = `mint.onboarding.${props.appId}.local`
|
|
41
|
+
if (props.news && localStorage.getItem(key) === null) localStorage.setItem(key, '["task:theme"]')
|
|
42
|
+
provideOnboarding(onboarding, { appId: props.appId, userId: 'local' })
|
|
43
|
+
return () => slots.default?.()
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const PageBody = defineComponent({
|
|
48
|
+
setup() {
|
|
49
|
+
const state = useOnboarding()
|
|
50
|
+
return () => h('div', { style: 'padding: 1.5rem; display: flex; gap: 0.5rem' }, [
|
|
51
|
+
h(BaseButton, { 'data-onboarding': 'story-new', onClick: () => state?.complete('create') }, () => 'New experiment'),
|
|
52
|
+
h(BaseButton, { variant: 'secondary', onClick: () => { localStorage.clear(); location.reload() } }, () => 'Reset onboarding'),
|
|
53
|
+
])
|
|
54
|
+
},
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const pill = ref('home')
|
|
58
|
+
</script>
|
|
59
|
+
|
|
60
|
+
<template>
|
|
61
|
+
<Story title="Layout/AppTopBar/Onboarding" :layout="{ type: 'single', iframe: true }">
|
|
62
|
+
<Variant title="Tour, checklist and news">
|
|
63
|
+
<OnboardingProvider app-id="story-onboarding" news>
|
|
64
|
+
<AppTopBar v-model:pill="pill" :spec="{ pills, settings: { tabs: [] }, standaloneLabel: false }" title="MINT" />
|
|
65
|
+
<PageBody />
|
|
66
|
+
</OnboardingProvider>
|
|
67
|
+
</Variant>
|
|
68
|
+
</Story>
|
|
69
|
+
</template>
|
|
@@ -4,14 +4,26 @@ import Avatar from './Avatar.vue'
|
|
|
4
4
|
import PluginIcon from './PluginIcon.vue'
|
|
5
5
|
import { ExperimentProvider } from '../__stories__/experiment-helpers'
|
|
6
6
|
import { providePluginIdentity } from '../composables/pluginIdentity'
|
|
7
|
-
import type
|
|
7
|
+
import { ref, type App } from 'vue'
|
|
8
8
|
import type {
|
|
9
9
|
TopBarVariant,
|
|
10
10
|
PillNavItem,
|
|
11
11
|
PageSelectorItem,
|
|
12
12
|
PluginInfo,
|
|
13
|
+
TopBarJob,
|
|
13
14
|
} from '../types'
|
|
14
15
|
|
|
16
|
+
const runSlotJobs: { label: string; job: TopBarJob | null }[] = [
|
|
17
|
+
{ label: 'Idle', job: null },
|
|
18
|
+
{ label: 'Running', job: { status: 'running', label: 'Extracting', percent: 40 } },
|
|
19
|
+
{ label: 'Needs input', job: { status: 'awaiting_input', label: 'Decoding', percent: 62 } },
|
|
20
|
+
{ label: 'Saved', job: { status: 'completed', label: 'Saved to EXP-0142' } },
|
|
21
|
+
{ label: 'Failed', job: { status: 'failed', label: '1 failed' } },
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
// An index, not the job object: Histoire merges object state across updates.
|
|
25
|
+
const liveRow = ref(0)
|
|
26
|
+
|
|
15
27
|
const variants: TopBarVariant[] = ['card', 'default']
|
|
16
28
|
|
|
17
29
|
const samplePillNav: PillNavItem[] = [
|
|
@@ -43,10 +55,12 @@ const samplePageSelector: PageSelectorItem[] = [
|
|
|
43
55
|
{ id: 'admin', label: 'Admin' },
|
|
44
56
|
]
|
|
45
57
|
|
|
58
|
+
// The menu shows each hint as a second line; a page without one stays a single line.
|
|
46
59
|
const samplePluginPages: PageSelectorItem[] = [
|
|
47
|
-
{ id: 'peaks', label: 'Peak integration' },
|
|
48
|
-
{ id: 'calibration', label: 'Calibration' },
|
|
49
|
-
{ id: 'report', label: 'Report' },
|
|
60
|
+
{ id: 'peaks', label: 'Peak integration', hint: 'Integrate peaks and review the fits per sample' },
|
|
61
|
+
{ id: 'calibration', label: 'Calibration', hint: 'Fit standard curves and check their residuals' },
|
|
62
|
+
{ id: 'report', label: 'Report', hint: 'Export concentrations for the experiment' },
|
|
63
|
+
{ id: 'settings', label: 'Settings' },
|
|
50
64
|
]
|
|
51
65
|
|
|
52
66
|
const samplePlugin: PluginInfo = {
|
|
@@ -147,6 +161,47 @@ function seedIdentity({ app }: { app: App }) {
|
|
|
147
161
|
</div>
|
|
148
162
|
</Variant>
|
|
149
163
|
|
|
164
|
+
<Variant title="Run slot · Experiment and job">
|
|
165
|
+
<!-- The plugin picks the viewer's own job; the bar joins it to the experiment chip. Like LEAF Extract: no Save. -->
|
|
166
|
+
<div style="padding: 2rem; background: var(--bg-primary); display: grid; gap: 0.75rem;">
|
|
167
|
+
<ExperimentProvider :plugin="samplePlugin" :save="false">
|
|
168
|
+
<AppTopBar
|
|
169
|
+
v-for="row in runSlotJobs"
|
|
170
|
+
:key="row.label"
|
|
171
|
+
:spec="{ themeToggle: true, homePath: '' }"
|
|
172
|
+
:page-selector="samplePluginPages"
|
|
173
|
+
current-page-selector-id="peaks"
|
|
174
|
+
:job="row.job"
|
|
175
|
+
/>
|
|
176
|
+
</ExperimentProvider>
|
|
177
|
+
</div>
|
|
178
|
+
</Variant>
|
|
179
|
+
|
|
180
|
+
<Variant title="Run slot · Animation">
|
|
181
|
+
<div style="padding: 2rem; background: var(--bg-primary); display: grid; gap: 0.75rem;">
|
|
182
|
+
<div style="display: flex; gap: 0.5rem;">
|
|
183
|
+
<button
|
|
184
|
+
v-for="(row, index) in runSlotJobs"
|
|
185
|
+
:key="row.label"
|
|
186
|
+
type="button"
|
|
187
|
+
data-run-slot-state
|
|
188
|
+
style="padding: 0.25rem 0.75rem; border: 1px solid var(--border-color); border-radius: var(--radius-md); background: var(--bg-card); color: var(--text-primary); cursor: pointer;"
|
|
189
|
+
@click="liveRow = index"
|
|
190
|
+
>
|
|
191
|
+
{{ row.label }}
|
|
192
|
+
</button>
|
|
193
|
+
</div>
|
|
194
|
+
<ExperimentProvider :plugin="samplePlugin" :save="false">
|
|
195
|
+
<AppTopBar
|
|
196
|
+
:spec="{ themeToggle: true, homePath: '' }"
|
|
197
|
+
:page-selector="samplePluginPages"
|
|
198
|
+
current-page-selector-id="peaks"
|
|
199
|
+
:job="runSlotJobs[liveRow].job"
|
|
200
|
+
/>
|
|
201
|
+
</ExperimentProvider>
|
|
202
|
+
</div>
|
|
203
|
+
</Variant>
|
|
204
|
+
|
|
150
205
|
<Variant title="Refresh · Admin">
|
|
151
206
|
<div style="padding: 2rem; background: var(--bg-primary); min-height: 12rem;">
|
|
152
207
|
<AppTopBar
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* The `#icon` slot applies in standalone mode only; integrated branding comes from `Metadata.icon`.
|
|
6
6
|
*/
|
|
7
|
-
import { ref, computed, inject, defineAsyncComponent, onMounted, onBeforeUnmount, toValue, useSlots, watch } from 'vue'
|
|
7
|
+
import { ref, computed, inject, defineAsyncComponent, onMounted, onBeforeUnmount, toValue, useSlots, watch, type Component } from 'vue'
|
|
8
8
|
import type {
|
|
9
9
|
PillNavOption,
|
|
10
10
|
PillNavItem,
|
|
@@ -19,12 +19,17 @@ import { applySpecDefaults } from '../composables/componentSpecs'
|
|
|
19
19
|
import ThemeToggle from './ThemeToggle.vue'
|
|
20
20
|
import AppTopBarPageSelectorInternal from './internal/AppTopBarPageSelectorInternal.vue'
|
|
21
21
|
import AppTopBarPillNavInternal from './internal/AppTopBarPillNavInternal.vue'
|
|
22
|
+
import AppTopBarOnboardingInternal from './internal/AppTopBarOnboardingInternal.vue'
|
|
22
23
|
import PluginIcon from './PluginIcon.vue'
|
|
23
24
|
import { usePlatform } from '../composables/usePlatform'
|
|
24
25
|
import { APP_EXPERIMENT_KEY } from '../composables/useAppExperiment'
|
|
26
|
+
import { useOnboarding } from '../composables/useOnboarding'
|
|
25
27
|
import { pluginIdentity } from '../composables/pluginIdentity'
|
|
26
28
|
import { integrationHost } from '../utils/integrationHost'
|
|
27
29
|
import { trackTopBarForToasts } from './AppTopBar.toastOffset'
|
|
30
|
+
import { CircleAlert, CircleCheck, CircleX, ListChecks, LoaderCircle } from 'lucide-vue-next'
|
|
31
|
+
import type { JobStatus, TopBarJob } from '../types/jobs'
|
|
32
|
+
import type { Tone } from '../types/components'
|
|
28
33
|
|
|
29
34
|
interface Props {
|
|
30
35
|
/** App or plugin title shown in the left title group when no page selector is present. */
|
|
@@ -39,10 +44,23 @@ interface Props {
|
|
|
39
44
|
pageSelector?: PageSelectorItemInput[]
|
|
40
45
|
/** Active id of the platform page selector. */
|
|
41
46
|
currentPageSelectorId?: string
|
|
47
|
+
/**
|
|
48
|
+
* The viewer's own job, joined to the experiment chip. Omit it for no jobs control;
|
|
49
|
+
* `null` shows the idle Jobs button. Both emit `job-click`.
|
|
50
|
+
*/
|
|
51
|
+
job?: TopBarJob | null
|
|
52
|
+
/** Completed jobs, shown as a small count on the idle Jobs button. Hidden when 0. */
|
|
53
|
+
jobsDone?: number
|
|
54
|
+
/** Whether the jobs panel is open; `aria-expanded` of the job control. */
|
|
55
|
+
jobsOpen?: boolean
|
|
56
|
+
/** DOM id of the jobs panel; `aria-controls` of the job control. */
|
|
57
|
+
jobsPanelId?: string
|
|
42
58
|
}
|
|
43
59
|
|
|
44
60
|
const props = withDefaults(defineProps<Props>(), {
|
|
45
61
|
spec: () => ({}),
|
|
62
|
+
// No boolean cast: without a jobs panel the control has no aria-expanded.
|
|
63
|
+
jobsOpen: undefined,
|
|
46
64
|
})
|
|
47
65
|
|
|
48
66
|
const config = computed(() => applySpecDefaults({
|
|
@@ -66,6 +84,7 @@ const emit = defineEmits<{
|
|
|
66
84
|
'pill-select': [item: PillNavItem]
|
|
67
85
|
'pill-option-select': [option: PillNavOption, item: PillNavItem]
|
|
68
86
|
'settings-values-change': [data: Record<string, unknown>]
|
|
87
|
+
'job-click': []
|
|
69
88
|
}>()
|
|
70
89
|
|
|
71
90
|
const settingsOpen = ref(false)
|
|
@@ -78,7 +97,46 @@ onBeforeUnmount(() => untrackToastOffset?.())
|
|
|
78
97
|
const { integrated: isIntegrated, plugin } = usePlatform()
|
|
79
98
|
const isStandalone = computed(() => !isIntegrated.value)
|
|
80
99
|
const appExperiment = inject(APP_EXPERIMENT_KEY, null)
|
|
100
|
+
const onboarding = useOnboarding()
|
|
81
101
|
const canBindExperiment = computed(() => plugin.value?.plugin_type !== 'static')
|
|
102
|
+
const showExperiment = computed(() => !!appExperiment && !isStandalone.value && canBindExperiment.value)
|
|
103
|
+
// The border joins only once the job is on screen and stays until it has left,
|
|
104
|
+
// so the name and the job never show at the same time.
|
|
105
|
+
const runSlotJoined = ref(!!props.job)
|
|
106
|
+
const JOB_STATUS_TEXT: Record<JobStatus, string> = {
|
|
107
|
+
queued: 'Job queued',
|
|
108
|
+
running: 'Job running',
|
|
109
|
+
awaiting_input: 'Job needs input',
|
|
110
|
+
completed: 'Job completed',
|
|
111
|
+
failed: 'Job failed',
|
|
112
|
+
cancelled: 'Job cancelled',
|
|
113
|
+
}
|
|
114
|
+
const jobPercent = computed(() =>
|
|
115
|
+
props.job?.percent == null ? null : Math.min(100, Math.max(0, Math.round(props.job.percent))),
|
|
116
|
+
)
|
|
117
|
+
const jobName = computed(() => props.job
|
|
118
|
+
? (jobPercent.value != null ? `${props.job.label} ${jobPercent.value}%` : props.job.label)
|
|
119
|
+
: undefined)
|
|
120
|
+
function syncRunSlotJoined() {
|
|
121
|
+
runSlotJoined.value = !!props.job
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const JOB_TONES: Record<JobStatus, Tone> = {
|
|
125
|
+
queued: 'primary',
|
|
126
|
+
running: 'primary',
|
|
127
|
+
awaiting_input: 'warning',
|
|
128
|
+
completed: 'success',
|
|
129
|
+
failed: 'error',
|
|
130
|
+
cancelled: 'neutral',
|
|
131
|
+
}
|
|
132
|
+
const JOB_ICONS: Record<JobStatus, Component> = {
|
|
133
|
+
queued: LoaderCircle,
|
|
134
|
+
running: LoaderCircle,
|
|
135
|
+
awaiting_input: CircleAlert,
|
|
136
|
+
completed: CircleCheck,
|
|
137
|
+
failed: CircleX,
|
|
138
|
+
cancelled: CircleX,
|
|
139
|
+
}
|
|
82
140
|
|
|
83
141
|
const slots = useSlots()
|
|
84
142
|
// Standalone: the seeded identity (providePluginIdentity) stands in for a missing icon and title.
|
|
@@ -103,6 +161,10 @@ watch(platformPageSelectorEl, (element, _previous, onCleanup) => {
|
|
|
103
161
|
const identityName = computed(() =>
|
|
104
162
|
isIntegrated.value && !hasPageSelector.value ? plugin.value?.display_name || plugin.value?.name : undefined,
|
|
105
163
|
)
|
|
164
|
+
/** The page selector crumb reads "<plugin> / <page>". */
|
|
165
|
+
const pageSelectorPluginName = computed(() =>
|
|
166
|
+
isIntegrated.value ? plugin.value?.display_name || plugin.value?.name : props.title || pluginIdentity.name,
|
|
167
|
+
)
|
|
106
168
|
// Platforms before 1.4 send no subtitle; the description fills line 2.
|
|
107
169
|
const identitySubtitle = computed(() => plugin.value?.subtitle || plugin.value?.description)
|
|
108
170
|
const hasPillNav = computed(() => !!config.value.pills?.length)
|
|
@@ -244,11 +306,13 @@ const normalizedSettingsTabs = computed<SettingsTab[]>(() =>
|
|
|
244
306
|
</span>
|
|
245
307
|
|
|
246
308
|
<!-- 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" />
|
|
309
|
+
<span v-if="hasPlatformPageSelector" ref="platformPageSelectorEl" class="mint-topbar__platform-page-selector" data-onboarding="topbar-page-selector" />
|
|
248
310
|
<AppTopBarPageSelectorInternal
|
|
249
311
|
v-else-if="hasPropPageSelector"
|
|
312
|
+
data-onboarding="topbar-page-selector"
|
|
250
313
|
:pages="normalizedPageSelector"
|
|
251
314
|
:current-page-id="currentPageSelectorId"
|
|
315
|
+
:plugin-name="pageSelectorPluginName"
|
|
252
316
|
@select="emit('page-selector-select', $event)"
|
|
253
317
|
>
|
|
254
318
|
<template v-if="$slots['page-selector-icon']" #icon="slotProps">
|
|
@@ -272,7 +336,7 @@ const normalizedSettingsTabs = computed<SettingsTab[]>(() =>
|
|
|
272
336
|
</div>
|
|
273
337
|
|
|
274
338
|
<!-- Center: pill nav -->
|
|
275
|
-
<div v-if="hasPillNav || $slots.center" class="mint-topbar__center">
|
|
339
|
+
<div v-if="hasPillNav || $slots.center" class="mint-topbar__center" data-onboarding="topbar-pills">
|
|
276
340
|
<slot name="center">
|
|
277
341
|
<AppTopBarPillNavInternal
|
|
278
342
|
v-if="hasPillNav"
|
|
@@ -290,16 +354,73 @@ const normalizedSettingsTabs = computed<SettingsTab[]>(() =>
|
|
|
290
354
|
{{ config.standaloneLabel }}
|
|
291
355
|
</span>
|
|
292
356
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
357
|
+
<!-- Run slot: while a job shows, its progress takes the place of the experiment name and Save:
|
|
358
|
+
[ EXP-0142 ▾ | job ]. The idle Jobs button stands apart. -->
|
|
359
|
+
<div
|
|
360
|
+
v-if="showExperiment || job !== undefined || runSlotJoined"
|
|
361
|
+
:class="['mint-topbar__run-slot', { 'mint-topbar__run-slot--joined': runSlotJoined }]"
|
|
362
|
+
data-onboarding="topbar-run"
|
|
363
|
+
>
|
|
364
|
+
<ExperimentPopover
|
|
365
|
+
v-if="showExperiment && appExperiment"
|
|
366
|
+
v-bind="job ? { ...appExperiment.popover.value, showSave: false } : appExperiment.popover.value"
|
|
367
|
+
@select="appExperiment.openModal()"
|
|
368
|
+
@save="appExperiment.handleSave()"
|
|
369
|
+
@detach="appExperiment.handleDetach()"
|
|
370
|
+
/>
|
|
371
|
+
<!-- Status words only, so a moving percent does not repeat. -->
|
|
372
|
+
<span class="mint-topbar__job-status" aria-live="polite">{{ job ? JOB_STATUS_TEXT[job.status] : '' }}</span>
|
|
373
|
+
<!-- appear: the slot can mount together with its first job; before-enter then joins it. -->
|
|
374
|
+
<Transition
|
|
375
|
+
name="mint-topbar-job"
|
|
376
|
+
mode="out-in"
|
|
377
|
+
appear
|
|
378
|
+
@before-enter="syncRunSlotJoined"
|
|
379
|
+
@after-leave="syncRunSlotJoined"
|
|
380
|
+
>
|
|
381
|
+
<button
|
|
382
|
+
v-if="job"
|
|
383
|
+
key="run"
|
|
384
|
+
type="button"
|
|
385
|
+
:class="['mint-topbar__job', `mint-topbar__job--${JOB_TONES[job.status]}`]"
|
|
386
|
+
:title="jobName"
|
|
387
|
+
:aria-label="jobName"
|
|
388
|
+
:aria-expanded="jobsOpen"
|
|
389
|
+
:aria-controls="jobsPanelId"
|
|
390
|
+
@click="emit('job-click')"
|
|
391
|
+
>
|
|
392
|
+
<component
|
|
393
|
+
:is="JOB_ICONS[job.status]"
|
|
394
|
+
:class="['mint-topbar__job-icon', { 'mint-topbar__job-icon--spin': job.status === 'running' || job.status === 'queued' }]"
|
|
395
|
+
:size="14"
|
|
396
|
+
aria-hidden="true"
|
|
397
|
+
/>
|
|
398
|
+
<span v-if="jobPercent != null" class="mint-topbar__job-bar" aria-hidden="true">
|
|
399
|
+
<span :style="{ width: `${jobPercent}%` }" />
|
|
400
|
+
</span>
|
|
401
|
+
</button>
|
|
402
|
+
<button
|
|
403
|
+
v-else-if="job === null"
|
|
404
|
+
key="idle"
|
|
405
|
+
type="button"
|
|
406
|
+
class="mint-topbar__job mint-topbar__job--idle"
|
|
407
|
+
:aria-label="jobsDone ? `Jobs, ${jobsDone} completed` : undefined"
|
|
408
|
+
:aria-expanded="jobsOpen"
|
|
409
|
+
:aria-controls="jobsPanelId"
|
|
410
|
+
@click="emit('job-click')"
|
|
411
|
+
>
|
|
412
|
+
<ListChecks class="mint-topbar__job-icon" :size="14" aria-hidden="true" />
|
|
413
|
+
<span class="mint-topbar__job-label">Jobs</span>
|
|
414
|
+
<span v-if="jobsDone" class="mint-topbar__job-count" aria-hidden="true">{{ jobsDone }}</span>
|
|
415
|
+
</button>
|
|
416
|
+
</Transition>
|
|
417
|
+
</div>
|
|
300
418
|
|
|
301
419
|
<slot name="actions" />
|
|
302
420
|
|
|
421
|
+
<!-- Onboarding: checklist chip, Help button and tour layer, when the app provides onboarding -->
|
|
422
|
+
<AppTopBarOnboardingInternal v-if="onboarding?.enabled.value" :state="onboarding" />
|
|
423
|
+
|
|
303
424
|
<!-- Theme toggle -->
|
|
304
425
|
<ThemeToggle v-if="config.themeToggle" size="sm" />
|
|
305
426
|
|
|
@@ -309,6 +430,7 @@ const normalizedSettingsTabs = computed<SettingsTab[]>(() =>
|
|
|
309
430
|
type="button"
|
|
310
431
|
class="mint-topbar__settings-btn"
|
|
311
432
|
aria-label="Open settings"
|
|
433
|
+
data-onboarding="topbar-settings"
|
|
312
434
|
@click="settingsOpen = true"
|
|
313
435
|
>
|
|
314
436
|
<svg class="mint-topbar__settings-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* <BaseButton variant="ghost" :icon="Trash2" label="Delete" />
|
|
11
11
|
* ```
|
|
12
12
|
*/
|
|
13
|
-
import { computed, useSlots, type Component } from 'vue'
|
|
13
|
+
import { Comment, Fragment, Text, computed, useSlots, type Component, type VNode } from 'vue'
|
|
14
14
|
import type { ButtonVariant, ButtonSize } from '../types'
|
|
15
15
|
|
|
16
16
|
interface Props {
|
|
@@ -45,6 +45,19 @@ const slots = useSlots()
|
|
|
45
45
|
const iconOnly = computed(() => Boolean(props.icon) && !slots.default)
|
|
46
46
|
const block = computed(() => (iconOnly.value ? 'mint-icon-button' : 'mint-button'))
|
|
47
47
|
|
|
48
|
+
// A text-only label gets its own span so the stylesheet can centre the cap height.
|
|
49
|
+
// Element children are left as flex items, so their gap and alignment do not change.
|
|
50
|
+
function isTextNode(node: VNode): boolean {
|
|
51
|
+
if (node.type === Text || node.type === Comment) return true
|
|
52
|
+
return node.type === Fragment && Array.isArray(node.children)
|
|
53
|
+
&& (node.children as VNode[]).every(isTextNode)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasTextLabel(): boolean {
|
|
57
|
+
const nodes = slots.default?.()
|
|
58
|
+
return !!nodes && nodes.length > 0 && nodes.every(isTextNode)
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
function handleClick(event: MouseEvent) {
|
|
49
62
|
if (!props.disabled && !props.loading) {
|
|
50
63
|
emit('click', event)
|
|
@@ -93,7 +106,8 @@ function handleClick(event: MouseEvent) {
|
|
|
93
106
|
<template v-if="!iconOnly">
|
|
94
107
|
<component :is="icon" v-if="icon" class="mint-button__icon" aria-hidden="true" />
|
|
95
108
|
<slot name="icon-left" />
|
|
96
|
-
<slot
|
|
109
|
+
<span v-if="hasTextLabel()" class="mint-button__label"><slot /></span>
|
|
110
|
+
<slot v-else />
|
|
97
111
|
<slot name="icon-right" />
|
|
98
112
|
</template>
|
|
99
113
|
</button>
|
|
@@ -107,8 +107,9 @@ watch(() => props.open, (isOpen) => {
|
|
|
107
107
|
if (!isOpen && index !== -1) openModals.splice(index, 1)
|
|
108
108
|
}, { immediate: true })
|
|
109
109
|
|
|
110
|
+
// A control inside the modal that handles Escape (an open select) prevents the default first.
|
|
110
111
|
function handleEscapeKeydown(event: KeyboardEvent) {
|
|
111
|
-
if (event.key === 'Escape' && props.closeOnEscape && props.open && openModals[openModals.length - 1] === self) {
|
|
112
|
+
if (event.key === 'Escape' && !event.defaultPrevented && props.closeOnEscape && props.open && openModals[openModals.length - 1] === self) {
|
|
112
113
|
close()
|
|
113
114
|
}
|
|
114
115
|
}
|
|
@@ -268,6 +268,7 @@ onMounted(async () => {
|
|
|
268
268
|
|
|
269
269
|
// -- Story state --
|
|
270
270
|
const isOpen = ref(false)
|
|
271
|
+
const isOpenRun = ref(false)
|
|
271
272
|
const isOpenEmpty = ref(false)
|
|
272
273
|
const isOpenWithSelection = ref(false)
|
|
273
274
|
const isOpenFilters = ref(false)
|
|
@@ -363,6 +364,43 @@ const selectedExperiment = computed(() => experimentStore.current)
|
|
|
363
364
|
</div>
|
|
364
365
|
</Variant>
|
|
365
366
|
|
|
367
|
+
<Variant title="Run Destination (slots)">
|
|
368
|
+
<div style="padding: 2rem;">
|
|
369
|
+
<p style="font-size: 0.8125rem; color: var(--text-muted, #94a3b8); margin: 0 0 1rem;">
|
|
370
|
+
A plugin asks where to save a run: the description slot explains why, and the footer slot adds Skip with close().
|
|
371
|
+
</p>
|
|
372
|
+
<button
|
|
373
|
+
style="padding: 0.5rem 1rem; background: var(--color-primary); color: var(--text-on-accent); border: none; border-radius: var(--radius-md); cursor: pointer; font-size: 0.875rem;"
|
|
374
|
+
@click="isOpenRun = true"
|
|
375
|
+
>
|
|
376
|
+
Run extraction
|
|
377
|
+
</button>
|
|
378
|
+
|
|
379
|
+
<ExperimentSelectorModal
|
|
380
|
+
v-model:open="isOpenRun"
|
|
381
|
+
title="Save the result to an experiment?"
|
|
382
|
+
>
|
|
383
|
+
<template #description>
|
|
384
|
+
Pick an experiment to save this run as a new artifact in it, next to the samples it belongs to.
|
|
385
|
+
</template>
|
|
386
|
+
<template #footer="{ close }">
|
|
387
|
+
<div style="display: flex; align-items: center; gap: 0.75rem;">
|
|
388
|
+
<span style="flex: 1; font-size: 0.75rem; color: var(--text-secondary);">
|
|
389
|
+
Skip: the result stays in the plugin. You can save it to an experiment later.
|
|
390
|
+
</span>
|
|
391
|
+
<button
|
|
392
|
+
type="button"
|
|
393
|
+
style="height: 2rem; padding: 0 0.75rem; border: 1px solid var(--border-color); border-radius: var(--radius-md); background: var(--bg-card); color: var(--text-primary); cursor: pointer;"
|
|
394
|
+
@click="close"
|
|
395
|
+
>
|
|
396
|
+
Skip
|
|
397
|
+
</button>
|
|
398
|
+
</div>
|
|
399
|
+
</template>
|
|
400
|
+
</ExperimentSelectorModal>
|
|
401
|
+
</div>
|
|
402
|
+
</Variant>
|
|
403
|
+
|
|
366
404
|
<Variant title="Empty State">
|
|
367
405
|
<div style="padding: 2rem;">
|
|
368
406
|
<p style="font-size: 0.8125rem; color: var(--text-muted, #94a3b8); margin: 0 0 1rem;">
|
|
@@ -40,6 +40,17 @@ const emit = defineEmits<{
|
|
|
40
40
|
'update:open': [value: boolean]
|
|
41
41
|
}>()
|
|
42
42
|
|
|
43
|
+
defineSlots<{
|
|
44
|
+
/** Text above the experiment list: why the caller asks for an experiment. */
|
|
45
|
+
description?: () => unknown
|
|
46
|
+
/** Extra footer content, before Clear selection. `close()` closes the modal without a selection. */
|
|
47
|
+
footer?: (props: { close: () => void }) => unknown
|
|
48
|
+
}>()
|
|
49
|
+
|
|
50
|
+
function close() {
|
|
51
|
+
emit('update:open', false)
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
const experimentStore = useExperimentStore()
|
|
44
55
|
|
|
45
56
|
const {
|
|
@@ -188,6 +199,10 @@ watch(
|
|
|
188
199
|
:size="size"
|
|
189
200
|
@update:open="emit('update:open', $event)"
|
|
190
201
|
>
|
|
202
|
+
<div v-if="$slots.description" class="mint-experiment-selector__description">
|
|
203
|
+
<slot name="description" />
|
|
204
|
+
</div>
|
|
205
|
+
|
|
191
206
|
<ResourcePickerListInternal
|
|
192
207
|
:key="openCount"
|
|
193
208
|
:search="filters.search ?? ''"
|
|
@@ -257,8 +272,12 @@ watch(
|
|
|
257
272
|
</template>
|
|
258
273
|
</ResourcePickerListInternal>
|
|
259
274
|
|
|
260
|
-
<div v-if="currentExperimentId != null" class="mint-experiment-selector__footer">
|
|
275
|
+
<div v-if="$slots.footer || currentExperimentId != null" class="mint-experiment-selector__footer">
|
|
276
|
+
<div v-if="$slots.footer" class="mint-experiment-selector__footer-slot">
|
|
277
|
+
<slot name="footer" :close="close" />
|
|
278
|
+
</div>
|
|
261
279
|
<button
|
|
280
|
+
v-if="currentExperimentId != null"
|
|
262
281
|
type="button"
|
|
263
282
|
class="mint-experiment-selector__clear-btn"
|
|
264
283
|
@click="handleDeselect"
|
|
@@ -12,11 +12,11 @@ import { computed, onMounted, provide, reactive, ref, watch } from 'vue'
|
|
|
12
12
|
import { generatedJobDefaults, generatedJobFormSchema, normalizeGeneratedJobInput } from '../generated/schema'
|
|
13
13
|
import type { FormSchema } from '../types/form-builder'
|
|
14
14
|
import type {
|
|
15
|
-
GeneratedAnalysisJob,
|
|
16
15
|
GeneratedAnalysisResult,
|
|
17
16
|
GeneratedJobDefinition,
|
|
18
17
|
} from '../types/generated-ui'
|
|
19
18
|
import type { PillNavItem, SidebarToolSection } from '../types'
|
|
19
|
+
import type { JobState } from '../types/jobs'
|
|
20
20
|
import {
|
|
21
21
|
useJobsEndpoint,
|
|
22
22
|
type GeneratedAnalysisTransport,
|
|
@@ -24,7 +24,6 @@ import {
|
|
|
24
24
|
import AlertBox from './AlertBox.vue'
|
|
25
25
|
import EmptyState from './EmptyState.vue'
|
|
26
26
|
import GeneratedResultRenderer from './GeneratedResultRenderer.vue'
|
|
27
|
-
import JobsStatusTray from './JobsStatusTray.vue'
|
|
28
27
|
import LoadingSpinner from './LoadingSpinner.vue'
|
|
29
28
|
import PluginWorkspaceView from './PluginWorkspaceView.vue'
|
|
30
29
|
import { GENERATED_JOBS_API_KEY } from './internal/generatedJobsApi'
|
|
@@ -84,6 +83,7 @@ const workspace = computed(() => definePluginWorkspaceView({
|
|
|
84
83
|
standaloneLabel: props.showStandaloneLabel ? 'Standalone' : false,
|
|
85
84
|
},
|
|
86
85
|
sidebar: { title: 'Parameters', panels: panels.value, forms: forms.value },
|
|
86
|
+
jobs: generated,
|
|
87
87
|
}))
|
|
88
88
|
|
|
89
89
|
// Seeds the standalone top bar icon; the title and subtitle above win over the seeded text.
|
|
@@ -137,7 +137,7 @@ async function submitJob(sectionId: string, values: Record<string, unknown>): Pr
|
|
|
137
137
|
await generated.submit(definition.id, normalizeGeneratedJobInput(definition, values))
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
-
function selectJob(job:
|
|
140
|
+
function selectJob(job: JobState): void {
|
|
141
141
|
selectedJobId.value = job.job_id
|
|
142
142
|
activeDefinitionId.value = String(job.metadata.definition_id ?? activeDefinitionId.value)
|
|
143
143
|
}
|
|
@@ -166,6 +166,7 @@ onMounted(() => {
|
|
|
166
166
|
:form-disabled="generated.isSubmitting.value"
|
|
167
167
|
@update:values="updateValues"
|
|
168
168
|
@form-submit="submitJob"
|
|
169
|
+
@job-select="selectJob"
|
|
169
170
|
>
|
|
170
171
|
<main class="mint-generated-workspace">
|
|
171
172
|
<div v-if="generated.isLoading.value" class="mint-generated-workspace__loading">
|
|
@@ -200,12 +201,6 @@ onMounted(() => {
|
|
|
200
201
|
/>
|
|
201
202
|
</section>
|
|
202
203
|
</template>
|
|
203
|
-
|
|
204
|
-
<JobsStatusTray
|
|
205
|
-
:source="generated"
|
|
206
|
-
:teleport-to="false"
|
|
207
|
-
@select="selectJob"
|
|
208
|
-
/>
|
|
209
204
|
</main>
|
|
210
205
|
</PluginWorkspaceView>
|
|
211
206
|
</template>
|