Skip to content
This repository was archived by the owner on Jun 26, 2026. It is now read-only.

Commit 213d4fc

Browse files
matejvasekclaude
andcommitted
fix: refresh function list on GitHub user switch
Switching GitHub users via the Avatar button left the function list stale. Two root causes: 1. useFunctionListPage had no dependency that changed on PAT switch (isConnectedToForge stayed true), so the useEffect never re-ran. 2. GithubService kept a cached repo list from the previous user, which bled into the new user's results through the "unfetched" union logic. Add a connectionId counter to ForgeConnectionContext that increments on every connectToForge call. The list page uses it to trigger a re-fetch and clear stale function items. Clear the repo and commit SHA caches in GithubService when the token changes. Also fix the PAT modal retaining the previous token on reopen: clear input on successful connect, and reset both input and error on cancel/dismiss via a handleClose callback. Signed-off-by: Matej Vašek <matejvasek@gmail.com> Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 5ba901c commit 213d4fc

7 files changed

Lines changed: 90 additions & 19 deletions

File tree

docs/claude-progress.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
11
# Claude Progress Log
22
# Newest entries first. Agents: append your entry at the top after the header.
33

4+
---
5+
## 2026-05-13 | Session: Fix function list not updating on GitHub user switch
6+
Worked on: Bug fix for PAT switching via Avatar button not refreshing the function list
7+
Completed:
8+
- Root cause: useEffect in useFunctionListPage had no dependency that changed on PAT switch (isConnectedToForge stayed true), and GithubService kept stale repo cache from previous user
9+
- Added connectionId counter to ForgeConnectionContext, incremented on every connectToForge call
10+
- useFunctionListPage uses connectionId as useEffect dependency and render-time state reset trigger
11+
- GithubService clears #cachedFunctionRepos and #lastCommitSha when token changes in the #octokit getter
12+
- Reworked usePatModal per PR review: replaced wasOpen render-time tracking with explicit cleanup at two exit points (setPat('') after successful connect, handleClose callback on cancel/dismiss)
13+
- Clear stale functionItems on user switch so previous user's data doesn't linger during re-fetch
14+
- Added tests for PAT modal cleanup (cleared on connect, cleared on cancel)
15+
- 13 suites, 112 tests, all passing, zero lint errors
16+
Left off: Branch chng-gh-user, PR #24 open. Reviewer requested dropping connectionId in favor of user.name, but keeping connectionId because it handles same-user different-PAT scenario.
17+
Blockers: None
18+
419
---
520
## 2026-04-30 | Session: Set KUBECONFIG GitHub secret for CI/CD deployment
621
Worked on: Encrypt and set KUBECONFIG secret on GitHub repos so GH Actions can deploy to the cluster

src/components/CreateFunctionForm.test.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const testUser: ForgeUser = { name: 'testuser' };
88
const forgeContext = {
99
isActive: true,
1010
user: testUser,
11+
connectionId: 0,
1112
connectToForge: vi.fn(),
1213
};
1314

src/components/UserAvatar.test.tsx

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const testUser = { name: 'twoGiants' };
2020

