fix(pro): prompt to save or discard draft when closing compose tab via tab-bar X

This commit is contained in:
Kristofer Pettijohn
2026-06-30 06:51:24 +02:00
committed by Linus Rath
parent 16daf6ea03
commit 2a41e73bf9
4 changed files with 74 additions and 3 deletions
+33
View File
@@ -87,6 +87,12 @@ interface ProTabState {
openComposeTab: (data: ProComposeTabData) => string;
openEmailTab: (data: ProEmailTabData) => string;
closeTab: (id: string) => void;
/**
* Request closing a tab, honouring any registered close interceptor (e.g. a
* compose tab with unsaved changes that wants to show the "Save or discard
* draft?" dialog first). Falls back to `closeTab` when none is registered.
*/
requestCloseTab: (id: string) => void;
setActiveTab: (id: string) => void;
setFocusedPane: (paneId: ProPaneId) => void;
@@ -131,6 +137,21 @@ const HOME_TAB: ProTab = {
paneId: 'main',
};
/**
* Module-level registry of tab close interceptors. Kept outside the persisted
* Zustand state so functions are never serialised. A compose tab registers a
* handler here so an external close request (tab-bar "X", middle-click) routes
* through the composer's unsaved-changes guard instead of closing instantly.
*/
const closeInterceptors = new Map<string, () => void>();
export function registerProTabCloseInterceptor(id: string, fn: () => void): () => void {
closeInterceptors.set(id, fn);
return () => {
if (closeInterceptors.get(id) === fn) closeInterceptors.delete(id);
};
}
function makeId(): string {
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
return crypto.randomUUID();
@@ -265,11 +286,23 @@ export const useProTabStore = create<ProTabState>()(
return newTab.id;
},
requestCloseTab: (id) => {
const interceptor = closeInterceptors.get(id);
if (interceptor) {
interceptor();
return;
}
get().closeTab(id);
},
closeTab: (id) => {
const state = get();
const tab = state.tabs.find((t) => t.id === id);
if (!tab || !tab.closeable) return;
// Drop any registered close interceptor for this tab.
closeInterceptors.delete(id);
const removedPane = tab.paneId;
const newTabs = state.tabs.filter((t) => t.id !== id);
const newLoaded = state.loadedTabIds.filter((tid) => tid !== id);