Skip to content

Commit 13b4936

Browse files
committed
Add browser extension popup UI for TanStack Query DevTools
- Add popup.html entry point and PopupApp component - Implement usePopupConnection hook for popup-specific state management - Update manifest.json to register popup interface - Extend background script to handle popup communication - Add popup-specific styling and React entry point - Update state sync and message types to support popup context
1 parent 91f627c commit 13b4936

14 files changed

Lines changed: 500 additions & 43 deletions

File tree

popup.html

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6+
<title>TanStack Query DevTools</title>
7+
<link rel="icon" type="image/png" href="/icon-48.png" />
8+
</head>
9+
<body class="overflow-hidden w-[450px] h-[600px] m-0 p-0">
10+
<div id="root"></div>
11+
<script type="module" src="/src/popup/main.tsx"></script>
12+
</body>
13+
</html>

public/manifest.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
"48": "icon-48-gray.png",
4242
"128": "icon-128-gray.png"
4343
},
44-
"default_title": "TanStack Query DevTools"
44+
"default_title": "TanStack Query DevTools",
45+
"default_popup": "popup.html"
4546
}
4647
}

src/background/background.ts

Lines changed: 104 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@ import { z } from "zod";
33
import { StorageManager } from "../shared/storage-manager";
44
import { safeDeserialize } from "../utils/serialization";
55
import type { QueryState } from "../types/storage";
6-
import type { UpdateMessage, QueryActionResult } from "../types/messages";
6+
import type {
7+
UpdateMessage,
8+
QueryActionResult,
9+
RequestImmediateUpdateMessage,
10+
} from "../types/messages";
711