2121
function renderWithContext(
2222
ui: ReactNode,
23-
contextValue = { isActive: false, user: testUser, connectToForge: vi.fn() },
23+
contextValue = { isActive: false, user: testUser, connectionId: 0, connectToForge: vi.fn() },
2424
) {
2525
return render(
2626
<ForgeConnectionContext.Provider value={contextValue}>{ui}</ForgeConnectionContext.Provider>,
@@ -120,6 +120,7 @@ describe('UserAvatar', () => {
120120
renderWithContext(<UserAvatar enableReconnect />, {
121121
isActive: false,
122122
user: testUser,
123+
connectionId: 0,
123124
connectToForge,
124125
});
125126

@@ -160,6 +161,48 @@ describe('UserAvatar', () => {
160161
expect(screen.queryByText('Personal Access Token')).not.toBeInTheDocument();
161162
});
162163

164+
it('clears PAT input after successful connect', async () => {
165+
const user = userEvent.setup();
166+
const connectToForge = vi.fn();
167+
mockFetchUserInfo.mockResolvedValue(testUser);
168+
169+
renderWithContext(<UserAvatar enableReconnect />, {
170+
isActive: false,
171+
user: testUser,
172+
connectionId: 0,
173+
connectToForge,
174+
});
175+
176+
await user.type(screen.getByLabelText('Personal Access Token'), 'ghp_valid');
177+
await user.click(screen.getByRole('button', { name: 'Connect' }));
178+
179+
await waitFor(() => {
180+
expect(screen.getByText('twoGiants')).toBeInTheDocument();
181+
});
182+
183+
await user.click(screen.getByRole('button', { name: 'twoGiants' }));
184+
185+
expect(screen.getByLabelText('Personal Access Token')).toHaveValue('');
186+
});
187+
188+
it('clears PAT input and error on cancel', async () => {
189+
const user = userEvent.setup();
190+
mockFetchUserInfo.mockRejectedValue(new Error('Bad credentials'));
191+
192+
renderWithContext(<UserAvatar enableReconnect />);
193+
194+
await user.type(screen.getByLabelText('Personal Access Token'), 'ghp_bad');
195+
await user.click(screen.getByRole('button', { name: 'Connect' }));
196+
197+
expect(await screen.findByText('Bad credentials')).toBeInTheDocument();
198+
199+
await user.click(screen.getByRole('button', { name: 'Cancel' }));
200+
await user.click(screen.getByRole('button', { name: 'Connect to GitHub' }));
201+
202+
expect(screen.getByLabelText('Personal Access Token')).toHaveValue('');
203+
expect(screen.queryByText('Bad credentials')).not.toBeInTheDocument();
204+
});
205+
163206
it('disables Cancel button while validating', async () => {
164207
const user = userEvent.setup();
165208
let resolveConnect: () => void;

src/components/UserAvatar.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,13 @@ interface PatModalProps {
9090

9191
function PatModal({ isOpen, onClose, onConnect }: PatModalProps) {
9292
const { t } = useTranslation('plugin__console-functions-plugin');
93-
const { pat, isValidating, error, setPat, handleConnect } = usePatModal(onConnect);
93+
const { pat, isValidating, error, setPat, handleConnect, handleClose } = usePatModal(
94+
onClose,
95+
onConnect,
96+
);
9497

9598
return (
96-
<Modal isOpen={isOpen} onClose={isValidating ? undefined : onClose} variant="small">
99+
<Modal isOpen={isOpen} onClose={isValidating ? undefined : handleClose} variant="small">
97100
<ModalHeader title={t('Connect to GitHub')} />
98101
<ModalBody>
99102
{error && (
@@ -120,15 +123,15 @@ function PatModal({ isOpen, onClose, onConnect }: PatModalProps) {
120123
>
121124
{t('Connect')}
122125
</Button>
123-
<Button variant="link" onClick={onClose} isDisabled={isValidating}>
126+
<Button variant="link" onClick={handleClose} isDisabled={isValidating}>
124127
{t('Cancel')}
125128
</Button>
126129
</ModalFooter>
127130
</Modal>
128131
);
129132
}
130133

131-
function usePatModal(onConnect: (pat: string) => Promise<void>) {
134+
function usePatModal(onClose: () => void, onConnect: (pat: string) => Promise<void>) {
132135
const [pat, setPat] = useState('');
133136
const [isValidating, setIsValidating] = useState(false);
134137
const [error, setError] = useState<string | null>(null);
@@ -138,12 +141,19 @@ function usePatModal(onConnect: (pat: string) => Promise<void>) {
138141
setError(null);
139142
try {
140143
await onConnect(pat);
144+
setPat('');
141145
} catch (err) {
142146
setError(errorMessage(err));
143147
} finally {
144148
setIsValidating(false);
145149
}
146150
};
147151

148-
return { pat, isValidating, error, setPat, handleConnect };
152+
const handleClose = () => {
153+
setPat('');
154+
setError(null);
155+
onClose();
156+
};
157+
158+
return { pat, isValidating, error, setPat, handleConnect, handleClose };
149159
}

src/context/ForgeConnectionProvider.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,30 @@ import { ForgeUser, PAT_KEY, USER_KEY } from '../services/types';
44
interface ForgeConnection {
55
isActive: boolean;
66
user: ForgeUser;
7+
connectionId: number;
78
connectToForge: (user: ForgeUser) => void;
89
}
910

1011
export const ForgeConnectionContext = createContext<ForgeConnection>({
1112
isActive: false,
1213
user: { name: '' },
14+
connectionId: 0,
1315
connectToForge: () => {},
1416
});
1517

1618
export function ForgeConnectionProvider({ children }: Readonly<{ children: ReactNode }>) {
1719
const [isActive, setIsActive] = useState<boolean>(() => !!sessionStorage.getItem(PAT_KEY));
1820
const [user, setUser] = useState<ForgeUser>(readStoredUser);
21+
const [connectionId, setConnectionId] = useState(0);
1922

2023
const connectToForge = (forgeUser: ForgeUser) => {
2124
setUser(forgeUser);
2225
setIsActive(true);
26+
setConnectionId((id) => id + 1);
2327
};
2428

2529
return (
26-
<ForgeConnectionContext.Provider value={{ isActive, user, connectToForge }}>
30+
<ForgeConnectionContext.Provider value={{ isActive, user, connectionId, connectToForge }}>
2731
{children}
2832
</ForgeConnectionContext.Provider>
2933
);

src/services/source-control/GithubService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ export class GithubService implements SourceControlService {
1818
if (token !== this.#cachedToken) {
1919
this.#cachedToken = token;
2020
this.#cachedOctokit = new Octokit({ auth: token });
21+
this.#cachedFunctionRepos = [];
22+
this.#lastCommitSha.clear();
2123
}
2224
return this.#cachedOctokit!;
2325
}

src/views/FunctionsListPage.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -92,26 +92,22 @@ function useFunctionListPage(): {
9292
isConnectedToForge: boolean;
9393
error: string;
9494
} {
95-
const isConnectedToForge = useContext(ForgeConnectionContext).isActive;
95+
const { isActive: isConnectedToForge, connectionId } = useContext(ForgeConnectionContext);
9696
const sourceControl = useSourceControlService();
9797
const navigate = useNavigate();
9898

9999
const [functionItems, setFunctionItems] = useState<FunctionTableItem[]>([]);
100100
const [reposLoaded, setReposLoaded] = useState(!isConnectedToForge);
101-
const [wasConnectedToForge, setWasConnectedToForge] = useState(isConnectedToForge);
101+
const [prevConnectionId, setPrevConnectionId] = useState(connectionId);
102102

103103
const [error, setError] = useState<string>('');
104104

105-
// Reset state when authentication status changes (render-time adjustment)
106-
if (isConnectedToForge !== wasConnectedToForge) {
107-
setWasConnectedToForge(isConnectedToForge);
105+
// Reset state when connection changes (initial connect or user switch)
106+
if (connectionId !== prevConnectionId) {
107+
setPrevConnectionId(connectionId);
108+
setFunctionItems([]);
108109
setError('');
109-
if (isConnectedToForge) {
110-
setReposLoaded(false);
111-
} else {
112-
setFunctionItems([]);
113-
setReposLoaded(true);
114-
}
110+
setReposLoaded(false);
115111
}
116112

117113
useEffect(() => {
@@ -149,7 +145,7 @@ function useFunctionListPage(): {
149145
return () => {
150146
ignore = true;
151147
};
152-
}, [sourceControl, isConnectedToForge]);
148+
}, [sourceControl, isConnectedToForge, connectionId]);
153149

154150
const functionNames = useMemo(() => functionItems.map((item) => item.name), [functionItems]);
155151

0 commit comments

Comments
 (0)