Skip to content

Commit ace86f1

Browse files
committed
fix: auto-open client panel when navigating from Manage Permissions
After OAuth approval, clicking "Manage Permissions" now sets a pendingClientId in the store. When ClientsPage mounts and loads data, it detects the pending ID and auto-opens that client's detail panel so the user can configure permissions immediately. Also adds e2e tests for the ConfirmDialog component verifying that delete prompts appear, cancel dismisses without action, and overlay click dismisses. Signed-off-by: mcpmux <mcpmux@users.noreply.github.com> Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent 813f142 commit ace86f1

7 files changed

Lines changed: 155 additions & 4 deletions

File tree

apps/desktop/src/components/OAuthConsentModal.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { listen, emit } from '@tauri-apps/api/event';
1616
import { Check, X, AlertCircle, Loader2, Globe, Lock } from 'lucide-react';
1717
import { Button, Card, CardHeader, CardTitle, CardDescription, CardContent } from '@mcpmux/ui';
1818
import { listSpaces, type Space } from '@/lib/api/spaces';
19-
import { useNavigateTo } from '@/stores';
19+
import { useNavigateTo, useSetPendingClientId } from '@/stores';
2020
import { resolveKnownClientKey } from '@/lib/clientIcons';
2121
import cursorIcon from '@/assets/client-icons/cursor.svg';
2222
import vscodeIcon from '@/assets/client-icons/vscode.png';
@@ -74,7 +74,7 @@ type ModalState =
7474
| { type: 'loading'; requestId: string }
7575
| { type: 'error'; requestId: string; error: ConsentError }
7676
| { type: 'consent'; details: ConsentRequestDetails }
77-
| { type: 'approved'; clientName: string };
77+
| { type: 'approved'; clientName: string; clientId: string };
7878

