From 238dd6ae1603910d13777ac8b9620558c1590a3d Mon Sep 17 00:00:00 2001 From: Ida Date: Fri, 17 Apr 2026 13:54:00 +0200 Subject: [PATCH] bugfix(CON-01, CON-02) --- src/hooks/useConnections.ts | 27 ++++++++++++++++++++++--- src/pages/basedata/ConnectionsPage.tsx | 28 +++++++++++++++++--------- 2 files changed, 43 insertions(+), 12 deletions(-) diff --git a/src/hooks/useConnections.ts b/src/hooks/useConnections.ts index d5ea099..563f7a1 100644 --- a/src/hooks/useConnections.ts +++ b/src/hooks/useConnections.ts @@ -333,6 +333,8 @@ export function useConnections() { // Create Google connection and open OAuth popup const createGoogleConnectionAndAuth = async (): Promise => { + if (isConnecting) return; + setIsConnecting(true); try { // Step 1: Create a Google connection const newConnection = await createConnection({ @@ -354,7 +356,7 @@ export function useConnections() { authUrl = `${apiBaseUrl}${authUrl}`; } - return new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { const popup = window.open( authUrl, 'google-connection', @@ -362,6 +364,7 @@ export function useConnections() { ); if (!popup) { + setIsConnecting(false); reject(new Error('Popup was blocked. Please allow popups and try again.')); return; } @@ -371,6 +374,7 @@ export function useConnections() { if (popup.closed) { clearInterval(checkClosed); window.removeEventListener('message', messageListener); + setIsConnecting(false); console.log('Google OAuth popup closed'); // Refresh connections in case it succeeded fetchConnections(); @@ -390,6 +394,7 @@ export function useConnections() { clearInterval(checkClosed); window.removeEventListener('message', messageListener); popup.close(); + setIsConnecting(false); console.log('Google connection successful'); // Refresh connections fetchConnections(); @@ -398,6 +403,7 @@ export function useConnections() { clearInterval(checkClosed); window.removeEventListener('message', messageListener); popup.close(); + setIsConnecting(false); reject(new Error(event.data.error || 'Google connection failed')); } }; @@ -405,6 +411,7 @@ export function useConnections() { window.addEventListener('message', messageListener); }); } catch (error) { + setIsConnecting(false); console.error('Error creating Google connection:', error); throw error; } @@ -412,6 +419,8 @@ export function useConnections() { // Create ClickUp connection and open OAuth popup const createClickupConnectionAndAuth = async (): Promise => { + if (isConnecting) return; + setIsConnecting(true); try { const newConnection = await createConnection({ type: 'clickup', @@ -430,7 +439,7 @@ export function useConnections() { authUrl = `${apiBaseUrl}${authUrl}`; } - return new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { const popup = window.open( authUrl, 'clickup-connection', @@ -438,6 +447,7 @@ export function useConnections() { ); if (!popup) { + setIsConnecting(false); reject(new Error('Popup was blocked. Please allow popups and try again.')); return; } @@ -446,6 +456,7 @@ export function useConnections() { if (popup.closed) { clearInterval(checkClosed); window.removeEventListener('message', messageListener); + setIsConnecting(false); console.log('ClickUp OAuth popup closed'); fetchConnections(); resolve(); @@ -462,6 +473,7 @@ export function useConnections() { clearInterval(checkClosed); window.removeEventListener('message', messageListener); popup.close(); + setIsConnecting(false); console.log('ClickUp connection successful'); fetchConnections(); resolve(); @@ -469,6 +481,7 @@ export function useConnections() { clearInterval(checkClosed); window.removeEventListener('message', messageListener); popup.close(); + setIsConnecting(false); reject(new Error(event.data.error || 'ClickUp connection failed')); } }; @@ -476,6 +489,7 @@ export function useConnections() { window.addEventListener('message', messageListener); }); } catch (error) { + setIsConnecting(false); console.error('Error creating ClickUp connection:', error); throw error; } @@ -483,6 +497,8 @@ export function useConnections() { // Create Microsoft connection and open OAuth popup const createMicrosoftConnectionAndAuth = async (): Promise => { + if (isConnecting) return; + setIsConnecting(true); try { // Step 1: Create a Microsoft connection const newConnection = await createConnection({ @@ -504,7 +520,7 @@ export function useConnections() { authUrl = `${apiBaseUrl}${authUrl}`; } - return new Promise((resolve, reject) => { + return await new Promise((resolve, reject) => { const popup = window.open( authUrl, 'msft-connection', @@ -512,6 +528,7 @@ export function useConnections() { ); if (!popup) { + setIsConnecting(false); reject(new Error('Popup was blocked. Please allow popups and try again.')); return; } @@ -521,6 +538,7 @@ export function useConnections() { if (popup.closed) { clearInterval(checkClosed); window.removeEventListener('message', messageListener); + setIsConnecting(false); console.log('Microsoft OAuth popup closed'); // Refresh connections in case it succeeded fetchConnections(); @@ -540,6 +558,7 @@ export function useConnections() { clearInterval(checkClosed); window.removeEventListener('message', messageListener); popup.close(); + setIsConnecting(false); console.log('Microsoft connection successful'); // Refresh connections fetchConnections(); @@ -548,6 +567,7 @@ export function useConnections() { clearInterval(checkClosed); window.removeEventListener('message', messageListener); popup.close(); + setIsConnecting(false); reject(new Error(event.data.error || 'Microsoft connection failed')); } }; @@ -555,6 +575,7 @@ export function useConnections() { window.addEventListener('message', messageListener); }); } catch (error) { + setIsConnecting(false); console.error('Error creating Microsoft connection:', error); throw error; } diff --git a/src/pages/basedata/ConnectionsPage.tsx b/src/pages/basedata/ConnectionsPage.tsx index b76aef8..a8967bc 100644 --- a/src/pages/basedata/ConnectionsPage.tsx +++ b/src/pages/basedata/ConnectionsPage.tsx @@ -97,12 +97,15 @@ export const ConnectionsPage: React.FC = () => { // Handle edit submit const handleEditSubmit = async (data: Partial) => { if (!editingConnection) return; - // Note: updateConnection is handled through the hook try { - // Ensure authority is properly typed - filter and validate authority value const updateData: Partial = { ...data }; - - // Validate and set authority if present + + // Strip computed/read-only fields the backend cannot write. + delete (updateData as any).connectionReference; + delete (updateData as any).displayLabel; + delete (updateData as any).tokenStatus; + delete (updateData as any).tokenExpiresAt; + if (data.authority) { if ( data.authority === 'local' || @@ -112,7 +115,6 @@ export const ConnectionsPage: React.FC = () => { ) { updateData.authority = data.authority; } else { - // Remove invalid authority value delete (updateData as any).authority; } } @@ -173,8 +175,10 @@ export const ConnectionsPage: React.FC = () => { } }; - // Handle create Google connection + // Guards prevent double-trigger while the OAuth popup is open, which would + // otherwise create additional orphan PENDING connections on every click. const handleCreateGoogle = async () => { + if (isConnecting) return; try { await createGoogleConnectionAndAuth(); refetch(); @@ -183,8 +187,8 @@ export const ConnectionsPage: React.FC = () => { } }; - // Handle create Microsoft connection const handleCreateMicrosoft = async () => { + if (isConnecting) return; try { await createMicrosoftConnectionAndAuth(); refetch(); @@ -194,6 +198,7 @@ export const ConnectionsPage: React.FC = () => { }; const handleCreateClickup = async () => { + if (isConnecting) return; try { await createClickupConnectionAndAuth(); refetch(); @@ -222,7 +227,12 @@ export const ConnectionsPage: React.FC = () => { // Form attributes for edit modal const formAttributes = useMemo(() => { - const excludedFields = ['id', 'mandateId', 'userId', 'sysCreatedBy', 'sysCreatedAt', 'sysModifiedAt', 'connectedAt', 'lastChecked']; + const excludedFields = [ + 'id', 'mandateId', 'userId', 'sysCreatedBy', 'sysCreatedAt', 'sysModifiedAt', + 'connectedAt', 'lastChecked', + // computed/read-only fields the backend rejects on write + 'connectionReference', 'displayLabel', 'tokenStatus', 'tokenExpiresAt', + ]; return (attributes || []) .filter(attr => !excludedFields.includes(attr.name)); }, [attributes]); @@ -255,7 +265,7 @@ export const ConnectionsPage: React.FC = () => { className={styles.secondaryButton} onClick={handleAdminConsent} disabled={adminConsentPending} - title={t('Microsoft Admin-Zustimmung erteilt der')} + title={t('Microsoft Admin-Zustimmung für die gesamte Organisation erteilen')} > {t('Admin-Zustimmung')}