@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,8 @@
|
|
|
1
|
-
import { a as BaseButton_default, t as useDropdownState } from "./useDropdownState-
|
|
2
|
-
import { r as readErrorMessage } from "./experiment-
|
|
3
|
-
import { E as useMenuKeyboard } from "./records-
|
|
4
|
-
import { Fragment, Teleport, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, onMounted, openBlock, ref, renderList, renderSlot, toDisplayString, unref
|
|
1
|
+
import { a as BaseButton_default, t as useDropdownState } from "./useDropdownState-uFIz6Yv8.js";
|
|
2
|
+
import { r as readErrorMessage } from "./experiment-DyO54FQB.js";
|
|
3
|
+
import { E as useMenuKeyboard, W as useFloatingPanel } from "./records-D4aQXyZu.js";
|
|
4
|
+
import { Fragment, Teleport, createBlock, createCommentVNode, createElementBlock, createElementVNode, createTextVNode, createVNode, defineComponent, nextTick, normalizeClass, normalizeStyle, onMounted, openBlock, ref, renderList, renderSlot, toDisplayString, unref } from "vue";
|
|
5
5
|
import { Check, Ellipsis } from "lucide-vue-next";
|
|
6
|
-
import { computePosition, flip, offset, shift } from "@floating-ui/dom";
|
|
7
6
|
//#region src/components/ActionMenu.vue?vue&type=script&setup=true&lang.ts
|
|
8
7
|
var _hoisted_1 = {
|
|
9
8
|
key: 0,
|
|
@@ -44,16 +43,12 @@ var ActionMenu_default = /* @__PURE__ */ defineComponent({
|
|
|
44
43
|
const props = __props;
|
|
45
44
|
const emit = __emit;
|
|
46
45
|
const panelRef = ref(null);
|
|
47
|
-
const panelStyle = ref({ visibility: "hidden" });
|
|
48
46
|
const { isOpen, rootRef, triggerRef, open, close } = useDropdownState({
|
|
49
|
-
closeOnScroll: true,
|
|
50
47
|
insideRefs: [panelRef],
|
|
51
|
-
onOpen: () =>
|
|
52
|
-
panelStyle.value = { visibility: "hidden" };
|
|
53
|
-
emit("open");
|
|
54
|
-
},
|
|
48
|
+
onOpen: () => emit("open"),
|
|
55
49
|
onClose: () => emit("close")
|
|
56
50
|
});
|
|
51
|
+
const { style: panelStyle } = useFloatingPanel(triggerRef, panelRef, isOpen, { placement: "bottom-end" });
|
|
57
52
|
onMounted(() => {
|
|
58
53
|
triggerRef.value = rootRef.value?.querySelector("button") ?? null;
|
|
59
54
|
});
|
|
@@ -61,25 +56,6 @@ var ActionMenu_default = /* @__PURE__ */ defineComponent({
|
|
|
61
56
|
if (!panelRef.value) return [];
|
|
62
57
|
return Array.from(panelRef.value.querySelectorAll(".mint-action-menu__item:not(:disabled)"));
|
|
63
58
|
}
|
|
64
|
-
async function positionPanel() {
|
|
65
|
-
const trigger = triggerRef.value;
|
|
66
|
-
const panel = panelRef.value;
|
|
67
|
-
if (!trigger || !panel) return;
|
|
68
|
-
const position = await computePosition(trigger, panel, {
|
|
69
|
-
placement: "bottom-end",
|
|
70
|
-
strategy: "fixed",
|
|
71
|
-
middleware: [
|
|
72
|
-
offset(4),
|
|
73
|
-
flip(),
|
|
74
|
-
shift({ padding: 8 })
|
|
75
|
-
]
|
|
76
|
-
});
|
|
77
|
-
panelStyle.value = {
|
|
78
|
-
left: `${position.x}px`,
|
|
79
|
-
top: `${position.y}px`,
|
|
80
|
-
visibility: "visible"
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
59
|
/** `focusFirst` is false when the keyboard composable owns the initial focus. */
|
|
84
60
|
function openMenu(focusFirst = true) {
|
|
85
61
|
if (props.disabled || isOpen.value) return;
|
|
@@ -102,11 +78,6 @@ var ActionMenu_default = /* @__PURE__ */ defineComponent({
|
|
|
102
78
|
emit("select", item);
|
|
103
79
|
close();
|
|
104
80
|
}
|
|
105
|
-
watch(isOpen, async (opened) => {
|
|
106
|
-
if (!opened) return;
|
|
107
|
-
await nextTick();
|
|
108
|
-
await positionPanel();
|
|
109
|
-
});
|
|
110
81
|
return (_ctx, _cache) => {
|
|
111
82
|
return openBlock(), createElementBlock(Fragment, null, [createElementVNode("span", {
|
|
112
83
|
ref_key: "rootRef",
|
|
@@ -137,7 +108,7 @@ var ActionMenu_default = /* @__PURE__ */ defineComponent({
|
|
|
137
108
|
ref_key: "panelRef",
|
|
138
109
|
ref: panelRef,
|
|
139
110
|
class: "mint-popover mint-action-menu__panel",
|
|
140
|
-
style: normalizeStyle(panelStyle
|
|
111
|
+
style: normalizeStyle(unref(panelStyle)),
|
|
141
112
|
role: "menu",
|
|
142
113
|
onKeydown: _cache[0] || (_cache[0] = (...args) => unref(handlePanelKeydown) && unref(handlePanelKeydown)(...args))
|
|
143
114
|
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.items, (item, index) => {
|
|
@@ -241,4 +212,4 @@ function useRequest(defaultErrorMessage = "Request failed.") {
|
|
|
241
212
|
//#endregion
|
|
242
213
|
export { ActionMenu_default as n, useRequest as t };
|
|
243
214
|
|
|
244
|
-
//# sourceMappingURL=useRequest-
|
|
215
|
+
//# sourceMappingURL=useRequest-BuDmW8Jc.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useRequest-BuDmW8Jc.js","names":[],"sources":["../src/components/ActionMenu.vue","../src/components/ActionMenu.vue","../src/composables/useRequest.ts"],"sourcesContent":["<script setup lang=\"ts\">\n/** Compact overflow actions with an icon-only BaseButton trigger and a viewport-safe teleported menu. */\nimport { Check, Ellipsis } from 'lucide-vue-next'\nimport { nextTick, onMounted, ref } from 'vue'\nimport { useDropdownState } from '../composables/useDropdownState'\nimport { useFloatingPanel } from '../composables/useFloatingPanel'\nimport { useMenuKeyboard } from '../composables/useMenuKeyboard'\nimport type { ActionMenuItem } from '../types/components'\nimport BaseButton from './BaseButton.vue'\n\ninterface Props {\n label: string\n items: ActionMenuItem[]\n disabled?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n disabled: false,\n})\n\nconst emit = defineEmits<{\n select: [item: ActionMenuItem]\n open: []\n close: []\n}>()\n\ndefineSlots<{\n /** Replaces the ellipsis button; `toggle` and `keydown` go on the slotted button. */\n trigger?: (props: { open: boolean; toggle: () => void; keydown: (event: KeyboardEvent) => void }) => unknown\n}>()\n\nconst panelRef = ref<HTMLElement | null>(null)\n\n// Disclosure state (outside-click, Escape, focus return) and placement come from\n// the shared composables; the open menu follows its trigger on scroll.\nconst { isOpen, rootRef, triggerRef, open, close } = useDropdownState({\n insideRefs: [panelRef],\n onOpen: () => emit('open'),\n onClose: () => emit('close'),\n})\nconst { style: panelStyle } = useFloatingPanel(triggerRef, panelRef, isOpen, { placement: 'bottom-end' })\n\n// BaseButton owns the actual <button>, so the trigger element is resolved from\n// the wrapper once mounted rather than bound with a template ref.\nonMounted(() => {\n triggerRef.value = rootRef.value?.querySelector('button') ?? null\n})\n\nfunction menuItems(): HTMLElement[] {\n if (!panelRef.value) return []\n return Array.from(\n panelRef.value.querySelectorAll<HTMLElement>('.mint-action-menu__item:not(:disabled)'),\n )\n}\n\n/** `focusFirst` is false when the keyboard composable owns the initial focus. */\nfunction openMenu(focusFirst = true) {\n if (props.disabled || isOpen.value) return\n open()\n if (focusFirst) {\n void nextTick(() => menuItems()[0]?.focus())\n }\n}\n\nfunction toggleMenu() {\n if (props.disabled) return\n if (isOpen.value) close()\n else openMenu()\n}\n\nconst { handleTriggerKeydown, handlePanelKeydown } = useMenuKeyboard({\n items: menuItems,\n isOpen: () => isOpen.value,\n open: () => openMenu(false),\n close,\n})\n\nfunction handleSelect(item: ActionMenuItem) {\n if (item.disabled) return\n emit('select', item)\n close()\n}\n</script>\n\n<template>\n <span ref=\"rootRef\" class=\"mint-action-menu\">\n <!-- A custom trigger must render one <button> and wire toggle + keydown to it. -->\n <slot name=\"trigger\" :open=\"isOpen\" :toggle=\"toggleMenu\" :keydown=\"handleTriggerKeydown\">\n <BaseButton\n variant=\"secondary\"\n size=\"sm\"\n :icon=\"Ellipsis\"\n :label=\"label\"\n :disabled=\"disabled\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"isOpen\"\n @click=\"toggleMenu\"\n @keydown=\"handleTriggerKeydown\"\n />\n </slot>\n </span>\n\n <Teleport to=\"body\">\n <div\n v-if=\"isOpen\"\n ref=\"panelRef\"\n class=\"mint-popover mint-action-menu__panel\"\n :style=\"panelStyle\"\n role=\"menu\"\n @keydown=\"handlePanelKeydown\"\n >\n <template v-for=\"(item, index) in items\" :key=\"item.id\">\n <div\n v-if=\"item.danger && index > 0 && !items[index - 1].danger\"\n class=\"mint-menu-separator\"\n role=\"separator\"\n />\n <a\n v-if=\"item.href !== undefined && !item.disabled\"\n :href=\"item.href\"\n :target=\"item.target\"\n :rel=\"item.target === '_blank' ? 'noopener noreferrer' : undefined\"\n :class=\"['mint-menu-item mint-action-menu__item', { 'mint-menu-item--danger mint-action-menu__item--danger': item.danger }]\"\n role=\"menuitem\"\n @click=\"handleSelect(item)\"\n >\n {{ item.label }}\n </a>\n <button\n v-else\n type=\"button\"\n :class=\"['mint-menu-item mint-action-menu__item', { 'mint-menu-item--danger mint-action-menu__item--danger': item.danger }]\"\n :role=\"item.checked === undefined ? 'menuitem' : 'menuitemradio'\"\n :aria-checked=\"item.checked\"\n :disabled=\"item.disabled\"\n @click=\"handleSelect(item)\"\n >\n <Check\n v-if=\"item.checked !== undefined\"\n class=\"mint-action-menu__check\"\n :class=\"{ 'mint-action-menu__check--off': !item.checked }\"\n :size=\"14\"\n aria-hidden=\"true\"\n />\n {{ item.label }}\n </button>\n </template>\n </div>\n </Teleport>\n</template>\n","<script setup lang=\"ts\">\n/** Compact overflow actions with an icon-only BaseButton trigger and a viewport-safe teleported menu. */\nimport { Check, Ellipsis } from 'lucide-vue-next'\nimport { nextTick, onMounted, ref } from 'vue'\nimport { useDropdownState } from '../composables/useDropdownState'\nimport { useFloatingPanel } from '../composables/useFloatingPanel'\nimport { useMenuKeyboard } from '../composables/useMenuKeyboard'\nimport type { ActionMenuItem } from '../types/components'\nimport BaseButton from './BaseButton.vue'\n\ninterface Props {\n label: string\n items: ActionMenuItem[]\n disabled?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n disabled: false,\n})\n\nconst emit = defineEmits<{\n select: [item: ActionMenuItem]\n open: []\n close: []\n}>()\n\ndefineSlots<{\n /** Replaces the ellipsis button; `toggle` and `keydown` go on the slotted button. */\n trigger?: (props: { open: boolean; toggle: () => void; keydown: (event: KeyboardEvent) => void }) => unknown\n}>()\n\nconst panelRef = ref<HTMLElement | null>(null)\n\n// Disclosure state (outside-click, Escape, focus return) and placement come from\n// the shared composables; the open menu follows its trigger on scroll.\nconst { isOpen, rootRef, triggerRef, open, close } = useDropdownState({\n insideRefs: [panelRef],\n onOpen: () => emit('open'),\n onClose: () => emit('close'),\n})\nconst { style: panelStyle } = useFloatingPanel(triggerRef, panelRef, isOpen, { placement: 'bottom-end' })\n\n// BaseButton owns the actual <button>, so the trigger element is resolved from\n// the wrapper once mounted rather than bound with a template ref.\nonMounted(() => {\n triggerRef.value = rootRef.value?.querySelector('button') ?? null\n})\n\nfunction menuItems(): HTMLElement[] {\n if (!panelRef.value) return []\n return Array.from(\n panelRef.value.querySelectorAll<HTMLElement>('.mint-action-menu__item:not(:disabled)'),\n )\n}\n\n/** `focusFirst` is false when the keyboard composable owns the initial focus. */\nfunction openMenu(focusFirst = true) {\n if (props.disabled || isOpen.value) return\n open()\n if (focusFirst) {\n void nextTick(() => menuItems()[0]?.focus())\n }\n}\n\nfunction toggleMenu() {\n if (props.disabled) return\n if (isOpen.value) close()\n else openMenu()\n}\n\nconst { handleTriggerKeydown, handlePanelKeydown } = useMenuKeyboard({\n items: menuItems,\n isOpen: () => isOpen.value,\n open: () => openMenu(false),\n close,\n})\n\nfunction handleSelect(item: ActionMenuItem) {\n if (item.disabled) return\n emit('select', item)\n close()\n}\n</script>\n\n<template>\n <span ref=\"rootRef\" class=\"mint-action-menu\">\n <!-- A custom trigger must render one <button> and wire toggle + keydown to it. -->\n <slot name=\"trigger\" :open=\"isOpen\" :toggle=\"toggleMenu\" :keydown=\"handleTriggerKeydown\">\n <BaseButton\n variant=\"secondary\"\n size=\"sm\"\n :icon=\"Ellipsis\"\n :label=\"label\"\n :disabled=\"disabled\"\n aria-haspopup=\"menu\"\n :aria-expanded=\"isOpen\"\n @click=\"toggleMenu\"\n @keydown=\"handleTriggerKeydown\"\n />\n </slot>\n </span>\n\n <Teleport to=\"body\">\n <div\n v-if=\"isOpen\"\n ref=\"panelRef\"\n class=\"mint-popover mint-action-menu__panel\"\n :style=\"panelStyle\"\n role=\"menu\"\n @keydown=\"handlePanelKeydown\"\n >\n <template v-for=\"(item, index) in items\" :key=\"item.id\">\n <div\n v-if=\"item.danger && index > 0 && !items[index - 1].danger\"\n class=\"mint-menu-separator\"\n role=\"separator\"\n />\n <a\n v-if=\"item.href !== undefined && !item.disabled\"\n :href=\"item.href\"\n :target=\"item.target\"\n :rel=\"item.target === '_blank' ? 'noopener noreferrer' : undefined\"\n :class=\"['mint-menu-item mint-action-menu__item', { 'mint-menu-item--danger mint-action-menu__item--danger': item.danger }]\"\n role=\"menuitem\"\n @click=\"handleSelect(item)\"\n >\n {{ item.label }}\n </a>\n <button\n v-else\n type=\"button\"\n :class=\"['mint-menu-item mint-action-menu__item', { 'mint-menu-item--danger mint-action-menu__item--danger': item.danger }]\"\n :role=\"item.checked === undefined ? 'menuitem' : 'menuitemradio'\"\n :aria-checked=\"item.checked\"\n :disabled=\"item.disabled\"\n @click=\"handleSelect(item)\"\n >\n <Check\n v-if=\"item.checked !== undefined\"\n class=\"mint-action-menu__check\"\n :class=\"{ 'mint-action-menu__check--off': !item.checked }\"\n :size=\"14\"\n aria-hidden=\"true\"\n />\n {{ item.label }}\n </button>\n </template>\n </div>\n </Teleport>\n</template>\n","import { ref, type Ref } from 'vue'\nimport { readErrorMessage as readSharedErrorMessage } from '../utils/errors'\n\nexport type RequestKind = 'load' | 'save' | 'run'\n\n/**\n * The unit of work `run()` drives.\n *\n * It receives an `AbortSignal` that fires when the run is superseded by a\n * newer `run()` or by `cancel()`. Zero-argument operations stay valid and\n * simply ignore it.\n */\nexport type RequestOperation<T> = (signal: AbortSignal) => Promise<T>\n\nexport interface RequestRunOptions {\n /** Which timestamp a successful run moves: `lastLoadedAt`, `lastSavedAt` or `lastRunAt`. */\n kind?: RequestKind\n errorMessage?: string\n /**\n * Abort the previously started run when this one starts. Default `true`;\n * pass `false` to let overlapping runs finish their own work (the stale\n * guard on shared state still applies either way).\n */\n abortPrevious?: boolean\n /** Caller-owned signal; aborting it aborts this run's signal too. */\n signal?: AbortSignal\n}\n\nexport interface UseRequestReturn {\n /** Whether a request is in progress. */\n isLoading: Ref<boolean>\n /** Message from the last failed request, or null. */\n error: Ref<string | null>\n /** Timestamp of the last successful load operation, or null. */\n lastLoadedAt: Ref<Date | null>\n /** Timestamp of the last successful save operation, or null. */\n lastSavedAt: Ref<Date | null>\n /** Timestamp of the last successful run operation, or null. */\n lastRunAt: Ref<Date | null>\n /** Clear the current error without changing loading or timestamps. */\n clearError: () => void\n /** Convert thrown values into a developer-facing error message. */\n readErrorMessage: (value: unknown, fallback?: string) => string\n /** Store and return a normalized error message. */\n setError: (value: unknown, fallback?: string) => string\n /**\n * Run a request with shared loading/error handling and optional sync marking.\n *\n * ## Stale-response semantics\n *\n * Every call takes a monotonically increasing token. Starting a run\n * supersedes any run started before it, and a superseded run writes **no**\n * shared state when it finally settles: it does not clear `isLoading`, does not\n * set `error`, and does not move `lastLoadedAt` / `lastSavedAt` / `lastRunAt`.\n * Only the newest run owns those.\n *\n * The caller's own promise is untouched by the guard — a superseded run still\n * resolves with its value or rejects with its own error, exactly as the\n * operation did. The guard never invents a rejection, so a caller awaiting\n * `run()` never sees a \"superseded\" error it did not cause.\n *\n * The operation additionally receives an `AbortSignal` that fires when its\n * run is superseded or `cancel()` is called, so an operation that forwards\n * the signal (`fetch(url, { signal })`) stops work instead of racing.\n */\n run: <T>(operation: RequestOperation<T>, options?: RequestRunOptions) => Promise<T>\n /**\n * Abort the in-flight run and drop `isLoading`. The aborted run's completion\n * is treated as stale and writes no state.\n */\n cancel: () => void\n}\n\n/** Loading, error and timestamp state for one request at a time; `run()` drives it. */\nexport function useRequest(\n defaultErrorMessage = 'Request failed.',\n): UseRequestReturn {\n const isLoading = ref(false)\n const error = ref<string | null>(null)\n const lastLoadedAt = ref<Date | null>(null)\n const lastSavedAt = ref<Date | null>(null)\n const lastRunAt = ref<Date | null>(null)\n\n function clearError(): void {\n error.value = null\n }\n\n function readErrorMessage(value: unknown, fallback = defaultErrorMessage): string {\n return readSharedErrorMessage(value, fallback)\n }\n\n function setError(value: unknown, fallback?: string): string {\n const message = readErrorMessage(value, fallback)\n error.value = message\n return message\n }\n\n // Monotonic token guarding every write to the shared loading/error/timestamp\n // state. Overlapping runs settle in any order, and a superseded one must not\n // clear a newer run's loading flag or overwrite its error.\n let runToken = 0\n let activeController: AbortController | null = null\n\n function cancel(): void {\n runToken += 1\n activeController?.abort()\n activeController = null\n isLoading.value = false\n }\n\n async function run<T>(\n operation: RequestOperation<T>,\n options: RequestRunOptions = {},\n ): Promise<T> {\n if (options.abortPrevious !== false) {\n activeController?.abort()\n }\n\n const token = (runToken += 1)\n const controller = new AbortController()\n activeController = controller\n\n const callerSignal = options.signal\n if (callerSignal) {\n if (callerSignal.aborted) controller.abort(callerSignal.reason)\n else callerSignal.addEventListener(\n 'abort',\n () => controller.abort(callerSignal.reason),\n { once: true },\n )\n }\n\n const isCurrent = (): boolean => token === runToken\n\n isLoading.value = true\n clearError()\n try {\n const response = await operation(controller.signal)\n if (isCurrent() && options.kind) {\n const settledAt = new Date()\n if (options.kind === 'load') lastLoadedAt.value = settledAt\n else if (options.kind === 'save') lastSavedAt.value = settledAt\n else lastRunAt.value = settledAt\n }\n return response\n } catch (err) {\n if (isCurrent()) {\n setError(err, options.errorMessage)\n }\n throw err\n } finally {\n if (isCurrent()) {\n isLoading.value = false\n }\n if (activeController === controller) {\n activeController = null\n }\n }\n }\n\n return {\n isLoading,\n error,\n lastLoadedAt,\n lastSavedAt,\n lastRunAt,\n clearError,\n readErrorMessage,\n setError,\n run,\n cancel,\n }\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@morscherlab/mint-sdk",
|
|
3
|
-
"version": "1.4.0
|
|
3
|
+
"version": "1.4.0",
|
|
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",
|
|
@@ -84,7 +84,8 @@
|
|
|
84
84
|
"@floating-ui/dom": "^1.8.0",
|
|
85
85
|
"axios": "1.20.0",
|
|
86
86
|
"lucide-vue-next": "1.0.0",
|
|
87
|
-
"plotly.js-dist-min": "^4.1.1"
|
|
87
|
+
"plotly.js-dist-min": "^4.1.1",
|
|
88
|
+
"read-excel-file": "9.3.14"
|
|
88
89
|
},
|
|
89
90
|
"devDependencies": {
|
|
90
91
|
"@histoire/plugin-vue": "1.0.0-beta.1",
|
|
@@ -65,19 +65,19 @@ export function installMockInterceptor() {
|
|
|
65
65
|
* Props:
|
|
66
66
|
* preselect (boolean, default true) - pre-select the first mock experiment
|
|
67
67
|
* plugin (PluginInfo, optional) - the integrated plugin, shown as AppTopBar's static identity
|
|
68
|
+
* save (boolean, default true) - show the Save half of the experiment chip
|
|
68
69
|
*/
|
|
69
70
|
export const ExperimentProvider = defineComponent({
|
|
70
71
|
props: {
|
|
71
72
|
preselect: { type: Boolean, default: true },
|
|
72
73
|
plugin: { type: Object as PropType<PluginInfo>, default: undefined },
|
|
74
|
+
save: { type: Boolean, default: true },
|
|
73
75
|
},
|
|
74
76
|
setup(props, { slots }) {
|
|
75
77
|
;(window as any).__MINT_PLATFORM__ = { isIntegrated: true, theme: 'system', plugin: props.plugin }
|
|
76
78
|
installMockInterceptor()
|
|
77
79
|
const experimentStore = useExperimentStore()
|
|
78
|
-
useAppExperiment({
|
|
79
|
-
onSave: async () => 'Saved!',
|
|
80
|
-
})
|
|
80
|
+
useAppExperiment(props.save ? { onSave: async () => 'Saved!' } : {})
|
|
81
81
|
if (props.preselect && mockExperiments[0]) {
|
|
82
82
|
experimentStore.select(mockExperiments[0])
|
|
83
83
|
}
|
|
@@ -132,7 +132,7 @@ describe('ActionMenu', () => {
|
|
|
132
132
|
expect(document.body.querySelector('.mint-menu-separator')).toBeNull()
|
|
133
133
|
})
|
|
134
134
|
|
|
135
|
-
it('should
|
|
135
|
+
it('should keep an open menu open when its scroll context moves', async () => {
|
|
136
136
|
const wrapper = mount(ActionMenu, {
|
|
137
137
|
attachTo: document.body,
|
|
138
138
|
props: { label: 'More user actions', items },
|
|
@@ -142,7 +142,7 @@ describe('ActionMenu', () => {
|
|
|
142
142
|
window.dispatchEvent(new Event('scroll'))
|
|
143
143
|
await wrapper.vm.$nextTick()
|
|
144
144
|
|
|
145
|
-
expect(document.body.querySelector('.mint-action-menu__panel')).toBeNull()
|
|
145
|
+
expect(document.body.querySelector('.mint-action-menu__panel')).not.toBeNull()
|
|
146
146
|
})
|
|
147
147
|
|
|
148
148
|
it('should open from the trigger keyboard shortcuts and focus the first item', async () => {
|
|
@@ -12,6 +12,7 @@ import { defineControlModel, defineControls } from '../../composables/useControl
|
|
|
12
12
|
import ThemeToggle from '../../components/ThemeToggle.vue'
|
|
13
13
|
import SettingsModal from '../../components/SettingsModal.vue'
|
|
14
14
|
import type { TopBarSettingsConfig } from '../../types/components'
|
|
15
|
+
import type { TopBarJob } from '../../types/jobs'
|
|
15
16
|
|
|
16
17
|
// Mock the usePlatform composable
|
|
17
18
|
vi.mock('../../composables/usePlatform', () => ({
|
|
@@ -908,7 +909,7 @@ describe('AppTopBar', () => {
|
|
|
908
909
|
const imageIcons = wrapper.findAll('.mint-plugin-icon__img')
|
|
909
910
|
expect(imageIcons).toHaveLength(1)
|
|
910
911
|
expect(imageIcons[0].attributes('src')).toBe(pngIcon)
|
|
911
|
-
expect(wrapper.
|
|
912
|
+
expect(wrapper.get('.mint-page-selector__item-hint').text()).toBe('Overview')
|
|
912
913
|
})
|
|
913
914
|
})
|
|
914
915
|
|
|
@@ -1237,6 +1238,26 @@ describe('AppTopBar', () => {
|
|
|
1237
1238
|
expect(wrapper.find('.mint-topbar__plugin-name').exists()).toBe(false)
|
|
1238
1239
|
})
|
|
1239
1240
|
|
|
1241
|
+
it.each([
|
|
1242
|
+
{ case: 'integrated with display_name', integrated: true, plugin: { ...integratedPlugin, display_name: 'LC-MS Peak Review' }, props: { title: 'Ignored' }, seed: false, expected: 'LC-MS Peak Review' },
|
|
1243
|
+
{ case: 'integrated without display_name', integrated: true, plugin: integratedPlugin, props: {}, seed: false, expected: 'LC-MS Analysis' },
|
|
1244
|
+
{ case: 'standalone with a title', integrated: false, plugin: undefined, props: { title: 'Planner Dev' }, seed: true, expected: 'Planner Dev' },
|
|
1245
|
+
{ case: 'standalone seeded', integrated: false, plugin: undefined, props: {}, seed: true, expected: 'MS Planner' },
|
|
1246
|
+
{ case: 'standalone without a name', integrated: false, plugin: undefined, props: {}, seed: false, expected: null },
|
|
1247
|
+
])('should put the plugin name in the page selector crumb when $case', ({ integrated, plugin, props, seed, expected }) => {
|
|
1248
|
+
mockPlatformCtx({ isIntegrated: integrated, plugin })
|
|
1249
|
+
if (seed) providePluginIdentity({ plugin: { name: 'ms-planner', displayName: 'MS Planner' } })
|
|
1250
|
+
|
|
1251
|
+
try {
|
|
1252
|
+
const wrapper = createWrapper({ ...props, pageSelector: pages, currentPageSelectorId: 'peaks' })
|
|
1253
|
+
|
|
1254
|
+
const name = wrapper.find('.mint-page-selector__plugin')
|
|
1255
|
+
expect(name.exists() ? name.text() : null).toBe(expected)
|
|
1256
|
+
} finally {
|
|
1257
|
+
providePluginIdentity({ plugin: { name: '' } })
|
|
1258
|
+
}
|
|
1259
|
+
})
|
|
1260
|
+
|
|
1240
1261
|
it('should hide the plugin name when a page selector is shown', () => {
|
|
1241
1262
|
mockPlatformCtx({ isIntegrated: true, plugin: integratedPlugin })
|
|
1242
1263
|
|
|
@@ -1483,4 +1504,188 @@ describe('AppTopBar', () => {
|
|
|
1483
1504
|
expect(removeListener).toHaveBeenCalledWith('scroll', expect.any(Function), expect.objectContaining({ capture: true }))
|
|
1484
1505
|
})
|
|
1485
1506
|
})
|
|
1507
|
+
describe('run slot', () => {
|
|
1508
|
+
function mountRunSlot(
|
|
1509
|
+
job: TopBarJob | null | undefined,
|
|
1510
|
+
{ experiment, jobsDone, jobsOpen, jobsPanelId }: { experiment: boolean; jobsDone?: number; jobsOpen?: boolean; jobsPanelId?: string },
|
|
1511
|
+
) {
|
|
1512
|
+
vi.mocked(usePlatform).mockReturnValueOnce({
|
|
1513
|
+
integrated: computed(() => experiment),
|
|
1514
|
+
isAdmin: computed(() => true),
|
|
1515
|
+
can: () => true,
|
|
1516
|
+
plugin: computed(() => undefined),
|
|
1517
|
+
user: computed(() => undefined),
|
|
1518
|
+
theme: computed(() => 'system' as const),
|
|
1519
|
+
features: computed(() => undefined),
|
|
1520
|
+
})
|
|
1521
|
+
const appExperiment = {
|
|
1522
|
+
popover: computed(() => ({ experimentName: 'Serine tracing', experimentCode: 'EXP-0142', showSave: true })),
|
|
1523
|
+
selectorModal: computed(() => ({ open: false })),
|
|
1524
|
+
openModal: vi.fn(),
|
|
1525
|
+
closeModal: vi.fn(),
|
|
1526
|
+
handleSave: vi.fn(),
|
|
1527
|
+
handleDetach: vi.fn(),
|
|
1528
|
+
} as unknown as AppExperimentState
|
|
1529
|
+
return mount(AppTopBar, {
|
|
1530
|
+
props: job === undefined
|
|
1531
|
+
? { title: 'LEAF', jobsDone, jobsOpen, jobsPanelId }
|
|
1532
|
+
: { title: 'LEAF', job, jobsDone, jobsOpen, jobsPanelId },
|
|
1533
|
+
global: {
|
|
1534
|
+
plugins: [createPinia()],
|
|
1535
|
+
provide: { [APP_EXPERIMENT_KEY as symbol]: appExperiment },
|
|
1536
|
+
stubs: { 'router-link': { template: '<a><slot /></a>' }, BaseModal: { template: '<div><slot /></div>' }, transition: false },
|
|
1537
|
+
},
|
|
1538
|
+
})
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
const running: TopBarJob = { status: 'running', label: 'Extracting', percent: 40 }
|
|
1542
|
+
|
|
1543
|
+
it.each([
|
|
1544
|
+
['no job prop, experiment', undefined, true, { popover: true, chip: false, idle: false, joined: false }],
|
|
1545
|
+
['idle, experiment', null, true, { popover: true, chip: true, idle: true, joined: false }],
|
|
1546
|
+
['running, experiment', running, true, { popover: true, chip: true, idle: false, joined: true }],
|
|
1547
|
+
['idle, standalone', null, false, { popover: false, chip: true, idle: true, joined: false }],
|
|
1548
|
+
['running, standalone', running, false, { popover: false, chip: true, idle: false, joined: true }],
|
|
1549
|
+
] as const)('should render the run slot for %s', async (_case, job, experiment, expected) => {
|
|
1550
|
+
const wrapper = mountRunSlot(job, { experiment })
|
|
1551
|
+
await waitForAsyncComponents(wrapper)
|
|
1552
|
+
|
|
1553
|
+
const chip = wrapper.find('.mint-topbar__job')
|
|
1554
|
+
expect({
|
|
1555
|
+
popover: wrapper.findComponent(ExperimentPopover).exists(),
|
|
1556
|
+
chip: chip.exists(),
|
|
1557
|
+
idle: chip.exists() && chip.classes('mint-topbar__job--idle'),
|
|
1558
|
+
joined: wrapper.find('.mint-topbar__run-slot--joined').exists(),
|
|
1559
|
+
}).toEqual(expected)
|
|
1560
|
+
})
|
|
1561
|
+
|
|
1562
|
+
it.each([
|
|
1563
|
+
[undefined, true],
|
|
1564
|
+
[null, true],
|
|
1565
|
+
[running, false],
|
|
1566
|
+
])('should pass showSave to the experiment chip when the job is %s', async (job, showSave) => {
|
|
1567
|
+
const wrapper = mountRunSlot(job, { experiment: true })
|
|
1568
|
+
await waitForAsyncComponents(wrapper)
|
|
1569
|
+
|
|
1570
|
+
expect(wrapper.findComponent(ExperimentPopover).props('showSave')).toBe(showSave)
|
|
1571
|
+
})
|
|
1572
|
+
|
|
1573
|
+
it('should keep the slot joined until the job has left, then show the idle button', async () => {
|
|
1574
|
+
const wrapper = mountRunSlot(running, { experiment: true })
|
|
1575
|
+
await waitForAsyncComponents(wrapper)
|
|
1576
|
+
const joined = () => wrapper.find('.mint-topbar__run-slot--joined').exists()
|
|
1577
|
+
|
|
1578
|
+
await wrapper.setProps({ job: null })
|
|
1579
|
+
expect(joined()).toBe(true)
|
|
1580
|
+
await vi.waitFor(() => expect(wrapper.find('.mint-topbar__job--idle').exists()).toBe(true))
|
|
1581
|
+
expect(joined()).toBe(false)
|
|
1582
|
+
|
|
1583
|
+
await wrapper.setProps({ job: running })
|
|
1584
|
+
await vi.waitFor(() => expect(wrapper.find('.mint-topbar__job--primary').exists()).toBe(true))
|
|
1585
|
+
expect(joined()).toBe(true)
|
|
1586
|
+
|
|
1587
|
+
await wrapper.setProps({ job: undefined })
|
|
1588
|
+
await vi.waitFor(() => expect(wrapper.find('.mint-topbar__job').exists()).toBe(false))
|
|
1589
|
+
expect(joined()).toBe(false)
|
|
1590
|
+
})
|
|
1591
|
+
|
|
1592
|
+
it('should join the slot when the job and the slot mount together', async () => {
|
|
1593
|
+
const wrapper = mountRunSlot(undefined, { experiment: false })
|
|
1594
|
+
await waitForAsyncComponents(wrapper)
|
|
1595
|
+
|
|
1596
|
+
await wrapper.setProps({ job: running })
|
|
1597
|
+
|
|
1598
|
+
await vi.waitFor(() => expect(wrapper.find('.mint-topbar__run-slot--joined').exists()).toBe(true))
|
|
1599
|
+
})
|
|
1600
|
+
|
|
1601
|
+
it.each([
|
|
1602
|
+
[120, '100%', 'Extracting 100%'],
|
|
1603
|
+
[-5, '0%', 'Extracting 0%'],
|
|
1604
|
+
])('should clamp percent %s to %s', async (percent, width, name) => {
|
|
1605
|
+
const wrapper = mountRunSlot({ ...running, percent }, { experiment: false })
|
|
1606
|
+
await waitForAsyncComponents(wrapper)
|
|
1607
|
+
|
|
1608
|
+
const chip = wrapper.get('.mint-topbar__job')
|
|
1609
|
+
expect(chip.get('.mint-topbar__job-bar > span').attributes('style')).toContain(`width: ${width}`)
|
|
1610
|
+
expect(chip.attributes('aria-label')).toBe(name)
|
|
1611
|
+
})
|
|
1612
|
+
|
|
1613
|
+
it.each([
|
|
1614
|
+
[running, 'Job running'],
|
|
1615
|
+
[{ status: 'awaiting_input', label: 'Decoding' } as TopBarJob, 'Job needs input'],
|
|
1616
|
+
[{ status: 'failed', label: '1 failed' } as TopBarJob, 'Job failed'],
|
|
1617
|
+
[null, ''],
|
|
1618
|
+
])('should announce the status of %o as %s', async (job, text) => {
|
|
1619
|
+
const wrapper = mountRunSlot(job, { experiment: false })
|
|
1620
|
+
await waitForAsyncComponents(wrapper)
|
|
1621
|
+
|
|
1622
|
+
const live = wrapper.get('.mint-topbar__job-status')
|
|
1623
|
+
expect(live.attributes('aria-live')).toBe('polite')
|
|
1624
|
+
expect(live.text()).toBe(text)
|
|
1625
|
+
})
|
|
1626
|
+
|
|
1627
|
+
it.each([
|
|
1628
|
+
['queued', 'primary'],
|
|
1629
|
+
['running', 'primary'],
|
|
1630
|
+
['awaiting_input', 'warning'],
|
|
1631
|
+
['completed', 'success'],
|
|
1632
|
+
['failed', 'error'],
|
|
1633
|
+
['cancelled', 'neutral'],
|
|
1634
|
+
] as const)('should tone a %s job as %s', async (status, tone) => {
|
|
1635
|
+
const wrapper = mountRunSlot({ status, label: 'Job' }, { experiment: false })
|
|
1636
|
+
await waitForAsyncComponents(wrapper)
|
|
1637
|
+
|
|
1638
|
+
expect(wrapper.get('.mint-topbar__job').classes()).toContain(`mint-topbar__job--${tone}`)
|
|
1639
|
+
})
|
|
1640
|
+
|
|
1641
|
+
it.each([
|
|
1642
|
+
[running, 'Extracting 40%', true],
|
|
1643
|
+
[{ status: 'completed', label: 'Saved to EXP-0142' } as TopBarJob, 'Saved to EXP-0142', false],
|
|
1644
|
+
])('should show only the status for %o and name it %s', async (job, name, bar) => {
|
|
1645
|
+
const wrapper = mountRunSlot(job, { experiment: false })
|
|
1646
|
+
await waitForAsyncComponents(wrapper)
|
|
1647
|
+
|
|
1648
|
+
const chip = wrapper.get('.mint-topbar__job')
|
|
1649
|
+
expect(chip.text()).toBe('')
|
|
1650
|
+
expect(chip.attributes('aria-label')).toBe(name)
|
|
1651
|
+
expect(chip.attributes('title')).toBe(name)
|
|
1652
|
+
expect(chip.find('.mint-topbar__job-bar').exists()).toBe(bar)
|
|
1653
|
+
if (bar) expect(chip.get('.mint-topbar__job-bar > span').attributes('style')).toContain('width: 40%')
|
|
1654
|
+
})
|
|
1655
|
+
|
|
1656
|
+
it.each([
|
|
1657
|
+
[null, undefined, null, undefined],
|
|
1658
|
+
[null, 0, null, undefined],
|
|
1659
|
+
[null, 2, '2', 'Jobs, 2 completed'],
|
|
1660
|
+
[running, 2, null, 'Extracting 40%'],
|
|
1661
|
+
])('should show the completed count only on the idle button when the job is %o and jobsDone %s', async (job, jobsDone, count, name) => {
|
|
1662
|
+
const wrapper = mountRunSlot(job, { experiment: false, jobsDone })
|
|
1663
|
+
await waitForAsyncComponents(wrapper)
|
|
1664
|
+
|
|
1665
|
+
const chip = wrapper.get('.mint-topbar__job')
|
|
1666
|
+
expect([chip.find('.mint-topbar__job-count').exists() ? chip.get('.mint-topbar__job-count').text() : null, chip.attributes('aria-label')])
|
|
1667
|
+
.toEqual([count, name])
|
|
1668
|
+
})
|
|
1669
|
+
|
|
1670
|
+
it.each([
|
|
1671
|
+
[null, undefined, undefined, [undefined, undefined]],
|
|
1672
|
+
[null, true, 'jobs-panel', ['true', 'jobs-panel']],
|
|
1673
|
+
[running, false, 'jobs-panel', ['false', 'jobs-panel']],
|
|
1674
|
+
])('should expose the tray state on the job control when it is %o and open is %s', async (job, jobsOpen, jobsPanelId, expected) => {
|
|
1675
|
+
const wrapper = mountRunSlot(job, { experiment: false, jobsOpen, jobsPanelId })
|
|
1676
|
+
await waitForAsyncComponents(wrapper)
|
|
1677
|
+
|
|
1678
|
+
const chip = wrapper.get('.mint-topbar__job')
|
|
1679
|
+
expect([chip.attributes('aria-expanded'), chip.attributes('aria-controls')]).toEqual(expected)
|
|
1680
|
+
})
|
|
1681
|
+
|
|
1682
|
+
it.each([null, running])('should emit job-click when the job control is %s', async (job) => {
|
|
1683
|
+
const wrapper = mountRunSlot(job, { experiment: false })
|
|
1684
|
+
await waitForAsyncComponents(wrapper)
|
|
1685
|
+
|
|
1686
|
+
await wrapper.get('.mint-topbar__job').trigger('click')
|
|
1687
|
+
|
|
1688
|
+
expect(wrapper.emitted('job-click')).toEqual([[]])
|
|
1689
|
+
})
|
|
1690
|
+
})
|
|
1486
1691
|
})
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils'
|
|
3
|
+
import { computed, defineComponent, h, nextTick } from 'vue'
|
|
4
|
+
import { createPinia } from 'pinia'
|
|
5
|
+
import AppTopBar from '../../components/AppTopBar.vue'
|
|
6
|
+
import { provideOnboarding, type OnboardingDefinition } from '../../composables/useOnboarding'
|
|
7
|
+
|
|
8
|
+
vi.mock('../../composables/usePlatform', () => ({
|
|
9
|
+
usePlatform: vi.fn(() => ({
|
|
10
|
+
integrated: computed(() => false),
|
|
11
|
+
isAdmin: computed(() => true),
|
|
12
|
+
can: () => true,
|
|
13
|
+
plugin: computed(() => undefined),
|
|
14
|
+
user: computed(() => undefined),
|
|
15
|
+
theme: computed(() => 'system' as const),
|
|
16
|
+
features: computed(() => undefined),
|
|
17
|
+
})),
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
const definition: OnboardingDefinition = {
|
|
21
|
+
tours: [{
|
|
22
|
+
id: 'intro',
|
|
23
|
+
title: 'Top bar tour',
|
|
24
|
+
steps: [
|
|
25
|
+
{ id: 'settings', anchor: 'topbar-settings', title: 'Settings live here', body: 'Theme and palette.' },
|
|
26
|
+
{ id: 'help', anchor: 'topbar-help', title: 'Help', body: 'Replay tours.', advanceOn: 'click' },
|
|
27
|
+
],
|
|
28
|
+
}],
|
|
29
|
+
checklist: [{ id: 'create', title: 'Create an experiment' }],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
enableAutoUnmount(afterEach)
|
|
33
|
+
|
|
34
|
+
function mountTopBar(onboarding?: OnboardingDefinition) {
|
|
35
|
+
return mount(defineComponent({
|
|
36
|
+
setup() {
|
|
37
|
+
if (onboarding) provideOnboarding(onboarding, { appId: 'test', userId: 1, autoStart: false })
|
|
38
|
+
return () => h(AppTopBar, { spec: { settings: { tabs: [] } } })
|
|
39
|
+
},
|
|
40
|
+
}), { global: { plugins: [createPinia()], stubs: { 'router-link': true } }, attachTo: document.body })
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const card = () => document.querySelector('.mint-onboarding__card')
|
|
44
|
+
|
|
45
|
+
describe('AppTopBar onboarding', () => {
|
|
46
|
+
beforeEach(() => localStorage.clear())
|
|
47
|
+
|
|
48
|
+
it.each([
|
|
49
|
+
[undefined, false],
|
|
50
|
+
[definition, true],
|
|
51
|
+
])('should show the Help button and checklist chip when onboarding is %s', (onboarding, shown) => {
|
|
52
|
+
const wrapper = mountTopBar(onboarding)
|
|
53
|
+
|
|
54
|
+
expect(wrapper.find('[data-onboarding="topbar-help"]').exists()).toBe(shown)
|
|
55
|
+
expect(wrapper.find('.mint-onboarding-help__chip').exists()).toBe(shown)
|
|
56
|
+
if (shown) expect(wrapper.find('.mint-onboarding-help__chip').text()).toContain('0/1')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('should run a tour picked in the Help panel and end it with Escape', async () => {
|
|
60
|
+
const wrapper = mountTopBar(definition)
|
|
61
|
+
await wrapper.get('[data-onboarding="topbar-help"]').trigger('click')
|
|
62
|
+
const tour = [...document.querySelectorAll<HTMLElement>('.mint-menu-item')].find((item) => item.textContent?.trim() === 'Top bar tour')!
|
|
63
|
+
tour.click()
|
|
64
|
+
await flushPromises()
|
|
65
|
+
|
|
66
|
+
expect(document.querySelector('.mint-onboarding-help__panel')).toBeNull()
|
|
67
|
+
expect(card()?.getAttribute('role')).toBe('dialog')
|
|
68
|
+
expect(card()?.textContent).toContain('Settings live here')
|
|
69
|
+
expect(card()?.textContent).toContain('Next')
|
|
70
|
+
|
|
71
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape' }))
|
|
72
|
+
await nextTick()
|
|
73
|
+
|
|
74
|
+
expect(card()).toBeNull()
|
|
75
|
+
expect(JSON.parse(localStorage.getItem('mint.onboarding.test.1')!)).toEqual(
|
|
76
|
+
expect.arrayContaining(['step:intro/settings', 'step:intro/help']),
|
|
77
|
+
)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('should focus the card, not the Next button, when a step opens', async () => {
|
|
81
|
+
const wrapper = mountTopBar(definition)
|
|
82
|
+
await wrapper.get('[data-onboarding="topbar-help"]').trigger('click')
|
|
83
|
+
;[...document.querySelectorAll<HTMLElement>('.mint-menu-item')].find((item) => item.textContent?.trim() === 'Top bar tour')!.click()
|
|
84
|
+
await flushPromises()
|
|
85
|
+
|
|
86
|
+
expect(document.activeElement).toBe(card())
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it.each([
|
|
90
|
+
[{ key: 'Tab', shiftKey: true }, 'Next'],
|
|
91
|
+
[{ key: 'Tab', shiftKey: false }, ''],
|
|
92
|
+
])('should keep %o from the focused card inside the card', async (init, label) => {
|
|
93
|
+
const wrapper = mountTopBar(definition)
|
|
94
|
+
await wrapper.get('[data-onboarding="topbar-help"]').trigger('click')
|
|
95
|
+
;[...document.querySelectorAll<HTMLElement>('.mint-menu-item')].find((item) => item.textContent?.trim() === 'Top bar tour')!.click()
|
|
96
|
+
await flushPromises()
|
|
97
|
+
|
|
98
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { ...init, bubbles: true, cancelable: true }))
|
|
99
|
+
|
|
100
|
+
expect(card()!.contains(document.activeElement)).toBe(true)
|
|
101
|
+
expect((document.activeElement as HTMLElement).textContent?.trim()).toBe(label)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should leave Escape and Tab to a control outside the tour that has focus', async () => {
|
|
105
|
+
const wrapper = mountTopBar(definition)
|
|
106
|
+
await wrapper.get('[data-onboarding="topbar-help"]').trigger('click')
|
|
107
|
+
;[...document.querySelectorAll<HTMLElement>('.mint-menu-item')].find((item) => item.textContent?.trim() === 'Top bar tour')!.click()
|
|
108
|
+
await flushPromises()
|
|
109
|
+
const input = document.body.appendChild(document.createElement('input'))
|
|
110
|
+
input.focus()
|
|
111
|
+
|
|
112
|
+
for (const key of ['Tab', 'Escape']) document.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true }))
|
|
113
|
+
await nextTick()
|
|
114
|
+
|
|
115
|
+
expect(document.activeElement).toBe(input)
|
|
116
|
+
expect(card()).not.toBeNull()
|
|
117
|
+
input.remove()
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
it('should go to the next step when Enter is pressed on the focused card', async () => {
|
|
121
|
+
const wrapper = mountTopBar(definition)
|
|
122
|
+
await wrapper.get('[data-onboarding="topbar-help"]').trigger('click')
|
|
123
|
+
;[...document.querySelectorAll<HTMLElement>('.mint-menu-item')].find((item) => item.textContent?.trim() === 'Top bar tour')!.click()
|
|
124
|
+
await flushPromises()
|
|
125
|
+
|
|
126
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true }))
|
|
127
|
+
await flushPromises()
|
|
128
|
+
|
|
129
|
+
expect(card()?.textContent).toContain('Replay tours.')
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should show no Next button on an advanceOn click step', async () => {
|
|
133
|
+
const wrapper = mountTopBar(definition)
|
|
134
|
+
await wrapper.get('[data-onboarding="topbar-help"]').trigger('click')
|
|
135
|
+
document.querySelector<HTMLElement>('.mint-menu-item')!.click()
|
|
136
|
+
await flushPromises()
|
|
137
|
+
;(document.querySelector('.mint-onboarding__card .mint-button') as HTMLElement).click()
|
|
138
|
+
await flushPromises()
|
|
139
|
+
|
|
140
|
+
expect(card()?.textContent).toContain('Click the highlighted control.')
|
|
141
|
+
expect(card()?.textContent).not.toContain('Done')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('should keep Tab on the anchor and the card on an advanceOn click step', async () => {
|
|
145
|
+
const wrapper = mountTopBar(definition)
|
|
146
|
+
await wrapper.get('[data-onboarding="topbar-help"]').trigger('click')
|
|
147
|
+
document.querySelector<HTMLElement>('.mint-menu-item')!.click()
|
|
148
|
+
await flushPromises()
|
|
149
|
+
;(document.querySelector('.mint-onboarding__card .mint-button') as HTMLElement).click()
|
|
150
|
+
await flushPromises()
|
|
151
|
+
const anchor = document.querySelector<HTMLElement>('[data-onboarding="topbar-help"]')!
|
|
152
|
+
expect(document.activeElement).toBe(anchor)
|
|
153
|
+
|
|
154
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab' }))
|
|
155
|
+
|
|
156
|
+
expect(card()?.contains(document.activeElement)).toBe(true)
|
|
157
|
+
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Tab', shiftKey: true }))
|
|
158
|
+
expect(document.activeElement).toBe(anchor)
|
|
159
|
+
})
|
|
160
|
+
})
|