7979
/** Open a URL using the backend open command (handles custom protocols like cursor://) */
8080
async function openRedirectUrl(url: string): Promise<void> {
@@ -124,6 +124,7 @@ export function OAuthConsentModal() {
124124
/** 2-second cooldown before the Approve button becomes active */
125125
const [approveReady, setApproveReady] = useState(false);
126126
const navigateTo = useNavigateTo();
127+
const setPendingClientId = useSetPendingClientId();
127128

128129
// Load spaces when modal opens
129130
useEffect(() => {
@@ -199,7 +200,7 @@ export function OAuthConsentModal() {
199200
if (response.success && response.redirect_url) {
200201
console.log('[OAuth] Approved, redirecting to:', response.redirect_url);
201202
await openRedirectUrl(response.redirect_url);
202-
setModalState({ type: 'approved', clientName: clientAlias || details.clientName });
203+
setModalState({ type: 'approved', clientName: clientAlias || details.clientName, clientId: details.clientId });
203204
} else {
204205
setProcessError(response.error || 'Failed to approve consent');
205206
}
@@ -328,6 +329,7 @@ export function OAuthConsentModal() {
328329
variant="primary"
329330
className="flex-1 whitespace-nowrap"
330331
onClick={() => {
332+
setPendingClientId(modalState.clientId);
331333
handleDismiss();
332334
navigateTo('clients');
333335
// Emit event after a short delay so ClientsPage has time to mount

apps/desktop/src/features/clients/ClientsPage.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import type { OAuthClient, UpdateClientRequest } from '@/lib/api/gateway';
3838
import { listOAuthClients, updateOAuthClient, deleteOAuthClient } from '@/lib/api/gateway';
3939
import type { Space } from '@/lib/api/spaces';
4040
import { listSpaces } from '@/lib/api/spaces';
41-
import { useViewSpace } from '@/stores';
41+
import { useViewSpace, usePendingClientId, useSetPendingClientId } from '@/stores';
4242
import type { FeatureSet } from '@/lib/api/featureSets';
4343
import { listFeatureSetsBySpace } from '@/lib/api/featureSets';
4444
import {
@@ -122,6 +122,8 @@ export default function ClientsPage() {
122122

123123
const { toasts, success, error: showError, info, dismiss } = useToast();
124124
const { confirm, ConfirmDialogElement } = useConfirm();
125+
const pendingClientId = usePendingClientId();
126+
const setPendingClientId = useSetPendingClientId();
125127

126128
// Edit state
127129
const [editAlias, setEditAlias] = useState('');
@@ -267,6 +269,16 @@ export default function ClientsPage() {
267269
loadData();
268270
}, []);
269271

272+
// Auto-open a client panel when navigated from "Manage Permissions"
273+
useEffect(() => {
274+
if (!pendingClientId || isLoading) return;
275+
const client = oauthClients.find(c => c.client_id === pendingClientId);
276+
if (client) {
277+
openPanel(client);
278+
setPendingClientId(null);
279+
}
280+
}, [pendingClientId, isLoading, oauthClients]);
281+
270282
useEffect(() => {
271283
setActiveSpace(viewSpace);
272284
}, [viewSpace?.id]);

apps/desktop/src/stores/appStore.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const initialState: AppState = {
88
activeSpaceId: null,
99
viewSpaceId: null,
1010
activeNav: 'home',
11+
pendingClientId: null,
1112
sidebarCollapsed: false,
1213
theme: 'system',
1314
analyticsEnabled: true,
@@ -92,6 +93,11 @@ export const useAppStore = create<AppStore>()(
9293
state.activeNav = nav;
9394
}),
9495

96+
setPendingClientId: (id) =>
97+
set((state) => {
98+
state.pendingClientId = id;
99+
}),
100+
95101
// UI
96102
toggleSidebar: () =>
97103
set((state) => {

apps/desktop/src/stores/selectors.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ export const useActiveSpaceId = () => useAppStore((state) => state.activeSpaceId
77
export const useViewSpaceId = () => useAppStore((state) => state.viewSpaceId);
88
export const useActiveNav = () => useAppStore((state) => state.activeNav);
99
export const useNavigateTo = () => useAppStore((state) => state.navigateTo);
10+
export const usePendingClientId = () => useAppStore((state) => state.pendingClientId);
11+
export const useSetPendingClientId = () => useAppStore((state) => state.setPendingClientId);
1012
export const useTheme = () => useAppStore((state) => state.theme);
1113
export const useSidebarCollapsed = () => useAppStore((state) => state.sidebarCollapsed);
1214
export const useAnalyticsEnabled = () => useAppStore((state) => state.analyticsEnabled);

apps/desktop/src/stores/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ export interface AppState {
1010

1111
// Navigation
1212
activeNav: NavItem;
13+
/** Client ID to auto-select when navigating to Clients page */
14+
pendingClientId: string | null;
1315

1416
// UI state
1517
sidebarCollapsed: boolean;
@@ -34,6 +36,7 @@ export interface AppActions {
3436

3537
// Navigation
3638
navigateTo: (nav: NavItem) => void;
39+
setPendingClientId: (id: string | null) => void;
3740

3841
// UI
3942
toggleSidebar: () => void;
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import { test, expect } from '@playwright/test';
2+
import { DashboardPage, SpacesPage, ClientsPage } from '../pages';
3+
4+
// Helper to click Spaces in sidebar (avoids space switcher button)
5+
async function goToSpaces(page: import('@playwright/test').Page) {
6+
await page.locator('nav button:has-text("Spaces")').last().click();
7+
}
8+
9+
test.describe('ConfirmDialog – Spaces', () => {
10+
test('should show confirm dialog when clicking delete on a non-default space', async ({ page }) => {
11+
const dashboard = new DashboardPage(page);
12+
await dashboard.navigate();
13+
await goToSpaces(page);
14+
await expect(page.locator('h1:has-text("Workspaces")')).toBeVisible();
15+
16+
// Look for a delete button
17+
const deleteBtn = page.locator('[data-testid^="delete-space-"]').first();
18+
if (await deleteBtn.isVisible().catch(() => false)) {
19+
await deleteBtn.click();
20+
21+
// Confirm dialog should appear
22+
await expect(page.getByTestId('confirm-dialog')).toBeVisible();
23+
await expect(page.getByTestId('confirm-dialog-confirm')).toBeVisible();
24+
await expect(page.getByTestId('confirm-dialog-cancel')).toBeVisible();
25+
26+
// Title should mention delete
27+
await expect(page.getByTestId('confirm-dialog').locator('h3')).toContainText(/[Dd]elete/);
28+
}
29+
});
30+
31+
test('should dismiss confirm dialog on cancel without deleting', async ({ page }) => {
32+
const dashboard = new DashboardPage(page);
33+
await dashboard.navigate();
34+
await goToSpaces(page);
35+
await expect(page.locator('h1:has-text("Workspaces")')).toBeVisible();
36+
37+
const deleteBtn = page.locator('[data-testid^="delete-space-"]').first();
38+
if (await deleteBtn.isVisible().catch(() => false)) {
39+
// Count spaces before
40+
const spaceBefore = await page.locator('[data-testid^="space-card-"]').count();
41+
42+
await deleteBtn.click();
43+
await expect(page.getByTestId('confirm-dialog')).toBeVisible();
44+
45+
// Click cancel
46+
await page.getByTestId('confirm-dialog-cancel').click();
47+
48+
// Dialog should close
49+
await expect(page.getByTestId('confirm-dialog')).not.toBeVisible();
50+
51+
// Space count should be the same (nothing was deleted)
52+
const spaceAfter = await page.locator('[data-testid^="space-card-"]').count();
53+
expect(spaceAfter).toBe(spaceBefore);
54+
}
55+
});
56+
57+
test('should dismiss confirm dialog when clicking overlay', async ({ page }) => {
58+
const dashboard = new DashboardPage(page);
59+
await dashboard.navigate();
60+
await goToSpaces(page);
61+
await expect(page.locator('h1:has-text("Workspaces")')).toBeVisible();
62+
63+
const deleteBtn = page.locator('[data-testid^="delete-space-"]').first();
64+
if (await deleteBtn.isVisible().catch(() => false)) {
65+
await deleteBtn.click();
66+
await expect(page.getByTestId('confirm-dialog')).toBeVisible();
67+
68+
// Click overlay (outside the dialog)
69+
await page.getByTestId('confirm-dialog-overlay').click({ position: { x: 5, y: 5 } });
70+
71+
// Dialog should close
72+
await expect(page.getByTestId('confirm-dialog')).not.toBeVisible();
73+
}
74+
});
75+
});
76+
77+
test.describe('ConfirmDialog – Clients', () => {
78+
test('should show confirm dialog when clicking Remove Client', async ({ page }) => {
79+
const dashboard = new DashboardPage(page);
80+
await dashboard.navigate();
81+
await page.locator('nav button:has-text("Clients")').click();
82+
await expect(page.getByRole('heading', { name: 'Connected Clients' })).toBeVisible();
83+
84+
// Click the first client card to open the detail panel
85+
const clientCards = page.locator('[data-testid^="client-card-"]');
86+
const count = await clientCards.count();
87+
88+
if (count > 0) {
89+
await clientCards.first().click();
90+
91+
// Wait for panel to open
92+
await page.waitForTimeout(300);
93+
94+
// Find the Remove Client button in the panel
95+
const removeBtn = page.getByRole('button', { name: /Remove Client/i });
96+
if (await removeBtn.isVisible().catch(() => false)) {
97+
await removeBtn.click();
98+
99+
// Confirm dialog should appear
100+
await expect(page.getByTestId('confirm-dialog')).toBeVisible();
101+
await expect(page.getByTestId('confirm-dialog-confirm')).toHaveText(/Remove/i);
102+
103+
// Cancel should dismiss without removing
104+
await page.getByTestId('confirm-dialog-cancel').click();
105+
await expect(page.getByTestId('confirm-dialog')).not.toBeVisible();
106+
107+
// Client should still be there
108+
const countAfter = await clientCards.count();
109+
expect(countAfter).toBe(count);
110+
}
111+
}
112+
});
113+
});

tests/ts/stores/appStore.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ describe('appStore', () => {
1010
activeSpaceId: null,
1111
viewSpaceId: null,
1212
activeNav: 'home',
13+
pendingClientId: null,
1314
sidebarCollapsed: false,
1415
theme: 'system',
1516
loading: { spaces: false, servers: false },
@@ -299,6 +300,18 @@ describe('appStore', () => {
299300
});
300301
});
301302

303+
describe('setPendingClientId', () => {
304+
it('should set and clear pending client id', () => {
305+
expect(useAppStore.getState().pendingClientId).toBeNull();
306+
307+
useAppStore.getState().setPendingClientId('client-123');
308+
expect(useAppStore.getState().pendingClientId).toBe('client-123');
309+
310+
useAppStore.getState().setPendingClientId(null);
311+
expect(useAppStore.getState().pendingClientId).toBeNull();
312+
});
313+
});
314+
302315
describe('setLoading', () => {
303316
it('should set spaces loading state', () => {
304317
useAppStore.getState().setLoading('spaces', true);

0 commit comments

Comments
 (0)