1/** 2 * Settings - Integrations Tab 3 * Connect / disconnect Microsoft 365 and Google accounts. 4 * 5 * Reuses the existing auth singletons (graph/auth.js, google/auth.js) which own 6 * the MSAL / GIS popup flows AND persist the connection to the backend. This tab 7 * just reflects status and triggers connect/disconnect. 8 */ 9 10import { connectedServicesApi } from '../../api/connected-services.js'; 11import { getGoogleConfig } from '../../google/config.js'; 12import { msGraph } from '../../graph/auth.js'; 13import { googleAuth } from '../../google/auth.js'; 14import { afterRender, runWithBusyButton } from '../../utils/dom.js'; 15import { auth } from '../../auth.js'; 16 17let clickHandler = null; 18let contentEl = null; 19 20const state = { 21 microsoft: { key: 'microsoft', name: 'Microsoft 365', icon: 'mail', available: false, connected: false, email: '' }, 22 google: { key: 'google', name: 'Google', icon: 'mail', available: false, connected: false, email: '' }, 23}; 24 25export async function init() { 26 contentEl = document.getElementById('settings-content'); 27 contentEl.innerHTML = $.templates('#tpl-tab-integrations').render({}); 28 afterRender(contentEl); 29 30 // Availability â Microsoft rides on the login company; Google self-fetches its config. 31 state.microsoft.available = !!auth.getCompany()?.msClientId; 32 state.google.available = !!(await getGoogleConfig().catch(() => null)); 33 34 // Persisted connection status (best-effort â the backend records the linked account). 35 const [ms, g] = await Promise.allSettled([ 36 connectedServicesApi.getMicrosoft(), 37 connectedServicesApi.getGoogle(), 38 ]); 39 applyStatus('microsoft', ms.status === 'fulfilled' ? ms.value : null); 40 applyStatus('google', g.status === 'fulfilled' ? g.value : null); 41 42 renderProvider('microsoft'); 43 renderProvider('google'); 44 45 clickHandler = onClick; 46 contentEl.addEventListener('click', clickHandler); 47} 48 49function applyStatus(key, info) { 50 state[key].connected = !!info?.principalName; 51 state[key].email = info?.principalName || ''; 52} 53 54function renderProvider(key) { 55 const host = document.getElementById(`integration-${key}`); 56 if (!host) return; 57 host.innerHTML = $.templates('#tpl-integration-provider').render(state[key]); 58 afterRender(host); 59} 60 61function onClick(e) { 62 const btn = e.target.closest('[data-action]'); 63 if (!btn) return; 64 const provider = btn.dataset.provider; 65 const action = btn.dataset.action; 66 if (!provider || !state[provider]) return; 67 68 if (action === 'connect') connect(provider, btn); 69 else if (action === 'disconnect') disconnect(provider, btn); 70} 71 72async function connect(provider, btn) { 73 await runWithBusyButton(btn, 'Connecting...', async () => { 74 const user = (provider === 'microsoft') ? await msGraph.signIn() : await googleAuth.signIn(); 75 if (!user) return; // user cancelled the popup â re-render restores the button 76 77 await refreshStatus(provider); 78 window.showToast?.('Connected', 'success', { description: `${state[provider].name} account connected` }); 79 }, { 80 errorTitle: 'Connection failed', 81 errorLogPrefix: `[Integrations] ${provider} connect failed:`, 82 resetOnSuccess: false, 83 }); 84 renderProvider(provider); // reflect new state (and replace the busy button) 85} 86 87async function disconnect(provider, btn) { 88 await runWithBusyButton(btn, 'Disconnecting...', async () => { 89 if (provider === 'microsoft') await msGraph.signOut(); 90 else await googleAuth.signOut(); 91 92 state[provider].connected = false; 93 state[provider].email = ''; 94 window.showToast?.('Disconnected', 'success', { description: `${state[provider].name} account disconnected` }); 95 }, { 96 errorTitle: 'Disconnect failed', 97 errorLogPrefix: `[Integrations] ${provider} disconnect failed:`, 98 resetOnSuccess: false, 99 }); 100 renderProvider(provider); 101} 102 103async function refreshStatus(provider) { 104 try { 105 const info = (provider === 'microsoft') 106 ? await connectedServicesApi.getMicrosoft() 107 : await connectedServicesApi.getGoogle(); 108 applyStatus(provider, info); 109 } catch { 110 // We just signed in â assume connected even if the status refetch hiccups. 111 state[provider].connected = true; 112 } 113} 114 115export function destroy() { 116 if (clickHandler && contentEl) contentEl.removeEventListener('click', clickHandler); 117 clickHandler = null; 118 contentEl = null; 119}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.