From 36b0336a3b369250d8be439d45376b5496ce4f7f Mon Sep 17 00:00:00 2001 From: Alexander Serdyukov Date: Sat, 13 Jun 2026 23:25:11 +0400 Subject: [PATCH 01/11] Add gaurd to FASTA selection exporter --- src/app/core/mapmanagers/CommonEventManager.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/app/core/mapmanagers/CommonEventManager.ts b/src/app/core/mapmanagers/CommonEventManager.ts index ee3bad3..d5f836f 100644 --- a/src/app/core/mapmanagers/CommonEventManager.ts +++ b/src/app/core/mapmanagers/CommonEventManager.ts @@ -697,6 +697,7 @@ class CommonEventManager { " and right selection border is ", toPx ); + return; } const bpResolution = this.mapManager.viewAndLayersManager.currentViewState.resolutionDesciptor @@ -706,6 +707,20 @@ class CommonEventManager { .map((px) => this.mapManager.contigDimensionHolder.getStartBpOfPx(px, bpResolution) ); + if ( + !Number.isFinite(fromBpX) || + !Number.isFinite(fromBpY) || + !Number.isFinite(toBpX) || + !Number.isFinite(toBpY) + ) { + console.warn("Not exporting FASTA because selection coordinates are invalid", { + fromBpX, + fromBpY, + toBpX, + toBpY, + }); + return; + } console.log("Bps: ", fromBpX, fromBpY, toBpX, toBpY, " pxs ", fromPx, toPx); this.mapManager.networkManager.requestManager From d416e322cf5462e50bba5cbe7bd05b86655c2e6d Mon Sep 17 00:00:00 2001 From: Alexander Serdyukov Date: Sat, 13 Jun 2026 23:40:17 +0400 Subject: [PATCH 02/11] Fasta notifications and Esc processing --- .../core/mapmanagers/CommonEventManager.ts | 16 ++-- src/app/ui/MainUIComponent.vue | 29 +++++++ .../notifications/NotificationCenterModal.vue | 9 ++ .../upper_ribbon/AGPFileSelector.vue | 9 ++ .../upper_ribbon/CoolerConverter.vue | 17 ++++ .../upper_ribbon/DotplotGenerator.vue | 13 +++ .../upper_ribbon/FASTAFileSelector.vue | 9 ++ .../upper_ribbon/FastaLinkWarningModal.vue | 9 ++ .../upper_ribbon/FileWizardModal.vue | 10 +++ .../components/upper_ribbon/NavigationBar.vue | 17 ++++ .../upper_ribbon/OpenFileSelector.vue | 9 ++ .../upper_ribbon/RenderingPipelineModal.vue | 9 ++ .../upper_ribbon/ServerStatisticsModal.vue | 9 ++ .../components/upper_ribbon/TrackManager.vue | 27 +++++- .../upper_ribbon/UniversalFileSelector.vue | 9 ++ .../upper_ribbon/WorkerDiagnosticsModal.vue | 9 ++ src/app/ui/escapeDialogRegistry.ts | 86 +++++++++++++++++++ 17 files changed, 289 insertions(+), 7 deletions(-) create mode 100644 src/app/ui/escapeDialogRegistry.ts diff --git a/src/app/core/mapmanagers/CommonEventManager.ts b/src/app/core/mapmanagers/CommonEventManager.ts index d5f836f..25b066b 100644 --- a/src/app/core/mapmanagers/CommonEventManager.ts +++ b/src/app/core/mapmanagers/CommonEventManager.ts @@ -691,12 +691,13 @@ class CommonEventManager { .rightPx, ]; if (!fromPx || !toPx) { - console.log( - "Not exporting FASTA because left selection border is ", + const message = + "Cannot export FASTA for selection because there is no valid selection."; + console.warn(message, { fromPx, - " and right selection border is ", - toPx - ); + toPx, + }); + toast.error(message); return; } const bpResolution = @@ -713,12 +714,15 @@ class CommonEventManager { !Number.isFinite(toBpX) || !Number.isFinite(toBpY) ) { - console.warn("Not exporting FASTA because selection coordinates are invalid", { + const message = + "Cannot export FASTA for selection because selection coordinates are invalid."; + console.warn(message, { fromBpX, fromBpY, toBpX, toBpY, }); + toast.error(message); return; } console.log("Bps: ", fromBpX, fromBpY, toBpX, toBpY, " pxs ", fromPx, toPx); diff --git a/src/app/ui/MainUIComponent.vue b/src/app/ui/MainUIComponent.vue index 58bdc4f..a3248b7 100644 --- a/src/app/ui/MainUIComponent.vue +++ b/src/app/ui/MainUIComponent.vue @@ -129,6 +129,11 @@ import { type SessionVisualizationPreset, } from "@/app/stores/sessionStore"; import { useMatrixViewStore } from "@/app/stores/matrixViewStore"; +import { + dismissTopmostEscDialog, + hasAnyOpenEscDialog, + useEscDismissableDialog, +} from "@/app/ui/escapeDialogRegistry"; // Reactively use these refs only inside component // Pass them to Map Manager on creation as values, not Refs as objects @@ -160,6 +165,12 @@ const openProgressPct = ref(0); let openProgressInFlight = false; const wizardOpen = ref(false); +useEscDismissableDialog({ + priority: 1050, + isOpen: () => openProgressVisible.value, + requestClose: closeOpenProgress, +}); + function startOpenProgress() { if (openProgressTimer !== undefined) { return; @@ -199,6 +210,22 @@ function closeOpenProgress() { openProgressVisible.value = false; } +function handleGlobalEscape(event: KeyboardEvent): void { + if (event.key !== "Escape" || event.repeat) { + return; + } + if (dismissTopmostEscDialog()) { + event.preventDefault(); + event.stopPropagation(); + return; + } + if (!hasAnyOpenEscDialog()) { + mapManager.value?.eventManager.resetSelection(); + event.preventDefault(); + event.stopPropagation(); + } +} + function safeColorTranslator( value: unknown, fallback: string @@ -733,10 +760,12 @@ watch( ); onMounted(() => { + window.addEventListener("keydown", handleGlobalEscape, true); syncUiChromePalette(); }); onUnmounted(() => { + window.removeEventListener("keydown", handleGlobalEscape, true); stopOpenProgress(); }); diff --git a/src/app/ui/components/notifications/NotificationCenterModal.vue b/src/app/ui/components/notifications/NotificationCenterModal.vue index 9ec2401..1a24a39 100644 --- a/src/app/ui/components/notifications/NotificationCenterModal.vue +++ b/src/app/ui/components/notifications/NotificationCenterModal.vue @@ -96,10 +96,19 @@ import { useNotificationCenterStore, type NotificationLevel, } from "@/app/stores/notificationCenterStore"; +import { useEscDismissableDialog } from "@/app/ui/escapeDialogRegistry"; const notificationStore = useNotificationCenterStore(); const expandedEntryIds = ref([]); +useEscDismissableDialog({ + priority: 2060, + isOpen: () => notificationStore.isOpen, + requestClose: () => { + notificationStore.isOpen = false; + }, +}); + const orderedEntries = computed(() => notificationStore.entries.slice().reverse() ); diff --git a/src/app/ui/components/upper_ribbon/AGPFileSelector.vue b/src/app/ui/components/upper_ribbon/AGPFileSelector.vue index fea7082..5fb861d 100644 --- a/src/app/ui/components/upper_ribbon/AGPFileSelector.vue +++ b/src/app/ui/components/upper_ribbon/AGPFileSelector.vue @@ -126,6 +126,7 @@ import { FileTreeNode, extensionToDataType } from "../ComponentCommon"; import Tree from "primevue/tree"; import FileSelectionTable from "@/app/ui/components/common/FileSelectionTable.vue"; import type { FileSelectionTableEntry } from "@/app/ui/components/common/FileSelectionTableTypes"; +import { useEscDismissableDialog } from "@/app/ui/escapeDialogRegistry"; interface FinalTreeNode { isLeaf: boolean; @@ -157,6 +158,14 @@ const loadAGPModal = ref(null); const expandedKeys: Ref> = ref({}); +useEscDismissableDialog({ + priority: 1080, + isOpen: () => true, + requestClose: () => { + onDismissClicked(); + }, +}); + const fileEntries = computed(() => (filenames.value ?? []).map((filename) => ({ path: filename, diff --git a/src/app/ui/components/upper_ribbon/CoolerConverter.vue b/src/app/ui/components/upper_ribbon/CoolerConverter.vue index 7551495..a6a0111 100644 --- a/src/app/ui/components/upper_ribbon/CoolerConverter.vue +++ b/src/app/ui/components/upper_ribbon/CoolerConverter.vue @@ -265,6 +265,7 @@ import { ConversionToolchainStatusResponse } from "@/app/core/net/api/response"; import ConverterStatusChecker from "./converter/ConverterStatusChecker.vue"; import FileSelectionTable from "@/app/ui/components/common/FileSelectionTable.vue"; import type { FileSelectionTableEntry } from "@/app/ui/components/common/FileSelectionTableTypes"; +import { useEscDismissableDialog } from "@/app/ui/escapeDialogRegistry"; import UniversalFileSelector from "./UniversalFileSelector.vue"; const emit = defineEmits<{ @@ -296,6 +297,22 @@ const toolchainStatus: Ref = ref(null) const toolchainLoading: Ref = ref(true); let overwriteConfirmResolver: ((approved: boolean) => void) | null = null; +useEscDismissableDialog({ + priority: 1060, + isOpen: () => true, + requestClose: () => { + onDismissClicked(); + }, +}); + +useEscDismissableDialog({ + priority: 1075, + isOpen: () => overwriteConfirmVisible.value, + requestClose: () => { + cancelPendingOverwriteConfirm(); + }, +}); + function cancelPendingOverwriteConfirm(): void { overwriteConfirmVisible.value = false; overwriteConfirmMessage.value = ""; diff --git a/src/app/ui/components/upper_ribbon/DotplotGenerator.vue b/src/app/ui/components/upper_ribbon/DotplotGenerator.vue index fb440cd..1849063 100644 --- a/src/app/ui/components/upper_ribbon/DotplotGenerator.vue +++ b/src/app/ui/components/upper_ribbon/DotplotGenerator.vue @@ -279,6 +279,7 @@ import { StartDotplotJobsRequest } from "@/app/core/net/api/request"; import type { ConversionJobResponse, ConversionToolchainStatusResponse } from "@/app/core/net/api/response"; import FileSelectionTable from "@/app/ui/components/common/FileSelectionTable.vue"; import type { FileSelectionTableEntry } from "@/app/ui/components/common/FileSelectionTableTypes"; +import { useEscDismissableDialog } from "@/app/ui/escapeDialogRegistry"; import UniversalFileSelector from "@/app/ui/components/upper_ribbon/UniversalFileSelector.vue"; const emit = defineEmits<{ @@ -321,6 +322,18 @@ const alignmentThreads = ref(Math.max(1, navigator.hardwareConcurrency || 4)); const conversionThreads = ref(Math.max(1, navigator.hardwareConcurrency || 4)); const overwrite = ref(false); +useEscDismissableDialog({ + priority: 1060, + isOpen: () => true, + requestClose: () => { + if (step.value === "select") { + emit("dismissed"); + return; + } + step.value = "select"; + }, +}); + const fastaEntries = computed(() => fastaFiles.value.map((path) => ({ path })) ); diff --git a/src/app/ui/components/upper_ribbon/FASTAFileSelector.vue b/src/app/ui/components/upper_ribbon/FASTAFileSelector.vue index 29d1e78..c56f224 100644 --- a/src/app/ui/components/upper_ribbon/FASTAFileSelector.vue +++ b/src/app/ui/components/upper_ribbon/FASTAFileSelector.vue @@ -86,6 +86,7 @@ import type { NetworkManager } from "@/app/core/net/NetworkManager.js"; import { LinkFASTARequest } from "@/app/core/net/api/request"; import FileSelectionTable from "@/app/ui/components/common/FileSelectionTable.vue"; import type { FileSelectionTableEntry } from "@/app/ui/components/common/FileSelectionTableTypes"; +import { useEscDismissableDialog } from "@/app/ui/escapeDialogRegistry"; const emit = defineEmits<{ (e: "selected", fastaFilename: string): void; @@ -104,6 +105,14 @@ const modal: Ref = ref(null); const openFASTAModal = ref(null); +useEscDismissableDialog({ + priority: 1080, + isOpen: () => true, + requestClose: () => { + onDismissClicked(); + }, +}); + const fileEntries = computed(() => (filenames.value ?? []).map((filename) => ({ path: filename, diff --git a/src/app/ui/components/upper_ribbon/FastaLinkWarningModal.vue b/src/app/ui/components/upper_ribbon/FastaLinkWarningModal.vue index de2b3f1..a3cf838 100644 --- a/src/app/ui/components/upper_ribbon/FastaLinkWarningModal.vue +++ b/src/app/ui/components/upper_ribbon/FastaLinkWarningModal.vue @@ -69,6 +69,7 @@