PageSourceSearch

https://smrtbld.ai/js/pages/settings/integrations.js

js smrtbld.ai collected 2026-10-05 15:09:13 UTC 4,505 bytes, 119 lines download raw bytes

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.