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
+19
View File
@@ -133,6 +133,13 @@ interface EmailComposerProps {
}) => void | Promise<void>;
onScheduledSendCreated?: () => void | Promise<void>;
onClose?: () => void;
/**
* When provided, the composer assigns its close handler to `current`. The
* handler shows the unsaved-changes dialog when the draft is dirty, so a
* host (e.g. the Pro tab bar's close button) can route an external close
* request through the same guard instead of discarding silently.
*/
requestCloseRef?: React.MutableRefObject<(() => void) | null>;
onDiscardDraft?: (draftId: string) => void;
onSaveState?: (data: ComposerDraftData) => void;
className?: string;
@@ -229,6 +236,7 @@ export function EmailComposer({
onSend,
onScheduledSendCreated,
onClose,
requestCloseRef,
onDiscardDraft,
onSaveState,
className,
@@ -1815,6 +1823,17 @@ export function EmailComposer({
}
};
// Expose the dirty-aware close handler so external hosts (e.g. the Pro tab
// bar) can trigger the same "Save or discard draft?" guard. Re-assigned on
// every render to capture the latest closure over the live refs/state.
useEffect(() => {
if (!requestCloseRef) return;
requestCloseRef.current = handleClose;
return () => {
requestCloseRef.current = null;
};
});
const handleComposerKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.defaultPrevented) return;
+20 -1
View File
@@ -7,7 +7,7 @@ import { ErrorBoundary, ComposerErrorFallback } from "@/components/error";
import { useAuthStore } from "@/stores/auth-store";
import { useEmailStore } from "@/stores/email-store";
import { toast } from "@/stores/toast-store";
import { useProTabStore, type ProComposeTabData } from "@/stores/pro-tab-store";
import { useProTabStore, registerProTabCloseInterceptor, type ProComposeTabData } from "@/stores/pro-tab-store";
import { debug } from "@/lib/debug";
interface ProComposeTabBodyProps {
@@ -38,6 +38,10 @@ export function ProComposeTabBody({ tabId, data }: ProComposeTabBodyProps) {
const tabIdRef = useRef(tabId);
tabIdRef.current = tabId;
// Set by the composer to its dirty-aware close handler. Lets the Pro tab
// bar's "X" route through the same "Save or discard draft?" guard.
const requestCloseRef = useRef<(() => void) | null>(null);
const handleScheduledSendCreated = useCallback(async () => {
if (client) {
await refreshScheduledMetadata(client);
@@ -120,6 +124,20 @@ export function ProComposeTabBody({ tabId, data }: ProComposeTabBodyProps) {
closeTab(tabIdRef.current);
}, [closeTab]);
// Register a close interceptor so closing the tab from the tab bar (the
// "X" button or middle-click) goes through the composer's unsaved-changes
// guard, mirroring the non-Pro inline composer.
useEffect(() => {
const id = tabIdRef.current;
return registerProTabCloseInterceptor(id, () => {
if (requestCloseRef.current) {
requestCloseRef.current();
} else {
closeTab(id);
}
});
}, [closeTab]);
const handleDiscardDraft = useCallback(async (draftId: string) => {
if (!client) return;
try {
@@ -160,6 +178,7 @@ export function ProComposeTabBody({ tabId, data }: ProComposeTabBodyProps) {
onSend={handleSend}
onScheduledSendCreated={handleScheduledSendCreated}
onClose={handleClose}
requestCloseRef={requestCloseRef}
onDiscardDraft={handleDiscardDraft}
onSaveState={handleSaveState}
className="flex-1"