@morscherlab/mint-sdk 1.4.0-beta.8 → 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/{ActionMenu-CScINahU.js → AlertBox-DwgNTEqf.js} +40 -209
- package/dist/AlertBox-DwgNTEqf.js.map +1 -0
- package/dist/{AppSidebar-BmiiPfz9.js → AppSidebar-CZZaxni0.js} +1249 -239
- package/dist/AppSidebar-CZZaxni0.js.map +1 -0
- package/dist/{ConfirmDialog-CJ7hqYUP.js → ConfirmDialog-CjWoxAD1.js} +2 -2
- package/dist/{ConfirmDialog-CJ7hqYUP.js.map → ConfirmDialog-CjWoxAD1.js.map} +1 -1
- package/dist/ExperimentPopover-DujiuQIO.js +337 -0
- package/dist/ExperimentPopover-DujiuQIO.js.map +1 -0
- package/dist/{ExperimentSelectorModal-D-9z7cNx.js → ExperimentSelectorModal-B4j4eUOJ.js} +244 -211
- package/dist/ExperimentSelectorModal-B4j4eUOJ.js.map +1 -0
- package/dist/{SegmentedControl-DERBN-Xd.js → SegmentedControl-DyUPH3I5.js} +2 -2
- package/dist/{SegmentedControl-DERBN-Xd.js.map → SegmentedControl-DyUPH3I5.js.map} +1 -1
- package/dist/{SettingsModal-CSLE31yg.js → SettingsModal-D2MOOtuO.js} +28 -27
- 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/AppSidebar.vue.d.ts +1 -1
- package/dist/components/AppTopBar.vue.d.ts +20 -5
- package/dist/components/ArtifactSaveDialog.vue.d.ts +9 -1
- package/dist/components/AuditTrail.vue.d.ts +1 -1
- package/dist/components/BaseButton.vue.d.ts +4 -2
- package/dist/components/BaseSelect.vue.d.ts +2 -2
- package/dist/components/ChartContainer.vue.d.ts +2 -0
- package/dist/components/DatePicker.vue.d.ts +1 -1
- package/dist/components/ExperimentPopover.vue.d.ts +18 -0
- package/dist/components/ExperimentSelectorModal.vue.d.ts +15 -5
- package/dist/components/FilePicker.vue.d.ts +1 -1
- package/dist/components/FileUploader.vue.d.ts +2 -2
- package/dist/components/JobsStatusTray.vue.d.ts +15 -4
- package/dist/components/PluginIcon.vue.d.ts +2 -2
- package/dist/components/PluginWorkspaceView.props.d.ts +3 -0
- package/dist/components/PluginWorkspaceView.vue.d.ts +3 -0
- package/dist/components/SampleSelector.vue.d.ts +6 -6
- package/dist/components/SequenceProgressBar.vue.d.ts +3 -0
- package/dist/components/SmartGroupFieldRecipe.vue.d.ts +3 -3
- package/dist/components/StepWizard.vue.d.ts +3 -0
- 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 +3 -3
- package/dist/components/internal/DatePickerTimeInternal.vue.d.ts +1 -1
- package/dist/components/internal/OnboardingLayerInternal.vue.d.ts +7 -0
- package/dist/components/internal/ResourcePickerListInternal.vue.d.ts +2 -7
- package/dist/components/internal/SelectMultipleInternal.vue.d.ts +4 -0
- package/dist/components/internal/WorkspaceShellInternal.vue.d.ts +8 -0
- package/dist/composables/createClient.d.ts +2 -0
- package/dist/composables/index.d.ts +1 -0
- package/dist/composables/pluginIdentity.d.ts +24 -0
- package/dist/composables/pluginWorkspaceModel.d.ts +2 -2
- package/dist/composables/useAppExperiment.d.ts +8 -0
- package/dist/composables/useCalendarGrid.d.ts +1 -1
- package/dist/composables/useExperimentSelector.d.ts +6 -2
- package/dist/composables/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/composables/usePlatform.d.ts +1 -0
- package/dist/{controlSchemaModel-CP5-_nAj.js → controlSchemaModel-Bsg0t5Ab.js} +2 -2
- package/dist/{controlSchemaModel-CP5-_nAj.js.map → controlSchemaModel-Bsg0t5Ab.js.map} +1 -1
- package/dist/{data-C2PwI_dn.js → data-Boeh4uPI.js} +180 -125
- package/dist/data-Boeh4uPI.js.map +1 -0
- 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-BZmU4czV.js → experimentStatus-DCn2QKG7.js} +3 -3
- package/dist/{experimentStatus-BZmU4czV.js.map → experimentStatus-DCn2QKG7.js.map} +1 -1
- package/dist/index.js +20 -20
- package/dist/{install-DAID6c2-.js → install-BEHp7Mo-.js} +15 -14
- package/dist/{install-DAID6c2-.js.map → install-BEHp7Mo-.js.map} +1 -1
- package/dist/install.js +1 -1
- package/dist/{lab-DLwRl_Ql.js → lab-D61eaX6f.js} +136 -148
- package/dist/lab-D61eaX6f.js.map +1 -0
- package/dist/lab.js +4 -3
- package/dist/{records-BzTcxekG.js → records-D4aQXyZu.js} +647 -389
- package/dist/records-D4aQXyZu.js.map +1 -0
- package/dist/{runtime-D6gVat6r.js → runtime-q-R0CNLh.js} +417 -54
- 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-5ju97nOS.js → shell-6CGFvUUw.js} +10 -5
- package/dist/{shell-5ju97nOS.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 +2753 -2101
- package/dist/types/componentWorkflowTypes.d.ts +2 -0
- package/dist/types/components.d.ts +2 -0
- package/dist/types/generated/platformContext.d.ts +1 -0
- package/dist/types/generated-ui.d.ts +4 -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/{BaseModal-DfRIuVoj.js → useDropdownState-uFIz6Yv8.js} +75 -5
- package/dist/useDropdownState-uFIz6Yv8.js.map +1 -0
- package/dist/{useExperimentSamples-CxcnxZ8h.js → useExperimentSamples-QEFoq4tf.js} +48 -6
- package/dist/useExperimentSamples-QEFoq4tf.js.map +1 -0
- package/dist/{useListReorder-CcbEYxX-.js → useListReorder-DsuzwWGS.js} +219 -437
- package/dist/useListReorder-DsuzwWGS.js.map +1 -0
- package/dist/{useRackEditor-BD5NIo5s.js → useRackEditor-KpZ4iJ6o.js} +85 -81
- 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-BuDmW8Jc.js +215 -0
- package/dist/useRequest-BuDmW8Jc.js.map +1 -0
- package/dist/utils/time.d.ts +7 -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 +257 -3
- package/src/__tests__/components/AppTopBarOnboarding.test.ts +160 -0
- package/src/__tests__/components/AppTopBarPageSelector.test.ts +125 -9
- package/src/__tests__/components/AppTopBarPillNav.test.ts +35 -0
- package/src/__tests__/components/ArtifactSaveDialog.test.ts +78 -0
- package/src/__tests__/components/BaseButton.test.ts +19 -0
- package/src/__tests__/components/BaseModal.test.ts +13 -0
- package/src/__tests__/components/BaseSelect.multiple.test.ts +87 -32
- package/src/__tests__/components/BaseSelect.test.ts +2 -1
- package/src/__tests__/components/BatchProgressList.test.ts +16 -0
- package/src/__tests__/components/CalendarGridPanel.test.ts +2 -2
- package/src/__tests__/components/ChartContainer.test.ts +11 -0
- package/src/__tests__/components/ChemicalFormula.test.ts +6 -3
- package/src/__tests__/components/DatePicker.datetime.test.ts +6 -0
- package/src/__tests__/components/DatePicker.test.ts +21 -0
- package/src/__tests__/components/DatePicker.time.test.ts +2 -2
- package/src/__tests__/components/EmptyState.test.ts +1 -1
- package/src/__tests__/components/ExperimentPopover.test.ts +125 -16
- package/src/__tests__/components/ExperimentSelectorModal.test.ts +153 -14
- package/src/__tests__/components/FloatingPanelsInModal.test.ts +94 -0
- package/src/__tests__/components/GeneratedPluginView.test.ts +23 -2
- package/src/__tests__/components/InstrumentStateBadge.test.ts +4 -4
- package/src/__tests__/components/JobsStatusTray.test.ts +373 -71
- package/src/__tests__/components/PluginWorkspaceView.test.ts +63 -1
- package/src/__tests__/components/ResourcePickerListInternal.test.ts +0 -20
- package/src/__tests__/components/SampleSelector.test.ts +33 -8
- package/src/__tests__/components/SequenceProgressBar.test.ts +12 -0
- package/src/__tests__/components/SettingsModal.test.ts +9 -2
- package/src/__tests__/components/StepWizard.test.ts +11 -0
- package/src/__tests__/components/WellPlate.decoration.test.ts +11 -4
- package/src/__tests__/components/deprecationTags.test.ts +1 -1
- package/src/__tests__/composables/useAppExperiment.test.ts +27 -1
- package/src/__tests__/composables/useCalendarGrid.test.ts +4 -2
- package/src/__tests__/composables/useExperimentSelector.test.ts +111 -0
- package/src/__tests__/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/baseControlsRules.test.ts +6 -2
- package/src/__tests__/styles/controlRefreshRules.test.ts +0 -6
- package/src/__tests__/styles/cssTokens.test.ts +6 -3
- package/src/__tests__/styles/menusShellRefresh.test.ts +22 -12
- package/src/__tests__/styles/paletteTokens.test.ts +44 -0
- package/src/__tests__/styles/v2AuditRules.test.ts +104 -0
- package/src/__tests__/utils/plotlyTemplate.test.ts +4 -4
- package/src/__tests__/utils/time.test.ts +18 -1
- package/src/components/ActionMenu.vue +6 -37
- package/src/components/AdductText.story.vue +7 -0
- package/src/components/AdductText.vue +1 -1
- package/src/components/AlertBox.story.vue +2 -2
- package/src/components/AlertBox.vue +18 -16
- package/src/components/AppSidebar.story.vue +0 -28
- package/src/components/AppTopBar.onboarding.story.vue +69 -0
- package/src/components/AppTopBar.story.vue +105 -15
- package/src/components/AppTopBar.vue +248 -122
- package/src/components/ArtifactSaveDialog.story.vue +23 -0
- package/src/components/ArtifactSaveDialog.vue +102 -12
- package/src/components/AuditTrail.story.vue +2 -1
- package/src/components/AuditTrail.vue +2 -21
- package/src/components/Avatar.vue +10 -14
- package/src/components/BaseButton.vue +16 -2
- package/src/components/BaseModal.vue +2 -1
- package/src/components/BaseSelect.multiple.story.vue +13 -2
- package/src/components/BaseSelect.vue +4 -3
- package/src/components/BatchProgressList.vue +35 -25
- package/src/components/ChartContainer.story.vue +2 -1
- package/src/components/ChartContainer.vue +8 -6
- package/src/components/ConfirmDialog.story.vue +1 -1
- package/src/components/EmptyState.vue +1 -1
- package/src/components/ExperimentPopover.story.vue +165 -53
- package/src/components/ExperimentPopover.vue +169 -187
- package/src/components/ExperimentSelectorModal.story.vue +73 -31
- package/src/components/ExperimentSelectorModal.vue +144 -111
- package/src/components/FormField.story.vue +8 -48
- package/src/components/GeneratedPluginView.vue +11 -10
- package/src/components/GeneratedResultRenderer.vue +3 -4
- package/src/components/InstrumentStateBadge.vue +1 -1
- 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/LoadingSpinner.vue +1 -1
- package/src/components/NumberInput.vue +3 -6
- package/src/components/PluginIcon.story.vue +2 -10
- package/src/components/PluginIcon.vue +2 -2
- package/src/components/PluginWorkspaceView.props.ts +3 -0
- package/src/components/PluginWorkspaceView.story.vue +1 -1
- package/src/components/PluginWorkspaceView.vue +17 -0
- package/src/components/RuntimeUpdateNotice.vue +1 -1
- package/src/components/SampleSelector.story.vue +22 -7
- package/src/components/SampleSelector.vue +34 -37
- package/src/components/SampleSelectorGroupNode.vue +1 -1
- package/src/components/SectionCard.story.vue +35 -2
- package/src/components/SequenceProgressBar.story.vue +9 -0
- package/src/components/SequenceProgressBar.vue +6 -1
- package/src/components/SettingsModal.schema.ts +1 -1
- package/src/components/SettingsModal.vue +9 -14
- package/src/components/SmartGroupFieldRecipe.story.vue +1 -1
- package/src/components/SmartGroupManual.story.vue +1 -1
- package/src/components/StepWizard.story.vue +33 -0
- package/src/components/StepWizard.vue +8 -1
- package/src/components/Tooltip.vue +9 -32
- package/src/components/WellPlate.story.vue +3 -1
- package/src/components/WellPlate.vue +7 -0
- package/src/components/internal/AppTopBarOnboardingInternal.vue +172 -0
- package/src/components/internal/AppTopBarPageSelectorInternal.vue +140 -72
- package/src/components/internal/AppTopBarPillNavInternal.vue +33 -2
- package/src/components/internal/DatePickerDateInternal.vue +23 -85
- package/src/components/internal/DatePickerDateTimeInternal.vue +10 -14
- package/src/components/internal/DatePickerTimeInternal.vue +13 -9
- package/src/components/internal/NumberUnitInputInternal.vue +4 -18
- package/src/components/internal/OnboardingLayerInternal.vue +173 -0
- package/src/components/internal/ResourcePickerListInternal.vue +45 -85
- package/src/components/internal/SelectMultipleInternal.vue +187 -65
- package/src/components/internal/SelectSearchableInternal.vue +8 -4
- package/src/components/internal/WellEditPopupInternal.vue +11 -22
- package/src/components/internal/WorkspaceShellInternal.vue +12 -0
- package/src/composables/createClient.ts +2 -0
- package/src/composables/index.ts +1 -0
- package/src/composables/pluginIdentity.ts +31 -0
- package/src/composables/pluginWorkspaceModel.ts +2 -2
- package/src/composables/useAppExperiment.ts +20 -0
- package/src/composables/useCalendarGrid.ts +4 -3
- package/src/composables/useExperimentSelector.ts +77 -31
- package/src/composables/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/adduct-text.css +13 -5
- package/src/styles/components/alert-box.css +39 -58
- package/src/styles/components/app-layout.css +10 -6
- package/src/styles/components/app-page-selector.css +121 -44
- package/src/styles/components/app-pill-nav.css +11 -12
- package/src/styles/components/app-sidebar.css +9 -18
- package/src/styles/components/app-top-bar.css +219 -55
- package/src/styles/components/artifact-save-dialog.css +153 -2
- package/src/styles/components/audit-trail.css +16 -19
- package/src/styles/components/avatar.css +6 -4
- package/src/styles/components/batch-progress-list.css +38 -36
- package/src/styles/components/button.css +36 -59
- package/src/styles/components/calendar.css +12 -12
- package/src/styles/components/chart-container.css +34 -23
- package/src/styles/components/checkbox.css +2 -12
- package/src/styles/components/collapsible-card.css +27 -29
- package/src/styles/components/confirm-dialog.css +13 -14
- package/src/styles/components/dataframe.css +13 -5
- package/src/styles/components/date-picker.css +84 -94
- package/src/styles/components/datetime-picker.css +35 -47
- package/src/styles/components/divider.css +20 -20
- package/src/styles/components/dose-calculator.css +3 -3
- package/src/styles/components/empty-state.css +20 -17
- package/src/styles/components/experiment-code-badge.css +2 -0
- package/src/styles/components/experiment-popover.css +174 -329
- package/src/styles/components/experiment-selector-modal.css +88 -56
- package/src/styles/components/file-picker.css +3 -3
- package/src/styles/components/file-uploader.css +38 -49
- package/src/styles/components/form-builder.css +3 -3
- package/src/styles/components/formula-input.css +1 -1
- package/src/styles/components/icon-button.css +18 -24
- package/src/styles/components/input.css +5 -4
- package/src/styles/components/instrument-monitor.css +13 -15
- package/src/styles/components/jobs-status-tray.css +140 -296
- package/src/styles/components/layout-resize-handle.css +59 -16
- package/src/styles/components/modal.css +5 -7
- package/src/styles/components/molecule-input.css +2 -2
- package/src/styles/components/multi-select.css +65 -117
- package/src/styles/components/number-input.css +26 -19
- package/src/styles/components/onboarding.css +368 -0
- package/src/styles/components/pill.css +3 -2
- package/src/styles/components/plate-editor.css +0 -1
- package/src/styles/components/plugin-icon.css +26 -23
- package/src/styles/components/popover.css +2 -2
- package/src/styles/components/presence-avatars.css +1 -1
- package/src/styles/components/progress-bar.css +3 -3
- package/src/styles/components/protocol-step-editor.css +1 -3
- package/src/styles/components/resource-card.css +2 -2
- package/src/styles/components/resource-picker-list.css +6 -57
- package/src/styles/components/sample-hierarchy-tree.css +4 -4
- package/src/styles/components/sample-selector.css +138 -79
- package/src/styles/components/schedule-calendar.css +8 -8
- package/src/styles/components/searchable-select.css +15 -19
- package/src/styles/components/section-card.css +54 -49
- package/src/styles/components/segmented-control.css +18 -15
- package/src/styles/components/select.css +3 -3
- package/src/styles/components/sequence-input.css +2 -2
- package/src/styles/components/sequence-progress-bar.css +17 -8
- package/src/styles/components/settings-modal.css +7 -2
- package/src/styles/components/smart-group.css +18 -18
- package/src/styles/components/step-wizard.css +52 -16
- package/src/styles/components/tabs.css +2 -2
- package/src/styles/components/textarea.css +3 -3
- package/src/styles/components/time-picker.css +37 -25
- package/src/styles/components/toast.css +13 -11
- package/src/styles/components/toggle.css +1 -11
- package/src/styles/components/tooltip.css +12 -11
- package/src/styles/components/unit-input.css +9 -8
- package/src/styles/components/well-edit-popup.css +2 -2
- package/src/styles/components/well-plate.css +9 -2
- package/src/styles/index.css +1 -0
- package/src/styles/variables.css +247 -78
- package/src/types/componentWorkflowTypes.ts +2 -0
- package/src/types/components.ts +2 -0
- package/src/types/generated/platformContext.ts +1 -0
- package/src/types/generated-ui.ts +4 -0
- package/src/types/index.ts +1 -0
- package/src/types/jobs.ts +10 -0
- package/src/types/platform.ts +1 -1
- package/src/utils/time.ts +21 -0
- package/dist/ActionMenu-CScINahU.js.map +0 -1
- package/dist/AppSidebar-BmiiPfz9.js.map +0 -1
- package/dist/BaseModal-DfRIuVoj.js.map +0 -1
- package/dist/ExperimentPopover-C4sGw-is.js +0 -359
- package/dist/ExperimentPopover-C4sGw-is.js.map +0 -1
- package/dist/ExperimentSelectorModal-D-9z7cNx.js.map +0 -1
- package/dist/SettingsModal-CSLE31yg.js.map +0 -1
- package/dist/data-C2PwI_dn.js.map +0 -1
- package/dist/experiment-43OJtH7R.js.map +0 -1
- package/dist/lab-DLwRl_Ql.js.map +0 -1
- package/dist/records-BzTcxekG.js.map +0 -1
- package/dist/runtime-D6gVat6r.js.map +0 -1
- package/dist/useExperimentSamples-CxcnxZ8h.js.map +0 -1
- package/dist/useListReorder-CcbEYxX-.js.map +0 -1
- package/dist/useRackEditor-BD5NIo5s.js.map +0 -1
- package/dist/useRealtime-PalKue84.js +0 -696
- package/dist/useRealtime-PalKue84.js.map +0 -1
- package/dist/useRequest-D5JDfsgZ.js +0 -76
- package/dist/useRequest-D5JDfsgZ.js.map +0 -1
|
@@ -0,0 +1,215 @@
|
|
|
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
|
+
import { Check, Ellipsis } from "lucide-vue-next";
|
|
6
|
+
//#region src/components/ActionMenu.vue?vue&type=script&setup=true&lang.ts
|
|
7
|
+
var _hoisted_1 = {
|
|
8
|
+
key: 0,
|
|
9
|
+
class: "mint-menu-separator",
|
|
10
|
+
role: "separator"
|
|
11
|
+
};
|
|
12
|
+
var _hoisted_2 = [
|
|
13
|
+
"href",
|
|
14
|
+
"target",
|
|
15
|
+
"rel",
|
|
16
|
+
"onClick"
|
|
17
|
+
];
|
|
18
|
+
var _hoisted_3 = [
|
|
19
|
+
"role",
|
|
20
|
+
"aria-checked",
|
|
21
|
+
"disabled",
|
|
22
|
+
"onClick"
|
|
23
|
+
];
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/components/ActionMenu.vue
|
|
26
|
+
var ActionMenu_default = /* @__PURE__ */ defineComponent({
|
|
27
|
+
__name: "ActionMenu",
|
|
28
|
+
props: {
|
|
29
|
+
label: {},
|
|
30
|
+
items: {},
|
|
31
|
+
disabled: {
|
|
32
|
+
type: Boolean,
|
|
33
|
+
default: false
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
emits: [
|
|
37
|
+
"select",
|
|
38
|
+
"open",
|
|
39
|
+
"close"
|
|
40
|
+
],
|
|
41
|
+
setup(__props, { emit: __emit }) {
|
|
42
|
+
/** Compact overflow actions with an icon-only BaseButton trigger and a viewport-safe teleported menu. */
|
|
43
|
+
const props = __props;
|
|
44
|
+
const emit = __emit;
|
|
45
|
+
const panelRef = ref(null);
|
|
46
|
+
const { isOpen, rootRef, triggerRef, open, close } = useDropdownState({
|
|
47
|
+
insideRefs: [panelRef],
|
|
48
|
+
onOpen: () => emit("open"),
|
|
49
|
+
onClose: () => emit("close")
|
|
50
|
+
});
|
|
51
|
+
const { style: panelStyle } = useFloatingPanel(triggerRef, panelRef, isOpen, { placement: "bottom-end" });
|
|
52
|
+
onMounted(() => {
|
|
53
|
+
triggerRef.value = rootRef.value?.querySelector("button") ?? null;
|
|
54
|
+
});
|
|
55
|
+
function menuItems() {
|
|
56
|
+
if (!panelRef.value) return [];
|
|
57
|
+
return Array.from(panelRef.value.querySelectorAll(".mint-action-menu__item:not(:disabled)"));
|
|
58
|
+
}
|
|
59
|
+
/** `focusFirst` is false when the keyboard composable owns the initial focus. */
|
|
60
|
+
function openMenu(focusFirst = true) {
|
|
61
|
+
if (props.disabled || isOpen.value) return;
|
|
62
|
+
open();
|
|
63
|
+
if (focusFirst) nextTick(() => menuItems()[0]?.focus());
|
|
64
|
+
}
|
|
65
|
+
function toggleMenu() {
|
|
66
|
+
if (props.disabled) return;
|
|
67
|
+
if (isOpen.value) close();
|
|
68
|
+
else openMenu();
|
|
69
|
+
}
|
|
70
|
+
const { handleTriggerKeydown, handlePanelKeydown } = useMenuKeyboard({
|
|
71
|
+
items: menuItems,
|
|
72
|
+
isOpen: () => isOpen.value,
|
|
73
|
+
open: () => openMenu(false),
|
|
74
|
+
close
|
|
75
|
+
});
|
|
76
|
+
function handleSelect(item) {
|
|
77
|
+
if (item.disabled) return;
|
|
78
|
+
emit("select", item);
|
|
79
|
+
close();
|
|
80
|
+
}
|
|
81
|
+
return (_ctx, _cache) => {
|
|
82
|
+
return openBlock(), createElementBlock(Fragment, null, [createElementVNode("span", {
|
|
83
|
+
ref_key: "rootRef",
|
|
84
|
+
ref: rootRef,
|
|
85
|
+
class: "mint-action-menu"
|
|
86
|
+
}, [renderSlot(_ctx.$slots, "trigger", {
|
|
87
|
+
open: unref(isOpen),
|
|
88
|
+
toggle: toggleMenu,
|
|
89
|
+
keydown: unref(handleTriggerKeydown)
|
|
90
|
+
}, () => [createVNode(BaseButton_default, {
|
|
91
|
+
variant: "secondary",
|
|
92
|
+
size: "sm",
|
|
93
|
+
icon: unref(Ellipsis),
|
|
94
|
+
label: __props.label,
|
|
95
|
+
disabled: __props.disabled,
|
|
96
|
+
"aria-haspopup": "menu",
|
|
97
|
+
"aria-expanded": unref(isOpen),
|
|
98
|
+
onClick: toggleMenu,
|
|
99
|
+
onKeydown: unref(handleTriggerKeydown)
|
|
100
|
+
}, null, 8, [
|
|
101
|
+
"icon",
|
|
102
|
+
"label",
|
|
103
|
+
"disabled",
|
|
104
|
+
"aria-expanded",
|
|
105
|
+
"onKeydown"
|
|
106
|
+
])])], 512), (openBlock(), createBlock(Teleport, { to: "body" }, [unref(isOpen) ? (openBlock(), createElementBlock("div", {
|
|
107
|
+
key: 0,
|
|
108
|
+
ref_key: "panelRef",
|
|
109
|
+
ref: panelRef,
|
|
110
|
+
class: "mint-popover mint-action-menu__panel",
|
|
111
|
+
style: normalizeStyle(unref(panelStyle)),
|
|
112
|
+
role: "menu",
|
|
113
|
+
onKeydown: _cache[0] || (_cache[0] = (...args) => unref(handlePanelKeydown) && unref(handlePanelKeydown)(...args))
|
|
114
|
+
}, [(openBlock(true), createElementBlock(Fragment, null, renderList(__props.items, (item, index) => {
|
|
115
|
+
return openBlock(), createElementBlock(Fragment, { key: item.id }, [item.danger && index > 0 && !__props.items[index - 1].danger ? (openBlock(), createElementBlock("div", _hoisted_1)) : createCommentVNode("", true), item.href !== void 0 && !item.disabled ? (openBlock(), createElementBlock("a", {
|
|
116
|
+
key: 1,
|
|
117
|
+
href: item.href,
|
|
118
|
+
target: item.target,
|
|
119
|
+
rel: item.target === "_blank" ? "noopener noreferrer" : void 0,
|
|
120
|
+
class: normalizeClass(["mint-menu-item mint-action-menu__item", { "mint-menu-item--danger mint-action-menu__item--danger": item.danger }]),
|
|
121
|
+
role: "menuitem",
|
|
122
|
+
onClick: ($event) => handleSelect(item)
|
|
123
|
+
}, toDisplayString(item.label), 11, _hoisted_2)) : (openBlock(), createElementBlock("button", {
|
|
124
|
+
key: 2,
|
|
125
|
+
type: "button",
|
|
126
|
+
class: normalizeClass(["mint-menu-item mint-action-menu__item", { "mint-menu-item--danger mint-action-menu__item--danger": item.danger }]),
|
|
127
|
+
role: item.checked === void 0 ? "menuitem" : "menuitemradio",
|
|
128
|
+
"aria-checked": item.checked,
|
|
129
|
+
disabled: item.disabled,
|
|
130
|
+
onClick: ($event) => handleSelect(item)
|
|
131
|
+
}, [item.checked !== void 0 ? (openBlock(), createBlock(unref(Check), {
|
|
132
|
+
key: 0,
|
|
133
|
+
class: normalizeClass(["mint-action-menu__check", { "mint-action-menu__check--off": !item.checked }]),
|
|
134
|
+
size: 14,
|
|
135
|
+
"aria-hidden": "true"
|
|
136
|
+
}, null, 8, ["class"])) : createCommentVNode("", true), createTextVNode(" " + toDisplayString(item.label), 1)], 10, _hoisted_3))], 64);
|
|
137
|
+
}), 128))], 36)) : createCommentVNode("", true)]))], 64);
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/composables/useRequest.ts
|
|
143
|
+
/** Loading, error and timestamp state for one request at a time; `run()` drives it. */
|
|
144
|
+
function useRequest(defaultErrorMessage = "Request failed.") {
|
|
145
|
+
const isLoading = ref(false);
|
|
146
|
+
const error = ref(null);
|
|
147
|
+
const lastLoadedAt = ref(null);
|
|
148
|
+
const lastSavedAt = ref(null);
|
|
149
|
+
const lastRunAt = ref(null);
|
|
150
|
+
function clearError() {
|
|
151
|
+
error.value = null;
|
|
152
|
+
}
|
|
153
|
+
function readErrorMessage$1(value, fallback = defaultErrorMessage) {
|
|
154
|
+
return readErrorMessage(value, fallback);
|
|
155
|
+
}
|
|
156
|
+
function setError(value, fallback) {
|
|
157
|
+
const message = readErrorMessage$1(value, fallback);
|
|
158
|
+
error.value = message;
|
|
159
|
+
return message;
|
|
160
|
+
}
|
|
161
|
+
let runToken = 0;
|
|
162
|
+
let activeController = null;
|
|
163
|
+
function cancel() {
|
|
164
|
+
runToken += 1;
|
|
165
|
+
activeController?.abort();
|
|
166
|
+
activeController = null;
|
|
167
|
+
isLoading.value = false;
|
|
168
|
+
}
|
|
169
|
+
async function run(operation, options = {}) {
|
|
170
|
+
if (options.abortPrevious !== false) activeController?.abort();
|
|
171
|
+
const token = runToken += 1;
|
|
172
|
+
const controller = new AbortController();
|
|
173
|
+
activeController = controller;
|
|
174
|
+
const callerSignal = options.signal;
|
|
175
|
+
if (callerSignal) {
|
|
176
|
+
if (callerSignal.aborted) controller.abort(callerSignal.reason);
|
|
177
|
+
else callerSignal.addEventListener("abort", () => controller.abort(callerSignal.reason), { once: true });
|
|
178
|
+
}
|
|
179
|
+
const isCurrent = () => token === runToken;
|
|
180
|
+
isLoading.value = true;
|
|
181
|
+
clearError();
|
|
182
|
+
try {
|
|
183
|
+
const response = await operation(controller.signal);
|
|
184
|
+
if (isCurrent() && options.kind) {
|
|
185
|
+
const settledAt = /* @__PURE__ */ new Date();
|
|
186
|
+
if (options.kind === "load") lastLoadedAt.value = settledAt;
|
|
187
|
+
else if (options.kind === "save") lastSavedAt.value = settledAt;
|
|
188
|
+
else lastRunAt.value = settledAt;
|
|
189
|
+
}
|
|
190
|
+
return response;
|
|
191
|
+
} catch (err) {
|
|
192
|
+
if (isCurrent()) setError(err, options.errorMessage);
|
|
193
|
+
throw err;
|
|
194
|
+
} finally {
|
|
195
|
+
if (isCurrent()) isLoading.value = false;
|
|
196
|
+
if (activeController === controller) activeController = null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
isLoading,
|
|
201
|
+
error,
|
|
202
|
+
lastLoadedAt,
|
|
203
|
+
lastSavedAt,
|
|
204
|
+
lastRunAt,
|
|
205
|
+
clearError,
|
|
206
|
+
readErrorMessage: readErrorMessage$1,
|
|
207
|
+
setError,
|
|
208
|
+
run,
|
|
209
|
+
cancel
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
//#endregion
|
|
213
|
+
export { ActionMenu_default as n, useRequest as t };
|
|
214
|
+
|
|
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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBA,MAAM,QAAQ;EAId,MAAM,OAAO;EAWb,MAAM,WAAW,IAAwB,IAAI;EAI7C,MAAM,EAAE,QAAQ,SAAS,YAAY,MAAM,UAAU,iBAAiB;GACpE,YAAY,CAAC,QAAQ;GACrB,cAAc,KAAK,MAAM;GACzB,eAAe,KAAK,OAAO;EAC7B,CAAC;EACD,MAAM,EAAE,OAAO,eAAe,iBAAiB,YAAY,UAAU,QAAQ,EAAE,WAAW,aAAa,CAAC;EAIxG,gBAAgB;GACd,WAAW,QAAQ,QAAQ,OAAO,cAAc,QAAQ,KAAK;EAC/D,CAAC;EAED,SAAS,YAA2B;GAClC,IAAI,CAAC,SAAS,OAAO,OAAO,CAAC;GAC7B,OAAO,MAAM,KACX,SAAS,MAAM,iBAA8B,wCAAwC,CACvF;EACF;;EAGA,SAAS,SAAS,aAAa,MAAM;GACnC,IAAI,MAAM,YAAY,OAAO,OAAO;GACpC,KAAK;GACL,IAAI,YACF,eAAoB,UAAU,CAAC,CAAC,EAAE,EAAE,MAAM,CAAC;EAE/C;EAEA,SAAS,aAAa;GACpB,IAAI,MAAM,UAAU;GACpB,IAAI,OAAO,OAAO,MAAM;QACnB,SAAS;EAChB;EAEA,MAAM,EAAE,sBAAsB,uBAAuB,gBAAgB;GACnE,OAAO;GACP,cAAc,OAAO;GACrB,YAAY,SAAS,KAAK;GAC1B;EACF,CAAC;EAED,SAAS,aAAa,MAAsB;GAC1C,IAAI,KAAK,UAAU;GACnB,KAAK,UAAU,IAAI;GACnB,MAAM;EACR;;GAIE,OAAA,UAAA,GAAA,mBAAA,UAAA,MAAA,CAAA,mBAeO,QAAA;IAfG,SAAA;IAAJ,KAAI;IAAU,OAAM;GAExB,GAAA,CAAA,WAYO,KAAA,QAAA,WAAA;IAZe,MAAM,MAAA,MAAA;IAAS,QAAQ;IAAa,SAAS,MAAA,oBAAA;GAAnE,SAYO,CAXL,YAUE,oBAAA;IATA,SAAQ;IACR,MAAK;IACJ,MAAM,MAAA,QAAA;IACN,OAAO,QAAA;IACP,UAAU,QAAA;IACX,iBAAc;IACb,iBAAe,MAAA,MAAA;IACf,SAAO;IACP,WAAS,MAAA,oBAAA;;;;;;;GAKhB,CAAA,CAAA,CAAA,CAAA,GAAA,GAAA,IAAA,UAAA,GAAA,YA8CW,UAAA,EA9CD,IAAG,OAAM,GAAA,CAET,MAAA,MAAA,KADR,UAAA,GAAA,mBA4CM,OAAA;;IA1CA,SAAA;IAAJ,KAAI;IACJ,OAAM;IACL,OAAK,eAAE,MAAA,UAAA,CAAU;IAClB,MAAK;IACJ,WAAO,OAAA,OAAA,OAAA,MAAE,GAAA,SAAA,MAAA,kBAAA,KAAA,MAAA,kBAAA,CAAA,CAAA,GAAA,IAAA;GAEV,GAAA,EAAA,UAAA,IAAA,GAAA,mBAmCW,UAAA,MAAA,WAnCuB,QAAA,QAAhB,MAAM,UAAK;IAAkB,OAAA,UAAA,GAAA,mBAAA,UAAA,EAAA,KAAA,KAAK,GAAA,GAAA,CAE1C,KAAK,UAAU,QAAK,KAAA,CAAS,QAAA,MAAM,QAAK,EAAA,CAAM,UADtD,UAAA,GAAA,mBAIE,OAJF,UAIE,KAAA,mBAAA,IAAA,IAAA,GAEM,KAAK,SAAS,KAAA,KAAS,CAAK,KAAK,YADzC,UAAA,GAAA,mBAUI,KAAA;;KARD,MAAM,KAAK;KACX,QAAQ,KAAK;KACb,KAAK,KAAK,WAAM,WAAA,wBAAwC,KAAA;KACxD,OAAK,eAAA,CAAA,yCAAA,EAAA,yDAAuG,KAAK,OAAM,CAAA,CAAA;KACxH,MAAK;KACJ,UAAK,WAAE,aAAa,IAAI;IAEtB,GAAA,gBAAA,KAAK,KAAK,GAAA,IAAA,UAAA,MAEf,UAAA,GAAA,mBAiBS,UAAA;;KAfP,MAAK;KACJ,OAAK,eAAA,CAAA,yCAAA,EAAA,yDAAuG,KAAK,OAAM,CAAA,CAAA;KACvH,MAAM,KAAK,YAAY,KAAA,IAAS,aAAA;KAChC,gBAAc,KAAK;KACnB,UAAU,KAAK;KACf,UAAK,WAAE,aAAa,IAAI;IAGjB,GAAA,CAAA,KAAK,YAAY,KAAA,KADzB,UAAA,GAAA,YAME,MAAA,KAAA,GAAA;;KAJA,OAAK,eAAA,CAAC,2BAAyB,EAAA,gCAAA,CACY,KAAK,QAAO,CAAA,CAAA;KACtD,MAAM;KACP,eAAY;IACZ,GAAA,MAAA,GAAA,CAAA,OAAA,CAAA,KAAA,mBAAA,IAAA,IAAA,GAAA,gBAAA,MACF,gBAAG,KAAK,KAAK,GAAA,CAAA,CAAA,GAAA,IAAA,UAAA,EAAA,GAAA,EAAA;;;;;;;;AEtEvB,SAAgB,WACd,sBAAsB,mBACJ;CAClB,MAAM,YAAY,IAAI,KAAK;CAC3B,MAAM,QAAQ,IAAmB,IAAI;CACrC,MAAM,eAAe,IAAiB,IAAI;CAC1C,MAAM,cAAc,IAAiB,IAAI;CACzC,MAAM,YAAY,IAAiB,IAAI;CAEvC,SAAS,aAAmB;EAC1B,MAAM,QAAQ;CAChB;CAEA,SAAS,mBAAiB,OAAgB,WAAW,qBAA6B;EAChF,OAAO,iBAAuB,OAAO,QAAQ;CAC/C;CAEA,SAAS,SAAS,OAAgB,UAA2B;EAC3D,MAAM,UAAU,mBAAiB,OAAO,QAAQ;EAChD,MAAM,QAAQ;EACd,OAAO;CACT;CAKA,IAAI,WAAW;CACf,IAAI,mBAA2C;CAE/C,SAAS,SAAe;EACtB,YAAY;EACZ,kBAAkB,MAAM;EACxB,mBAAmB;EACnB,UAAU,QAAQ;CACpB;CAEA,eAAe,IACb,WACA,UAA6B,CAAC,GAClB;EACZ,IAAI,QAAQ,kBAAkB,OAC5B,kBAAkB,MAAM;EAG1B,MAAM,QAAS,YAAY;EAC3B,MAAM,aAAa,IAAI,gBAAgB;EACvC,mBAAmB;EAEnB,MAAM,eAAe,QAAQ;EAC7B,IAAI,cAAc;GAChB,IAAI,aAAa,SAAS,WAAW,MAAM,aAAa,MAAM;QACzD,aAAa,iBAChB,eACM,WAAW,MAAM,aAAa,MAAM,GAC1C,EAAE,MAAM,KAAK,CACf;EACF;EAEA,MAAM,kBAA2B,UAAU;EAE3C,UAAU,QAAQ;EAClB,WAAW;EACX,IAAI;GACF,MAAM,WAAW,MAAM,UAAU,WAAW,MAAM;GAClD,IAAI,UAAU,KAAK,QAAQ,MAAM;IAC/B,MAAM,4BAAY,IAAI,KAAK;IAC3B,IAAI,QAAQ,SAAS,QAAQ,aAAa,QAAQ;SAC7C,IAAI,QAAQ,SAAS,QAAQ,YAAY,QAAQ;SACjD,UAAU,QAAQ;GACzB;GACA,OAAO;EACT,SAAS,KAAK;GACZ,IAAI,UAAU,GACZ,SAAS,KAAK,QAAQ,YAAY;GAEpC,MAAM;EACR,UAAU;GACR,IAAI,UAAU,GACZ,UAAU,QAAQ;GAEpB,IAAI,qBAAqB,YACvB,mBAAmB;EAEvB;CACF;CAEA,OAAO;EACL;EACA;EACA;EACA;EACA;EACA;EACA,kBAAA;EACA;EACA;EACA;CACF;AACF"}
|
package/dist/utils/time.d.ts
CHANGED
|
@@ -15,5 +15,12 @@ export declare function snapToSlot(time: string, stepMinutes: number): string;
|
|
|
15
15
|
export declare function addMinutes(time: string, minutes: number): string;
|
|
16
16
|
export declare function compareTime(a: string, b: string): number;
|
|
17
17
|
export declare function findNearestTimeSlotIndex(time: string, slots: readonly string[]): number;
|
|
18
|
+
/**
|
|
19
|
+
* "in 5 minutes" / "2 hours ago" up to a week, then a locale date.
|
|
20
|
+
* With `weeks`, times up to five weeks away read as a week count first.
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatRelativeTime(value: Date | string, { weeks }?: {
|
|
23
|
+
weeks?: boolean;
|
|
24
|
+
}): string;
|
|
18
25
|
export declare function toMinutes(time: string): number;
|
|
19
26
|
export declare function fromMinutes(total: number): string;
|
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 () => {
|
|
@@ -6,11 +6,13 @@ import AppTopBar from '../../components/AppTopBar.vue'
|
|
|
6
6
|
import ExperimentPopover from '../../components/ExperimentPopover.vue'
|
|
7
7
|
import ExperimentSelectorModal from '../../components/ExperimentSelectorModal.vue'
|
|
8
8
|
import { usePlatform } from '../../composables/usePlatform'
|
|
9
|
+
import { providePluginIdentity } from '../../composables/pluginIdentity'
|
|
9
10
|
import { APP_EXPERIMENT_KEY, type AppExperimentState } from '../../composables/useAppExperiment'
|
|
10
11
|
import { defineControlModel, defineControls } from '../../composables/useControlSchema'
|
|
11
12
|
import ThemeToggle from '../../components/ThemeToggle.vue'
|
|
12
13
|
import SettingsModal from '../../components/SettingsModal.vue'
|
|
13
14
|
import type { TopBarSettingsConfig } from '../../types/components'
|
|
15
|
+
import type { TopBarJob } from '../../types/jobs'
|
|
14
16
|
|
|
15
17
|
// Mock the usePlatform composable
|
|
16
18
|
vi.mock('../../composables/usePlatform', () => ({
|
|
@@ -907,7 +909,7 @@ describe('AppTopBar', () => {
|
|
|
907
909
|
const imageIcons = wrapper.findAll('.mint-plugin-icon__img')
|
|
908
910
|
expect(imageIcons).toHaveLength(1)
|
|
909
911
|
expect(imageIcons[0].attributes('src')).toBe(pngIcon)
|
|
910
|
-
expect(wrapper.
|
|
912
|
+
expect(wrapper.get('.mint-page-selector__item-hint').text()).toBe('Overview')
|
|
911
913
|
})
|
|
912
914
|
})
|
|
913
915
|
|
|
@@ -1058,7 +1060,7 @@ describe('AppTopBar', () => {
|
|
|
1058
1060
|
expect(wrapper.find('.mint-topbar__identity').exists()).toBe(true)
|
|
1059
1061
|
})
|
|
1060
1062
|
|
|
1061
|
-
it('should pass
|
|
1063
|
+
it('should not pass the manifest color to PluginIcon when the plugin declares one', () => {
|
|
1062
1064
|
const iconPath = 'M13 10V3L4 14h7v7l9-11h-7z'
|
|
1063
1065
|
mockPlatformCtx({
|
|
1064
1066
|
isIntegrated: true,
|
|
@@ -1071,7 +1073,7 @@ describe('AppTopBar', () => {
|
|
|
1071
1073
|
|
|
1072
1074
|
const wrapper = createWrapper({ title: 'Test App' })
|
|
1073
1075
|
|
|
1074
|
-
expect(wrapper.findComponent({ name: 'PluginIcon' }).props('color')).
|
|
1076
|
+
expect(wrapper.findComponent({ name: 'PluginIcon' }).props('color')).toBeUndefined()
|
|
1075
1077
|
})
|
|
1076
1078
|
|
|
1077
1079
|
it('should ignore the custom icon slot when integrated', () => {
|
|
@@ -1236,6 +1238,26 @@ describe('AppTopBar', () => {
|
|
|
1236
1238
|
expect(wrapper.find('.mint-topbar__plugin-name').exists()).toBe(false)
|
|
1237
1239
|
})
|
|
1238
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
|
+
|
|
1239
1261
|
it('should hide the plugin name when a page selector is shown', () => {
|
|
1240
1262
|
mockPlatformCtx({ isIntegrated: true, plugin: integratedPlugin })
|
|
1241
1263
|
|
|
@@ -1268,6 +1290,54 @@ describe('AppTopBar', () => {
|
|
|
1268
1290
|
|
|
1269
1291
|
expect(wrapper.find('.mint-topbar__title-only').exists()).toBe(false)
|
|
1270
1292
|
})
|
|
1293
|
+
|
|
1294
|
+
it.each([
|
|
1295
|
+
{ fields: { subtitle: 'Peak Review', description: 'Reviews LC-MS peaks' }, expected: 'Peak Review' },
|
|
1296
|
+
{ fields: { description: 'Reviews LC-MS peaks' }, expected: 'Reviews LC-MS peaks' },
|
|
1297
|
+
{ fields: {}, expected: null },
|
|
1298
|
+
])('should show $expected under the integrated name when the plugin has $fields', ({ fields, expected }) => {
|
|
1299
|
+
mockPlatformCtx({ isIntegrated: true, plugin: { ...integratedPlugin, ...fields } })
|
|
1300
|
+
|
|
1301
|
+
const wrapper = createWrapper()
|
|
1302
|
+
|
|
1303
|
+
const subtitle = wrapper.find('.mint-topbar__plugin-subtitle')
|
|
1304
|
+
expect(subtitle.exists() ? subtitle.text() : null).toBe(expected)
|
|
1305
|
+
})
|
|
1306
|
+
})
|
|
1307
|
+
|
|
1308
|
+
describe('seeded standalone identity', () => {
|
|
1309
|
+
const seedIcon = 'M13 10V3L4 14h7v7l9-11h-7z'
|
|
1310
|
+
|
|
1311
|
+
beforeEach(() => {
|
|
1312
|
+
providePluginIdentity({
|
|
1313
|
+
plugin: { name: 'ms-planner', displayName: 'MS Planner', subtitle: 'Experiment Scheduler', icon: seedIcon },
|
|
1314
|
+
})
|
|
1315
|
+
})
|
|
1316
|
+
|
|
1317
|
+
afterEach(() => providePluginIdentity({ plugin: { name: '' } }))
|
|
1318
|
+
|
|
1319
|
+
it('should show the seeded name, subtitle and icon when standalone', () => {
|
|
1320
|
+
const wrapper = createWrapper()
|
|
1321
|
+
|
|
1322
|
+
expect(wrapper.get('.mint-topbar-title').text()).toBe('MS Planner')
|
|
1323
|
+
expect(wrapper.get('.mint-topbar-subtitle').text()).toBe('Experiment Scheduler')
|
|
1324
|
+
expect(wrapper.findComponent({ name: 'PluginIcon' }).props('icon')).toBe(seedIcon)
|
|
1325
|
+
})
|
|
1326
|
+
|
|
1327
|
+
it.each([
|
|
1328
|
+
{ slot: 'icon' },
|
|
1329
|
+
{ slot: 'logo' },
|
|
1330
|
+
])('should keep the title props and the #$slot slot over the seed', ({ slot }) => {
|
|
1331
|
+
const wrapper = createWrapper(
|
|
1332
|
+
{ title: 'Planner Dev', subtitle: 'Local build' },
|
|
1333
|
+
{ [slot]: '<span class="custom-brand">P</span>' },
|
|
1334
|
+
)
|
|
1335
|
+
|
|
1336
|
+
expect(wrapper.get('.mint-topbar-title').text()).toBe('Planner Dev')
|
|
1337
|
+
expect(wrapper.get('.mint-topbar-subtitle').text()).toBe('Local build')
|
|
1338
|
+
expect(wrapper.find('.custom-brand').exists()).toBe(true)
|
|
1339
|
+
expect(wrapper.findComponent({ name: 'PluginIcon' }).exists()).toBe(false)
|
|
1340
|
+
})
|
|
1271
1341
|
})
|
|
1272
1342
|
})
|
|
1273
1343
|
|
|
@@ -1434,4 +1504,188 @@ describe('AppTopBar', () => {
|
|
|
1434
1504
|
expect(removeListener).toHaveBeenCalledWith('scroll', expect.any(Function), expect.objectContaining({ capture: true }))
|
|
1435
1505
|
})
|
|
1436
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
|
+
})
|
|
1437
1691
|
})
|