812
// Zod schema for serialized payload validation
913
const SerializedPayloadSchema = z.object({
@@ -12,18 +16,18 @@ const SerializedPayloadSchema = z.object({
1216
isSerializedPayload: z.literal(true),
1317
});
1418

19+
// Track preservation flags per tab
20+
const preserveArtificialStatesForTab = new Map<number, boolean>();
21+
1522
// Define possible background messages
1623
type BackgroundMessage =
1724
| UpdateMessage
1825
| QueryActionResult
26+
| (RequestImmediateUpdateMessage & { inspectedTabId?: number })
1927
| {
2028
type: "QUERY_ACTION";
2129
inspectedTabId?: number;
2230
[key: string]: unknown;
23-
}
24-
| {
25-
type: "REQUEST_IMMEDIATE_UPDATE";
26-
[key: string]: unknown;
2731
};
2832

2933
// Handle messages from both content scripts and DevTools
@@ -36,7 +40,18 @@ chrome.runtime.onMessage.addListener(
3640
!sender.tab?.id
3741
) {
3842
// Forward DevTools actions to content script of the specified tab
39-
if (message.type === "QUERY_ACTION") {
43+
if (
44+
message.type === "QUERY_ACTION" ||
45+
message.type === "REQUEST_IMMEDIATE_UPDATE"
46+
) {
47+
// Track preservation flag for REQUEST_IMMEDIATE_UPDATE messages
48+
if (
49+
message.type === "REQUEST_IMMEDIATE_UPDATE" &&
50+
message.preserveArtificialStates
51+
) {
52+
preserveArtificialStatesForTab.set(message.inspectedTabId, true);
53+
}
54+
4055
chrome.tabs
4156
.sendMessage(message.inspectedTabId, message)
4257
.then(() => {
@@ -124,9 +139,34 @@ chrome.runtime.onMessage.addListener(
124139
processedPayload.tanStackQueryDetected;
125140

126141
// Clear artificial states when QueryClient is freshly detected
127-
// This handles page refreshes and navigations
142+
// Only clear if this tab doesn't have preservation flag set
128143
if (processedPayload.tanStackQueryDetected === true) {
129-
updateData.artificialStates = {};
144+
const shouldPreserve = preserveArtificialStatesForTab.get(tabId);
145+
if (!shouldPreserve) {
146+
// Clear artificial states for this tab only on fresh page loads/navigations
147+
StorageManager.getState()
148+
.then((currentState) => {
149+
const artificialStates = {
150+
...(currentState.artificialStates || {}),
151+
};
152+
delete artificialStates[tabId];
153+
return StorageManager.updatePartialState({
154+
...updateData,
155+
artificialStates,
156+
});
157+
})
158+
.then(() => {
159+
sendResponse({ received: true });
160+
})
161+
.catch((error) => {
162+
console.error("Failed to update storage:", error);
163+
sendResponse({ received: false, error: error.message });
164+
});
165+
return true;
166+
} else {
167+
// Clear the preservation flag after using it
168+
preserveArtificialStatesForTab.delete(tabId);
169+
}
130170
}
131171
}
132172

@@ -157,21 +197,26 @@ chrome.runtime.onMessage.addListener(
157197
};
158198
const queryHash = message.queryHash as string;
159199

200+
// Ensure this tab has an entry in artificial states
201+
if (!artificialStates[tabId]) {
202+
artificialStates[tabId] = {};
203+
}
204+
160205
if (message.action === "TRIGGER_LOADING") {
161-
if (artificialStates[queryHash] === "loading") {
206+
if (artificialStates[tabId][queryHash] === "loading") {
162207
// Cancel loading state
163-
delete artificialStates[queryHash];
208+
delete artificialStates[tabId][queryHash];
164209
} else {
165210
// Start loading state
166-
artificialStates[queryHash] = "loading";
211+
artificialStates[tabId][queryHash] = "loading";
167212
}
168213
} else if (message.action === "TRIGGER_ERROR") {
169-
if (artificialStates[queryHash] === "error") {
214+
if (artificialStates[tabId][queryHash] === "error") {
170215
// Cancel error state
171-
delete artificialStates[queryHash];
216+
delete artificialStates[tabId][queryHash];
172217
} else {
173218
// Start error state
174-
artificialStates[queryHash] = "error";
219+
artificialStates[tabId][queryHash] = "error";
175220
}
176221
}
177222

@@ -258,7 +303,24 @@ StorageManager.onStateChange(async () => {
258303

259304
// Listen to tab activation (when user switches tabs)
260305
chrome.tabs.onActivated.addListener(async (activeInfo) => {
306+
// First update icon based on current storage (may be stale)
261307
await updateIconForActiveTab(activeInfo.tabId);
308+
309+
// Set preservation flag and request immediate update from the newly active tab
310+
preserveArtificialStatesForTab.set(activeInfo.tabId, true);
311+
try {
312+
await chrome.tabs.sendMessage(activeInfo.tabId, {
313+
type: "REQUEST_IMMEDIATE_UPDATE",
314+
preserveArtificialStates: true,
315+
});
316+
} catch (error) {
317+
// Tab might not have the content script loaded, that's fine
318+
console.warn(
319+
"Could not request immediate update for tab",
320+
activeInfo.tabId,
321+
error,
322+
);
323+
}
262324
});
263325

264326
// Listen to window focus changes (when user switches between browser windows)
@@ -267,7 +329,24 @@ chrome.windows.onFocusChanged.addListener(async (windowId) => {
267329
try {
268330
const tabs = await chrome.tabs.query({ active: true, windowId });
269331
if (tabs[0]?.id) {
332+
// First update icon based on current storage (may be stale)
270333
await updateIconForActiveTab(tabs[0].id);
334+
335+
// Set preservation flag and request immediate update from the newly focused tab
336+
preserveArtificialStatesForTab.set(tabs[0].id, true);
337+
try {
338+
await chrome.tabs.sendMessage(tabs[0].id, {
339+
type: "REQUEST_IMMEDIATE_UPDATE",
340+
preserveArtificialStates: true,
341+
});
342+
} catch (error) {
343+
// Tab might not have the content script loaded, that's fine
344+
console.warn(
345+
"Could not request immediate update for focused tab",
346+
tabs[0].id,
347+
error,
348+
);
349+
}
271350
}
272351
} catch (error) {
273352
console.warn("Failed to handle window focus change:", error);
@@ -286,6 +365,14 @@ updateIconForActiveTab().catch((error) => {
286365
chrome.tabs.onRemoved.addListener(async (tabId) => {
287366
try {
288367
const currentState = await StorageManager.getState();
368+
369+
// Clean up artificial states for closed tab
370+
if (currentState.artificialStates && currentState.artificialStates[tabId]) {
371+
const artificialStates = { ...currentState.artificialStates };
372+
delete artificialStates[tabId];
373+
await StorageManager.updatePartialState({ artificialStates });
374+
}
375+
289376
if (currentState.tabId === tabId) {
290377
// Clear state if it was from the closed tab
291378
await StorageManager.setState({
@@ -295,6 +382,9 @@ chrome.tabs.onRemoved.addListener(async (tabId) => {
295382
lastUpdated: Date.now(),
296383
});
297384
}
385+
386+
// Clean up preservation flag for closed tab
387+
preserveArtificialStatesForTab.delete(tabId);
298388
} catch (error) {
299389
console.error("Failed to clean up state for closed tab:", error);
300390
}

src/hooks/useConnection.ts

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import "webextension-polyfill";
22

3-
import { useState, useEffect, useCallback } from "react";
3+
import { useState, useEffect } from "react";
44
import type { QueryData, MutationData } from "../types/query";
5-
import { StateSync } from "../shared/state-sync";
5+
import { stateSync } from "../shared/state-sync";
66

77
interface UseConnectionReturn {
88
// State
@@ -15,6 +15,22 @@ interface UseConnectionReturn {
1515
sendMessage: (message: unknown) => void;
1616
}
1717

18+
// Send message function - now sends through background to content script
19+
const sendMessage = (message: unknown) => {
20+
try {
21+
// Add inspected tab ID for proper routing
22+
const messageWithTab = {
23+
...(message as Record<string, unknown>),
24+
inspectedTabId: chrome.devtools.inspectedWindow.tabId,
25+
};
26+
27+
chrome.runtime.sendMessage(messageWithTab);
28+
} catch (error) {
29+
console.error("Failed to send message:", error);
30+
throw error;
31+
}
32+
};
33+
1834
export const useConnection = (): UseConnectionReturn => {
1935
const [tanStackQueryDetected, setTanStackQueryDetected] = useState<
2036
boolean | null
@@ -26,25 +42,6 @@ export const useConnection = (): UseConnectionReturn => {
2642
Map<string, "loading" | "error">
2743
>(new Map());
2844

29-
// State sync instance - DevTools only listens to storage, not messages
30-
const [stateSync] = useState(() => new StateSync(false));
31-
32-
// Send message function - now sends through background to content script
33-
const sendMessage = useCallback((message: unknown) => {
34-
try {
35-
// Add inspected tab ID for proper routing
36-
const messageWithTab = {
37-
...(message as Record<string, unknown>),
38-
inspectedTabId: chrome.devtools.inspectedWindow.tabId,
39-
};
40-
41-
chrome.runtime.sendMessage(messageWithTab);
42-
} catch (error) {
43-
console.error("Failed to send message:", error);
44-
throw error;
45-
}
46-
}, []);
47-
4845
// Subscribe to storage changes
4946
useEffect(() => {
5047
const unsubscribe = stateSync.subscribe((state) => {
@@ -61,10 +58,11 @@ export const useConnection = (): UseConnectionReturn => {
6158
setQueries(state.queries);
6259
setMutations(state.mutations);
6360

64-
// Update artificial states from storage
65-
if (state.artificialStates) {
61+
// Update artificial states from storage - extract only current tab's states
62+
if (state.artificialStates?.[currentTabId]) {
63+
const currentTabArtificialStates = state.artificialStates[currentTabId];
6664
const artificialStatesMap = new Map(
67-
Object.entries(state.artificialStates),
65+
Object.entries(currentTabArtificialStates),
6866
);
6967
setArtificialStates(artificialStatesMap);
7068
} else {
@@ -81,7 +79,7 @@ export const useConnection = (): UseConnectionReturn => {
8179
return () => {
8280
unsubscribe();
8381
};
84-
}, [stateSync]);
82+
}, []);
8583

8684
return {
8785
// State

0 commit comments

Comments
 (0)