diff --git a/src/components/favorites/QueryFavorites.tsx b/src/components/favorites/QueryFavorites.tsx index 7e5233e..3cfd1b8 100644 --- a/src/components/favorites/QueryFavorites.tsx +++ b/src/components/favorites/QueryFavorites.tsx @@ -36,6 +36,8 @@ export function QueryFavorites() { const [editDescValue, setEditDescValue] = useState(""); const [showNewCategory, setShowNewCategory] = useState(false); const [newCategoryName, setNewCategoryName] = useState(""); + const [pendingFavorite, setPendingFavorite] = useState(null); + const [showConfirm, setShowConfirm] = useState(false); const [pendingDelete, setPendingDelete] = useState<{ cat: string; count: number } | null>( null, ); @@ -67,7 +69,7 @@ export function QueryFavorites() { return groups; }, [filtered, categories]); - const handleClick = (fav: Favorite) => { + const openFavorite = (fav: Favorite) => { const store = useEditorStore.getState(); const activeTab = store.tabs.find((t) => t.id === store.activeTabId); if (activeTab && activeTab.type === "query") { @@ -78,6 +80,30 @@ export function QueryFavorites() { } }; + const handleClick = (fav: Favorite) => { + const store = useEditorStore.getState(); + const activeTab = store.tabs.find((t) => t.id === store.activeTabId); + if (activeTab?.type === "query" && activeTab.isDirty) { + setPendingFavorite(fav); + setShowConfirm(true); + return; + } + openFavorite(fav); + }; + + const handleConfirmReplace = () => { + if (pendingFavorite) { + openFavorite(pendingFavorite); + } + setPendingFavorite(null); + setShowConfirm(false); + }; + + const handleCancelReplace = () => { + setPendingFavorite(null); + setShowConfirm(false); + }; + const handleDoubleClick = (fav: Favorite) => { const store = useEditorStore.getState(); const tabId = store.addTab(); @@ -389,6 +415,13 @@ export function QueryFavorites() { }} onCancel={() => setPendingDelete(null)} /> + ); } diff --git a/src/components/favorites/__tests__/QueryFavorites.test.tsx b/src/components/favorites/__tests__/QueryFavorites.test.tsx index d6c79af..811e24b 100644 --- a/src/components/favorites/__tests__/QueryFavorites.test.tsx +++ b/src/components/favorites/__tests__/QueryFavorites.test.tsx @@ -2,9 +2,15 @@ import { fireEvent, render, screen } from "@testing-library/react"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { QueryFavorites } from "../QueryFavorites"; -const { useFavoritesStoreFn } = vi.hoisted(() => { - return { useFavoritesStoreFn: vi.fn() }; -}); +const { useFavoritesStoreFn, updateTabContent, addTab, editorState } = vi.hoisted(() => ({ + useFavoritesStoreFn: vi.fn(), + updateTabContent: vi.fn(), + addTab: vi.fn(() => "newTabId"), + editorState: { + tabs: [{ id: "tab1", type: "query", content: "", isDirty: false }], + activeTabId: "tab1", + }, +})); vi.mock("../../../stores/favoritesStore", () => ({ useFavoritesStore: useFavoritesStoreFn, @@ -13,10 +19,9 @@ vi.mock("../../../stores/favoritesStore", () => ({ vi.mock("../../../stores/editorStore", () => ({ useEditorStore: { getState: vi.fn(() => ({ - tabs: [{ id: "tab1", type: "query", content: "" }], - activeTabId: "tab1", - addTab: vi.fn(() => "newTabId"), - updateTabContent: vi.fn(), + ...editorState, + addTab, + updateTabContent, })), }, })); @@ -106,6 +111,12 @@ beforeEach(() => { storeState.updateFavorite.mockClear(); storeState.addCategory.mockClear(); storeState.deleteCategory.mockClear(); + updateTabContent.mockReset(); + addTab.mockReset().mockReturnValue("newTabId"); + editorState.tabs = [ + { id: "tab1", type: "query", content: "", isDirty: false }, + ]; + editorState.activeTabId = "tab1"; }); afterEach(() => { @@ -128,6 +139,50 @@ describe("QueryFavorites", () => { const { container } = render(); expect(container.querySelector(".flex.h-full.flex-col")).toBeInTheDocument(); }); + + it("confirms before replacing a dirty query tab", () => { + editorState.tabs = [ + { + id: "tab1", + type: "query", + content: "SELECT unsaved_work", + isDirty: true, + }, + ]; + render(); + + fireEvent.click(screen.getByText("Get Active Users")); + + expect(screen.getByText("Replace current tab content?")).toBeInTheDocument(); + expect(screen.getByText("Unsaved changes will be lost.")).toBeInTheDocument(); + expect(updateTabContent).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByRole("button", { name: "Confirm" })); + + expect(updateTabContent).toHaveBeenCalledWith( + "tab1", + "SELECT * FROM users WHERE active = 1", + ); + expect(screen.queryByText("Unsaved changes will be lost.")).not.toBeInTheDocument(); + }); + + it("keeps dirty query content when replacement is cancelled", () => { + editorState.tabs = [ + { + id: "tab1", + type: "query", + content: "SELECT unsaved_work", + isDirty: true, + }, + ]; + render(); + + fireEvent.click(screen.getByText("Get Active Users")); + fireEvent.click(screen.getByRole("button", { name: "Cancel" })); + + expect(updateTabContent).not.toHaveBeenCalled(); + expect(screen.queryByText("Unsaved changes will be lost.")).not.toBeInTheDocument(); + }); }); // ───────────────────────────────────────────────────────────────────────── @@ -213,3 +268,52 @@ describe("QueryFavorites — delete category confirmation (#337)", () => { expect(screen.getByText("1 favorite will be moved to Uncategorized.")).toBeInTheDocument(); }); }); + +// ───────────────────────────────────────────────────────────────────────── +// Issue #341: confirm before overwriting a dirty editor tab. +// ───────────────────────────────────────────────────────────────────────── +describe("QueryFavorites — dirty-tab overwrite confirmation (#341)", () => { + it("confirms before replacing a dirty query tab", () => { + editorState.tabs = [ + { + id: "tab1", + type: "query", + content: "SELECT unsaved_work", + isDirty: true, + }, + ]; + render(); + + fireEvent.click(screen.getByText("Get Active Users")); + + expect(screen.getByText("Replace current tab content?")).toBeInTheDocument(); + expect(screen.getByText("Unsaved changes will be lost.")).toBeInTheDocument(); + expect(updateTabContent).not.toHaveBeenCalled(); + + fireEvent.click(screen.getByRole("button", { name: "Confirm" })); + + expect(updateTabContent).toHaveBeenCalledWith( + "tab1", + "SELECT * FROM users WHERE active = 1", + ); + expect(screen.queryByText("Unsaved changes will be lost.")).not.toBeInTheDocument(); + }); + + it("keeps dirty query content when replacement is cancelled", () => { + editorState.tabs = [ + { + id: "tab1", + type: "query", + content: "SELECT unsaved_work", + isDirty: true, + }, + ]; + render(); + + fireEvent.click(screen.getByText("Get Active Users")); + fireEvent.click(screen.getByRole("button", { name: "Cancel" })); + + expect(updateTabContent).not.toHaveBeenCalled(); + expect(screen.queryByText("Unsaved changes will be lost.")).not.toBeInTheDocument(); + }); +});