@devicai/ui 0.38.0 → 0.39.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +40 -4
  2. package/dist/cjs/components/ChatDrawer/ChatDrawer.js +39 -12
  3. package/dist/cjs/components/ChatDrawer/ChatDrawer.js.map +1 -1
  4. package/dist/cjs/components/CoreMemoryModal/CoreMemoryModal.js +6 -2
  5. package/dist/cjs/components/CoreMemoryModal/CoreMemoryModal.js.map +1 -1
  6. package/dist/cjs/components/IntegrationsModal/IntegrationLogo.js +23 -0
  7. package/dist/cjs/components/IntegrationsModal/IntegrationLogo.js.map +1 -0
  8. package/dist/cjs/components/IntegrationsModal/IntegrationsLauncher.js +68 -0
  9. package/dist/cjs/components/IntegrationsModal/IntegrationsLauncher.js.map +1 -0
  10. package/dist/cjs/components/IntegrationsModal/IntegrationsModal.js +78 -78
  11. package/dist/cjs/components/IntegrationsModal/IntegrationsModal.js.map +1 -1
  12. package/dist/cjs/components/IntegrationsModal/useIntegrations.js +106 -0
  13. package/dist/cjs/components/IntegrationsModal/useIntegrations.js.map +1 -0
  14. package/dist/cjs/components/theme.js +73 -0
  15. package/dist/cjs/components/theme.js.map +1 -0
  16. package/dist/cjs/index.js +9 -0
  17. package/dist/cjs/index.js.map +1 -1
  18. package/dist/cjs/styles.css +1 -1
  19. package/dist/esm/components/ChatDrawer/ChatDrawer.js +39 -12
  20. package/dist/esm/components/ChatDrawer/ChatDrawer.js.map +1 -1
  21. package/dist/esm/components/ChatDrawer/ChatDrawer.types.d.ts +15 -7
  22. package/dist/esm/components/CoreMemoryModal/CoreMemoryModal.d.ts +8 -1
  23. package/dist/esm/components/CoreMemoryModal/CoreMemoryModal.js +6 -2
  24. package/dist/esm/components/CoreMemoryModal/CoreMemoryModal.js.map +1 -1
  25. package/dist/esm/components/IntegrationsModal/IntegrationLogo.d.ts +16 -0
  26. package/dist/esm/components/IntegrationsModal/IntegrationLogo.js +21 -0
  27. package/dist/esm/components/IntegrationsModal/IntegrationLogo.js.map +1 -0
  28. package/dist/esm/components/IntegrationsModal/IntegrationsLauncher.d.ts +31 -0
  29. package/dist/esm/components/IntegrationsModal/IntegrationsLauncher.js +65 -0
  30. package/dist/esm/components/IntegrationsModal/IntegrationsLauncher.js.map +1 -0
  31. package/dist/esm/components/IntegrationsModal/IntegrationsModal.d.ts +17 -1
  32. package/dist/esm/components/IntegrationsModal/IntegrationsModal.js +77 -77
  33. package/dist/esm/components/IntegrationsModal/IntegrationsModal.js.map +1 -1
  34. package/dist/esm/components/IntegrationsModal/index.d.ts +6 -0
  35. package/dist/esm/components/IntegrationsModal/useIntegrations.d.ts +49 -0
  36. package/dist/esm/components/IntegrationsModal/useIntegrations.js +104 -0
  37. package/dist/esm/components/IntegrationsModal/useIntegrations.js.map +1 -0
  38. package/dist/esm/components/theme.d.ts +42 -0
  39. package/dist/esm/components/theme.js +70 -0
  40. package/dist/esm/components/theme.js.map +1 -0
  41. package/dist/esm/index.d.ts +4 -2
  42. package/dist/esm/index.js +4 -0
  43. package/dist/esm/index.js.map +1 -1
  44. package/dist/esm/styles.css +1 -1
  45. package/package.json +1 -1
@@ -0,0 +1,65 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { useMemo, useRef, useState, useEffect } from 'react';
3
+ import { IntegrationLogo } from './IntegrationLogo.js';
4
+
5
+ /** How many logos fit before the rest are counted instead. */
6
+ const DEFAULT_MAX_LOGOS = 6;
7
+ /** Connected first: with more apps than fit, those are the ones worth showing. */
8
+ function order(integrations) {
9
+ return [...integrations].sort((a, b) => {
10
+ if (a.connected !== b.connected)
11
+ return a.connected ? -1 : 1;
12
+ return 0;
13
+ });
14
+ }
15
+ /**
16
+ * Logos a row this wide can afford.
17
+ *
18
+ * The stack shares the drawer header with a title, the conversation picker and
19
+ * two more buttons, and a 400px drawer is the common case. Six logos there push
20
+ * the picker down to a stub — so on a narrow header the count drops and the
21
+ * `+N` box absorbs the difference, which is what it is for.
22
+ */
23
+ function logosThatFit(hostWidth, max) {
24
+ if (!hostWidth)
25
+ return max;
26
+ if (hostWidth >= 520)
27
+ return max;
28
+ if (hostWidth >= 460)
29
+ return Math.min(max, 5);
30
+ return Math.min(max, 4);
31
+ }
32
+ /**
33
+ * The header control that opens the connected-apps modal, drawn as the real
34
+ * logos of the apps on offer.
35
+ *
36
+ * It renders nothing until the server has confirmed the assistant offers apps
37
+ * to its tenants. A button that opens an empty dialog is worse than no button:
38
+ * it promises the end user something the assistant was never configured to
39
+ * give them.
40
+ */
41
+ function IntegrationsLauncher({ state, onClick, label = "Connected apps", maxLogos = DEFAULT_MAX_LOGOS, dark = false, className = "", }) {
42
+ const sorted = useMemo(() => order(state.integrations), [state.integrations]);
43
+ const ref = useRef(null);
44
+ const [fit, setFit] = useState(maxLogos);
45
+ useEffect(() => {
46
+ const host = ref.current?.closest(".devic-drawer-header") ??
47
+ ref.current?.parentElement;
48
+ if (!host || typeof ResizeObserver === "undefined")
49
+ return;
50
+ const measure = () => setFit(logosThatFit(host.getBoundingClientRect().width, maxLogos));
51
+ measure();
52
+ const observer = new ResizeObserver(measure);
53
+ observer.observe(host);
54
+ return () => observer.disconnect();
55
+ }, [maxLogos, state.offered]);
56
+ if (!state.offered || sorted.length === 0)
57
+ return null;
58
+ const shown = sorted.slice(0, Math.max(1, fit));
59
+ const extra = sorted.length - shown.length;
60
+ const connected = sorted.filter((i) => i.connected).length;
61
+ return (jsxs("button", { type: "button", ref: ref, className: `devic-int-launcher ${className}`.trim(), "data-dark": dark, onClick: onClick, title: label, "aria-label": `${label} (${connected}/${sorted.length} connected)`, children: [shown.map((integration) => (jsx("span", { className: "devic-int-launcher-item", "data-connected": integration.connected, title: integration.name, children: jsx(IntegrationLogo, { integration: integration, className: "devic-int-launcher-logo" }) }, integration.app))), extra > 0 && (jsxs("span", { className: "devic-int-launcher-item devic-int-launcher-more", children: ["+", extra] }))] }));
62
+ }
63
+
64
+ export { DEFAULT_MAX_LOGOS, IntegrationsLauncher };
65
+ //# sourceMappingURL=IntegrationsLauncher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"IntegrationsLauncher.js","sources":["../../../../src/components/IntegrationsModal/IntegrationsLauncher.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState, type JSX } from \"react\";\nimport type { Integration } from \"../../api/types\";\nimport { IntegrationLogo } from \"./IntegrationLogo\";\nimport type { IntegrationsState } from \"./useIntegrations\";\nimport \"./IntegrationsModal.css\";\n\n/** How many logos fit before the rest are counted instead. */\nexport const DEFAULT_MAX_LOGOS = 6;\n\nexport interface IntegrationsLauncherProps {\n /** Shared listing, so this and the modal load the catalogue once. */\n state: IntegrationsState;\n onClick: () => void;\n /** Tooltip and accessible name. @default \"Connected apps\" */\n label?: string;\n /** Logos shown before the `+N` box. @default 6 */\n maxLogos?: number;\n /**\n * Whether the surrounding surface is dark, so the logo chips go light. Many\n * app logos are solid black on transparency and vanish otherwise.\n */\n dark?: boolean;\n className?: string;\n}\n\n/** Connected first: with more apps than fit, those are the ones worth showing. */\nfunction order(integrations: Integration[]): Integration[] {\n return [...integrations].sort((a, b) => {\n if (a.connected !== b.connected) return a.connected ? -1 : 1;\n return 0;\n });\n}\n\n/**\n * Logos a row this wide can afford.\n *\n * The stack shares the drawer header with a title, the conversation picker and\n * two more buttons, and a 400px drawer is the common case. Six logos there push\n * the picker down to a stub — so on a narrow header the count drops and the\n * `+N` box absorbs the difference, which is what it is for.\n */\nfunction logosThatFit(hostWidth: number, max: number): number {\n if (!hostWidth) return max;\n if (hostWidth >= 520) return max;\n if (hostWidth >= 460) return Math.min(max, 5);\n return Math.min(max, 4);\n}\n\n/**\n * The header control that opens the connected-apps modal, drawn as the real\n * logos of the apps on offer.\n *\n * It renders nothing until the server has confirmed the assistant offers apps\n * to its tenants. A button that opens an empty dialog is worse than no button:\n * it promises the end user something the assistant was never configured to\n * give them.\n */\nexport function IntegrationsLauncher({\n state,\n onClick,\n label = \"Connected apps\",\n maxLogos = DEFAULT_MAX_LOGOS,\n dark = false,\n className = \"\",\n}: IntegrationsLauncherProps): JSX.Element | null {\n const sorted = useMemo(() => order(state.integrations), [state.integrations]);\n const ref = useRef<HTMLButtonElement>(null);\n const [fit, setFit] = useState(maxLogos);\n\n useEffect(() => {\n const host =\n ref.current?.closest(\".devic-drawer-header\") ??\n ref.current?.parentElement;\n if (!host || typeof ResizeObserver === \"undefined\") return;\n const measure = () =>\n setFit(logosThatFit(host.getBoundingClientRect().width, maxLogos));\n measure();\n const observer = new ResizeObserver(measure);\n observer.observe(host);\n return () => observer.disconnect();\n }, [maxLogos, state.offered]);\n\n if (!state.offered || sorted.length === 0) return null;\n\n const shown = sorted.slice(0, Math.max(1, fit));\n const extra = sorted.length - shown.length;\n const connected = sorted.filter((i) => i.connected).length;\n\n return (\n <button\n type=\"button\"\n ref={ref}\n className={`devic-int-launcher ${className}`.trim()}\n data-dark={dark}\n onClick={onClick}\n title={label}\n aria-label={`${label} (${connected}/${sorted.length} connected)`}\n >\n {shown.map((integration) => (\n <span\n key={integration.app}\n className=\"devic-int-launcher-item\"\n // Dimmed until connected, so the stack doubles as the status: the\n // end user can see at a glance which of their apps are set up.\n data-connected={integration.connected}\n title={integration.name}\n >\n <IntegrationLogo\n integration={integration}\n className=\"devic-int-launcher-logo\"\n />\n </span>\n ))}\n {extra > 0 && (\n <span className=\"devic-int-launcher-item devic-int-launcher-more\">\n +{extra}\n </span>\n )}\n </button>\n );\n}\n\nexport default IntegrationsLauncher;\n"],"names":["_jsxs","_jsx"],"mappings":"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"}
@@ -1,5 +1,7 @@
1
1
  import { type JSX } from "react";
2
2
  import type { Integration } from "../../api/types";
3
+ import { type DevicTheme } from "../theme";
4
+ import { type IntegrationsState } from "./useIntegrations";
3
5
  import "./IntegrationsModal.css";
4
6
  export interface IntegrationsModalProps {
5
7
  /** Whether the modal is visible. */
@@ -17,8 +19,22 @@ export interface IntegrationsModalProps {
17
19
  baseUrl?: string;
18
20
  /** Modal title. @default "Connected apps" */
19
21
  title?: string;
22
+ /** Search field placeholder. @default "Search connected apps" */
23
+ searchPlaceholder?: string;
20
24
  /** Called after an account is connected or disconnected. */
21
25
  onChange?: (integrations: Integration[]) => void;
26
+ /**
27
+ * Colours and font. Same names as the drawer's style options, and the drawer
28
+ * passes its own down — a dialog opening in the default light palette over a
29
+ * themed application is the one thing this must not do.
30
+ */
31
+ theme?: DevicTheme;
32
+ /**
33
+ * Listing loaded elsewhere (see `useIntegrations`). The drawer already has to
34
+ * load it to decide whether its button exists, and passing it down is what
35
+ * keeps opening the modal from asking for the very same thing again.
36
+ */
37
+ state?: IntegrationsState;
22
38
  }
23
39
  /**
24
40
  * Modal where the END USER of an application manages their *own* third-party
@@ -36,5 +52,5 @@ export interface IntegrationsModalProps {
36
52
  * after the round trip is what gets it blocked. When it is blocked anyway, the
37
53
  * URL is offered as a link instead.
38
54
  */
39
- export declare function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId, apiKey, baseUrl, title, onChange, }: IntegrationsModalProps): JSX.Element | null;
55
+ export declare function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId, apiKey, baseUrl, title, searchPlaceholder, onChange, theme, state, }: IntegrationsModalProps): JSX.Element | null;
40
56
  export default IntegrationsModal;
@@ -1,12 +1,16 @@
1
1
  import { jsx, jsxs } from 'react/jsx-runtime';
2
- import { useMemo, useState, useCallback, useRef, useEffect } from 'react';
2
+ import { useState, useMemo, useRef, useEffect, useCallback } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
- import { DevicApiClient } from '../../api/client.js';
5
- import { useOptionalDevicContext } from '../../provider/DevicContext.js';
4
+ import { isDarkTheme, themeVars } from '../theme.js';
5
+ import { IntegrationLogo } from './IntegrationLogo.js';
6
+ import { useIntegrations } from './useIntegrations.js';
6
7
 
7
8
  function PlugIcon() {
8
9
  return (jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [jsx("path", { d: "M12 22v-5" }), jsx("path", { d: "M9 8V2" }), jsx("path", { d: "M15 8V2" }), jsx("path", { d: "M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z" })] }));
9
10
  }
11
+ function SearchIcon() {
12
+ return (jsxs("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", children: [jsx("circle", { cx: "11", cy: "11", r: "7" }), jsx("path", { d: "m20 20-3.5-3.5" })] }));
13
+ }
10
14
  /** A random value tying an OAuth round trip to the window that started it. */
11
15
  function newNonce() {
12
16
  const c = typeof crypto !== "undefined" ? crypto : undefined;
@@ -31,6 +35,14 @@ function accountLabel(account) {
31
35
  return account.status.toLowerCase();
32
36
  return `connected ${when.toLocaleDateString()}`;
33
37
  }
38
+ function matches(integration, query) {
39
+ const q = query.trim().toLowerCase();
40
+ if (!q)
41
+ return true;
42
+ return (integration.name.toLowerCase().includes(q) ||
43
+ integration.app.toLowerCase().includes(q) ||
44
+ (integration.description ?? "").toLowerCase().includes(q));
45
+ }
34
46
  /**
35
47
  * Modal where the END USER of an application manages their *own* third-party
36
48
  * accounts: the apps the developer offered to tenants of this assistant, each
@@ -47,70 +59,55 @@ function accountLabel(account) {
47
59
  * after the round trip is what gets it blocked. When it is blocked anyway, the
48
60
  * URL is offered as a link instead.
49
61
  */
50
- function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId, apiKey, baseUrl, title = "Connected apps", onChange, }) {
51
- const context = useOptionalDevicContext();
52
- const resolvedApiKey = apiKey || context?.apiKey;
53
- const resolvedBaseUrl = baseUrl || context?.baseUrl || "https://api.devic.ai";
54
- const resolvedTenantId = tenantId || context?.tenantId;
55
- const resolvedSubtenantId = subtenantId || context?.subtenantId;
56
- const client = useMemo(() => resolvedApiKey
57
- ? new DevicApiClient({
58
- apiKey: resolvedApiKey,
59
- baseUrl: resolvedBaseUrl,
60
- })
61
- : null, [resolvedApiKey, resolvedBaseUrl]);
62
- const scopeOptions = useMemo(() => ({
62
+ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId, apiKey, baseUrl, title = "Connected apps", searchPlaceholder = "Search connected apps", onChange, theme, state, }) {
63
+ // Hooks cannot be skipped, so the fallback is always built and only fetches
64
+ // when nobody handed a listing down.
65
+ const own = useIntegrations({
63
66
  assistantId,
64
- tenantId: resolvedTenantId || undefined,
65
- subtenantId: resolvedSubtenantId || undefined,
66
- }), [assistantId, resolvedTenantId, resolvedSubtenantId]);
67
- const [loading, setLoading] = useState(false);
68
- const [integrations, setIntegrations] = useState([]);
69
- const [error, setError] = useState(null);
67
+ tenantId,
68
+ subtenantId,
69
+ apiKey,
70
+ baseUrl,
71
+ enabled: isOpen && !state,
72
+ });
73
+ const { integrations, loading, error: loadError, refresh, client, scope } = state ?? own;
74
+ /** Errors from connecting or disconnecting, kept apart from load failures. */
75
+ const [actionError, setActionError] = useState(null);
76
+ const error = actionError ?? loadError;
70
77
  /** App slug with a connect/disconnect in flight, so only its card is busy. */
71
78
  const [busyApp, setBusyApp] = useState(null);
72
79
  /** Authorization URL surfaced as a link when the popup was blocked. */
73
80
  const [blockedUrl, setBlockedUrl] = useState(null);
74
- /**
75
- * Apps whose logo failed to load. The URL comes from the provider and is
76
- * fetched from whatever host it names, so a broken one is a matter of when,
77
- * not if and a broken-image icon in someone else's product looks like our
78
- * bug.
79
- */
80
- const [brokenLogos, setBrokenLogos] = useState([]);
81
- const refresh = useCallback(async (dropCache = false) => {
82
- if (!client) {
83
- setError("API key not configured");
84
- return;
85
- }
86
- setLoading(true);
87
- setError(null);
88
- try {
89
- // The server caches a scope's connections briefly; after an OAuth round
90
- // trip that cache is exactly one step behind, so drop it first.
91
- if (dropCache) {
92
- await client.refreshIntegrations(scopeOptions).catch(() => undefined);
93
- }
94
- const list = await client.getIntegrations(scopeOptions);
95
- setIntegrations(list);
96
- onChange?.(list);
97
- }
98
- catch (err) {
99
- setError(err instanceof Error ? err.message : String(err));
100
- }
101
- finally {
102
- setLoading(false);
103
- }
104
- }, [client, scopeOptions, onChange]);
105
- // Fetch on every open: accounts may have been connected or revoked elsewhere.
81
+ const [query, setQuery] = useState("");
82
+ const visible = useMemo(() => integrations.filter((i) => matches(i, query)), [integrations, query]);
83
+ // Report the listing without making the caller's identity part of the
84
+ // dependency: an inline arrow would fire this on every render.
85
+ const onChangeRef = useRef(onChange);
86
+ onChangeRef.current = onChange;
87
+ useEffect(() => {
88
+ if (integrations.length)
89
+ onChangeRef.current?.(integrations);
90
+ }, [integrations]);
91
+ // Reopening starts clean, and re-reads: accounts may have been connected or
92
+ // revoked elsewhere since the last look.
106
93
  const wasOpenRef = useRef(false);
94
+ const openedBeforeRef = useRef(false);
107
95
  useEffect(() => {
108
96
  if (isOpen && !wasOpenRef.current) {
109
97
  setBlockedUrl(null);
110
- void refresh();
98
+ setActionError(null);
99
+ setQuery("");
100
+ // The very first open of an uncontrolled modal is already covered by the
101
+ // hook switching on; asking again here would double every first open.
102
+ if (state || openedBeforeRef.current)
103
+ void refresh();
104
+ openedBeforeRef.current = true;
111
105
  }
112
106
  wasOpenRef.current = isOpen;
113
- }, [isOpen, refresh]);
107
+ // `state.refresh` is stable per scope; re-running on every render of the
108
+ // owner is exactly what this must not do.
109
+ // eslint-disable-next-line react-hooks/exhaustive-deps
110
+ }, [isOpen]);
114
111
  // Escape closes
115
112
  useEffect(() => {
116
113
  if (!isOpen)
@@ -165,7 +162,7 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
165
162
  const handleConnect = async (integration) => {
166
163
  if (!client || busyApp)
167
164
  return;
168
- setError(null);
165
+ setActionError(null);
169
166
  setBlockedUrl(null);
170
167
  setBusyApp(integration.app);
171
168
  const nonce = newNonce();
@@ -173,7 +170,7 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
173
170
  // Opened empty inside the click, navigated once the URL is known.
174
171
  const popup = window.open("", "devic-oauth", "width=520,height=680,menubar=no,toolbar=no");
175
172
  try {
176
- const { authorizationUrl } = await client.connectIntegration(integration.app, { ...scopeOptions, returnTo });
173
+ const { authorizationUrl } = await client.connectIntegration(integration.app, { ...scope, returnTo });
177
174
  pendingRef.current = { app: integration.app, returnTo };
178
175
  if (popup && !popup.closed) {
179
176
  popupRef.current = popup;
@@ -196,7 +193,7 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
196
193
  catch (err) {
197
194
  popup?.close();
198
195
  pendingRef.current = null;
199
- setError(err instanceof Error ? err.message : String(err));
196
+ setActionError(err instanceof Error ? err.message : String(err));
200
197
  setBusyApp(null);
201
198
  }
202
199
  };
@@ -204,13 +201,13 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
204
201
  if (!client || busyApp)
205
202
  return;
206
203
  setBusyApp(app);
207
- setError(null);
204
+ setActionError(null);
208
205
  try {
209
- await client.disconnectIntegration(account.id, scopeOptions);
206
+ await client.disconnectIntegration(account.id, scope);
210
207
  await refresh(true);
211
208
  }
212
209
  catch (err) {
213
- setError(err instanceof Error ? err.message : String(err));
210
+ setActionError(err instanceof Error ? err.message : String(err));
214
211
  }
215
212
  finally {
216
213
  setBusyApp(null);
@@ -218,23 +215,26 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
218
215
  };
219
216
  if (!isOpen)
220
217
  return null;
221
- return createPortal(jsx("div", { className: "devic-int-overlay", onClick: onClose, children: jsxs("div", { className: "devic-int-modal", role: "dialog", "aria-modal": "true", "aria-label": title, onClick: (e) => e.stopPropagation(), children: [jsxs("div", { className: "devic-int-header", children: [jsxs("h3", { className: "devic-int-title", children: [jsx(PlugIcon, {}), title] }), jsx("button", { className: "devic-int-close", onClick: onClose, type: "button", "aria-label": "Close", children: "\u00D7" })] }), jsxs("div", { className: "devic-int-body", children: [error && jsx("div", { className: "devic-int-error", children: error }), blockedUrl && (jsxs("div", { className: "devic-int-notice", children: ["Your browser blocked the pop-up.", " ", jsx("a", { href: blockedUrl.url, target: "_blank", rel: "noopener noreferrer", onClick: () => {
218
+ return createPortal(
219
+ // The variables go on the overlay, not on the modal: the backdrop is part
220
+ // of the dialog, and a portal inherits nothing from the drawer that opened
221
+ // it.
222
+ jsx("div", { className: "devic-int-overlay", style: themeVars(theme), "data-dark": isDarkTheme(theme), onClick: onClose, children: jsxs("div", { className: "devic-int-modal", role: "dialog", "aria-modal": "true", "aria-label": title, onClick: (e) => e.stopPropagation(), children: [jsxs("div", { className: "devic-int-header", children: [jsxs("h3", { className: "devic-int-title", children: [jsx(PlugIcon, {}), title] }), jsx("button", { className: "devic-int-close", onClick: onClose, type: "button", "aria-label": "Close", children: "\u00D7" })] }), jsxs("div", { className: "devic-int-search", children: [jsx(SearchIcon, {}), jsx("input", { type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: searchPlaceholder, "aria-label": searchPlaceholder, autoComplete: "off" })] }), jsxs("div", { className: "devic-int-body", children: [error && jsx("div", { className: "devic-int-error", children: error }), blockedUrl && (jsxs("div", { className: "devic-int-notice", children: ["Your browser blocked the pop-up.", " ", jsx("a", { href: blockedUrl.url, target: "_blank", rel: "noopener noreferrer", onClick: () => {
222
223
  pendingRef.current = null;
223
224
  setBlockedUrl(null);
224
- }, children: "Open the authorisation page" }), " ", "and come back \u2014 then use Refresh."] })), loading && integrations.length === 0 ? (jsx("div", { className: "devic-int-loading", children: "Loading apps\u2026" })) : integrations.length === 0 ? (jsx("div", { className: "devic-int-empty", children: "No apps available here yet." })) : (integrations.map((integration) => {
225
- const state = stateOf(integration);
226
- const busy = busyApp === integration.app;
227
- return (jsxs("div", { className: "devic-int-card", "data-state": state.key, children: [jsxs("div", { className: "devic-int-card-head", children: [integration.logo &&
228
- !brokenLogos.includes(integration.app) ? (jsx("img", { className: "devic-int-logo", src: integration.logo, alt: "", "aria-hidden": "true", onError: () => setBrokenLogos((prev) => prev.includes(integration.app)
229
- ? prev
230
- : [...prev, integration.app]) })) : (jsx("span", { className: "devic-int-logo devic-int-logo-fallback", children: integration.name.charAt(0).toUpperCase() })), jsxs("div", { className: "devic-int-card-text", children: [jsx("div", { className: "devic-int-name", children: integration.name }), jsx("div", { className: "devic-int-state", children: state.label })] }), jsx("button", { type: "button", className: `devic-int-btn${state.key === "connected" ? "" : " devic-int-btn-primary"}`, onClick: () => handleConnect(integration), disabled: busy || !!busyApp, children: busy
231
- ? "Waiting…"
232
- : state.key === "disconnected"
233
- ? "Connect"
234
- : state.key === "reconnect"
235
- ? "Reconnect"
236
- : "Add account" })] }), integration.description && (jsx("div", { className: "devic-int-description", children: integration.description })), integration.accounts.length > 0 && (jsx("ul", { className: "devic-int-accounts", children: integration.accounts.map((account) => (jsxs("li", { className: "devic-int-account", children: [jsx("span", { className: "devic-int-dot", "data-ok": !account.needsReconnect, "aria-hidden": "true" }), jsxs("span", { className: "devic-int-account-label", children: [accountLabel(account), account.needsReconnect && (jsxs("span", { className: "devic-int-account-warn", children: [" ", "\u00B7 reconnect required"] }))] }), jsx("button", { type: "button", className: "devic-int-btn devic-int-btn-small devic-int-btn-danger", onClick: () => handleDisconnect(integration.app, account), disabled: !!busyApp, children: "Disconnect" })] }, account.id))) }))] }, integration.app));
237
- }))] }), jsxs("div", { className: "devic-int-footer", children: [jsx("span", { children: "Only you can see and use the accounts you connect here." }), jsx("button", { type: "button", className: "devic-int-btn devic-int-btn-small", onClick: () => void refresh(true), disabled: loading || !!busyApp, children: "Refresh" })] })] }) }), document.body);
225
+ }, children: "Open the authorisation page" }), " ", "and come back \u2014 then use Refresh."] })), loading && integrations.length === 0 ? (jsx("div", { className: "devic-int-loading", children: "Loading apps\u2026" })) : integrations.length === 0 ? (jsx("div", { className: "devic-int-empty", children: "No apps available here yet." })) : visible.length === 0 ? (jsxs("div", { className: "devic-int-empty", children: ["No apps match \u201C", query.trim(), "\u201D."] })) : (jsx("div", { className: "devic-int-grid", children: visible.map((integration) => {
226
+ const cardState = stateOf(integration);
227
+ const busy = busyApp === integration.app;
228
+ return (jsxs("div", { className: "devic-int-card", "data-state": cardState.key, children: [jsxs("div", { className: "devic-int-card-head", children: [jsx(IntegrationLogo, { integration: integration }), jsxs("span", { className: "devic-int-state", children: [jsx("span", { className: "devic-int-dot", "data-ok": cardState.key === "connected", "data-off": cardState.key === "disconnected", "aria-hidden": "true" }), cardState.label] })] }), jsx("div", { className: "devic-int-name", title: integration.name, children: integration.name }), integration.description && (jsx("div", { className: "devic-int-description", title: integration.description, children: integration.description })), jsx("button", { type: "button", className: `devic-int-btn devic-int-btn-block${cardState.key === "connected"
229
+ ? ""
230
+ : " devic-int-btn-primary"}`, onClick: () => handleConnect(integration), disabled: busy || !!busyApp, children: busy
231
+ ? "Waiting…"
232
+ : cardState.key === "disconnected"
233
+ ? "Connect"
234
+ : cardState.key === "reconnect"
235
+ ? "Reconnect"
236
+ : "Add account" }), integration.accounts.length > 0 && (jsx("ul", { className: "devic-int-accounts", children: integration.accounts.map((account) => (jsxs("li", { className: "devic-int-account", children: [jsx("span", { className: "devic-int-dot", "data-ok": !account.needsReconnect, "aria-hidden": "true" }), jsxs("span", { className: "devic-int-account-label", children: [accountLabel(account), account.needsReconnect && (jsxs("span", { className: "devic-int-account-warn", children: [" ", "\u00B7 reconnect required"] }))] }), jsx("button", { type: "button", className: "devic-int-unlink", onClick: () => handleDisconnect(integration.app, account), disabled: !!busyApp, title: "Disconnect this account", "aria-label": `Disconnect ${integration.name}`, children: "\u00D7" })] }, account.id))) }))] }, integration.app));
237
+ }) }))] }), jsxs("div", { className: "devic-int-footer", children: [jsx("span", { children: "Only you can see and use the accounts you connect here." }), jsx("button", { type: "button", className: "devic-int-btn devic-int-btn-small", onClick: () => void refresh(true), disabled: loading || !!busyApp, children: "Refresh" })] })] }) }), document.body);
238
238
  }
239
239
 
240
240
  export { IntegrationsModal };
@@ -1 +1 @@
1
- {"version":3,"file":"IntegrationsModal.js","sources":["../../../../src/components/IntegrationsModal/IntegrationsModal.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type JSX,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport { DevicApiClient } from \"../../api/client\";\nimport type { Integration, IntegrationAccount } from \"../../api/types\";\nimport { useOptionalDevicContext } from \"../../provider\";\nimport \"./IntegrationsModal.css\";\n\n/** Message the OAuth callback page posts back to this window when it is done. */\ninterface CallbackMessage {\n source?: string;\n type?: string;\n app?: string;\n status?: string;\n returnTo?: string;\n}\n\nexport interface IntegrationsModalProps {\n /** Whether the modal is visible. */\n isOpen: boolean;\n onClose: () => void;\n /** Assistant whose offered apps are shown. */\n assistantId: string;\n /** Tenant of the end user (falls back to the provider's tenantId). */\n tenantId?: string;\n /** Subtenant of the end user (falls back to the provider's subtenantId). */\n subtenantId?: string;\n /** API key override (falls back to the provider's). */\n apiKey?: string;\n /** Base URL override (falls back to the provider's). */\n baseUrl?: string;\n /** Modal title. @default \"Connected apps\" */\n title?: string;\n /** Called after an account is connected or disconnected. */\n onChange?: (integrations: Integration[]) => void;\n}\n\nfunction PlugIcon(): JSX.Element {\n return (\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M12 22v-5\" />\n <path d=\"M9 8V2\" />\n <path d=\"M15 8V2\" />\n <path d=\"M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z\" />\n </svg>\n );\n}\n\n/** A random value tying an OAuth round trip to the window that started it. */\nfunction newNonce(): string {\n const c = typeof crypto !== \"undefined\" ? crypto : undefined;\n if (c?.randomUUID) return c.randomUUID();\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\n\n/** How the card describes an app at a glance. */\nfunction stateOf(integration: Integration): {\n key: \"connected\" | \"reconnect\" | \"disconnected\";\n label: string;\n} {\n if (integration.connected) return { key: \"connected\", label: \"Connected\" };\n if (integration.accounts.some((a) => a.needsReconnect)) {\n return { key: \"reconnect\", label: \"Needs reconnection\" };\n }\n return { key: \"disconnected\", label: \"Not connected\" };\n}\n\nfunction accountLabel(account: IntegrationAccount): string {\n if (!account.connectedAt) return account.status.toLowerCase();\n const when = new Date(account.connectedAt);\n if (Number.isNaN(when.getTime())) return account.status.toLowerCase();\n return `connected ${when.toLocaleDateString()}`;\n}\n\n/**\n * Modal where the END USER of an application manages their *own* third-party\n * accounts: the apps the developer offered to tenants of this assistant, each\n * with the accounts this tenant has connected, and the buttons to add or\n * remove one.\n *\n * Backed by `/api/v1/tenant-integrations`, which resolves the tenant\n * server-side, so what is listed here is only ever this tenant's — never the\n * workspace-wide accounts an admin connected, and never another tenant's.\n *\n * Connecting opens the provider's consent screen in a popup. The popup is\n * opened empty *before* the request that produces its URL, because browsers\n * only honour `window.open` inside the gesture that triggered it: opening it\n * after the round trip is what gets it blocked. When it is blocked anyway, the\n * URL is offered as a link instead.\n */\nexport function IntegrationsModal({\n isOpen,\n onClose,\n assistantId,\n tenantId,\n subtenantId,\n apiKey,\n baseUrl,\n title = \"Connected apps\",\n onChange,\n}: IntegrationsModalProps): JSX.Element | null {\n const context = useOptionalDevicContext();\n const resolvedApiKey = apiKey || context?.apiKey;\n const resolvedBaseUrl = baseUrl || context?.baseUrl || \"https://api.devic.ai\";\n const resolvedTenantId = tenantId || context?.tenantId;\n const resolvedSubtenantId = subtenantId || context?.subtenantId;\n\n const client = useMemo(\n () =>\n resolvedApiKey\n ? new DevicApiClient({\n apiKey: resolvedApiKey,\n baseUrl: resolvedBaseUrl,\n })\n : null,\n [resolvedApiKey, resolvedBaseUrl]\n );\n\n const scopeOptions = useMemo(\n () => ({\n assistantId,\n tenantId: resolvedTenantId || undefined,\n subtenantId: resolvedSubtenantId || undefined,\n }),\n [assistantId, resolvedTenantId, resolvedSubtenantId]\n );\n\n const [loading, setLoading] = useState(false);\n const [integrations, setIntegrations] = useState<Integration[]>([]);\n const [error, setError] = useState<string | null>(null);\n /** App slug with a connect/disconnect in flight, so only its card is busy. */\n const [busyApp, setBusyApp] = useState<string | null>(null);\n /** Authorization URL surfaced as a link when the popup was blocked. */\n const [blockedUrl, setBlockedUrl] = useState<{ app: string; url: string } | null>(\n null\n );\n /**\n * Apps whose logo failed to load. The URL comes from the provider and is\n * fetched from whatever host it names, so a broken one is a matter of when,\n * not if — and a broken-image icon in someone else's product looks like our\n * bug.\n */\n const [brokenLogos, setBrokenLogos] = useState<string[]>([]);\n\n const refresh = useCallback(\n async (dropCache = false) => {\n if (!client) {\n setError(\"API key not configured\");\n return;\n }\n setLoading(true);\n setError(null);\n try {\n // The server caches a scope's connections briefly; after an OAuth round\n // trip that cache is exactly one step behind, so drop it first.\n if (dropCache) {\n await client.refreshIntegrations(scopeOptions).catch(() => undefined);\n }\n const list = await client.getIntegrations(scopeOptions);\n setIntegrations(list);\n onChange?.(list);\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setLoading(false);\n }\n },\n [client, scopeOptions, onChange]\n );\n\n // Fetch on every open: accounts may have been connected or revoked elsewhere.\n const wasOpenRef = useRef(false);\n useEffect(() => {\n if (isOpen && !wasOpenRef.current) {\n setBlockedUrl(null);\n void refresh();\n }\n wasOpenRef.current = isOpen;\n }, [isOpen, refresh]);\n\n // Escape closes\n useEffect(() => {\n if (!isOpen) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [isOpen, onClose]);\n\n /** The round trip currently in flight, if any. */\n const pendingRef = useRef<{ app: string; returnTo: string } | null>(null);\n const popupRef = useRef<Window | null>(null);\n const pollRef = useRef<number | null>(null);\n\n const finishConnect = useCallback(() => {\n if (pollRef.current !== null) {\n window.clearInterval(pollRef.current);\n pollRef.current = null;\n }\n pendingRef.current = null;\n popupRef.current = null;\n setBusyApp(null);\n void refresh(true);\n }, [refresh]);\n\n // The callback page tells us it is done. Treat the message as a nudge, never\n // as the result: what is displayed comes from re-reading the server, so a\n // forged message can at worst cause one redundant fetch.\n useEffect(() => {\n if (!isOpen) return;\n const onMessage = (event: MessageEvent) => {\n const data = event.data as CallbackMessage | undefined;\n if (!data || data.source !== \"devic\") return;\n if (data.type !== \"integration-connected\") return;\n const pending = pendingRef.current;\n if (!pending || data.returnTo !== pending.returnTo) return;\n popupRef.current?.close();\n finishConnect();\n };\n window.addEventListener(\"message\", onMessage);\n return () => window.removeEventListener(\"message\", onMessage);\n }, [isOpen, finishConnect]);\n\n // Stop polling if the modal goes away mid-flow.\n useEffect(\n () => () => {\n if (pollRef.current !== null) window.clearInterval(pollRef.current);\n },\n []\n );\n\n const handleConnect = async (integration: Integration) => {\n if (!client || busyApp) return;\n setError(null);\n setBlockedUrl(null);\n setBusyApp(integration.app);\n\n const nonce = newNonce();\n const returnTo = `${window.location.origin}/?devic_oauth=${nonce}`;\n // Opened empty inside the click, navigated once the URL is known.\n const popup = window.open(\n \"\",\n \"devic-oauth\",\n \"width=520,height=680,menubar=no,toolbar=no\"\n );\n\n try {\n const { authorizationUrl } = await client.connectIntegration(\n integration.app,\n { ...scopeOptions, returnTo }\n );\n pendingRef.current = { app: integration.app, returnTo };\n if (popup && !popup.closed) {\n popupRef.current = popup;\n popup.location.href = authorizationUrl;\n // The user may close the popup without the callback ever posting back\n // — a cancelled consent screen, or a provider that lands somewhere\n // else. Watching for the close is what keeps the card from staying\n // busy forever.\n pollRef.current = window.setInterval(() => {\n if (popup.closed) finishConnect();\n }, 700);\n } else {\n // Blocked (Safari, in-app browsers, extensions): hand over the URL.\n setBlockedUrl({ app: integration.app, url: authorizationUrl });\n setBusyApp(null);\n }\n } catch (err) {\n popup?.close();\n pendingRef.current = null;\n setError(err instanceof Error ? err.message : String(err));\n setBusyApp(null);\n }\n };\n\n const handleDisconnect = async (app: string, account: IntegrationAccount) => {\n if (!client || busyApp) return;\n setBusyApp(app);\n setError(null);\n try {\n await client.disconnectIntegration(account.id, scopeOptions);\n await refresh(true);\n } catch (err) {\n setError(err instanceof Error ? err.message : String(err));\n } finally {\n setBusyApp(null);\n }\n };\n\n if (!isOpen) return null;\n\n return createPortal(\n <div className=\"devic-int-overlay\" onClick={onClose}>\n <div\n className=\"devic-int-modal\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"devic-int-header\">\n <h3 className=\"devic-int-title\">\n <PlugIcon />\n {title}\n </h3>\n <button\n className=\"devic-int-close\"\n onClick={onClose}\n type=\"button\"\n aria-label=\"Close\"\n >\n ×\n </button>\n </div>\n\n <div className=\"devic-int-body\">\n {error && <div className=\"devic-int-error\">{error}</div>}\n\n {blockedUrl && (\n <div className=\"devic-int-notice\">\n Your browser blocked the pop-up.{\" \"}\n <a\n href={blockedUrl.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => {\n pendingRef.current = null;\n setBlockedUrl(null);\n }}\n >\n Open the authorisation page\n </a>{\" \"}\n and come back — then use Refresh.\n </div>\n )}\n\n {loading && integrations.length === 0 ? (\n <div className=\"devic-int-loading\">Loading apps…</div>\n ) : integrations.length === 0 ? (\n <div className=\"devic-int-empty\">\n No apps available here yet.\n </div>\n ) : (\n integrations.map((integration) => {\n const state = stateOf(integration);\n const busy = busyApp === integration.app;\n return (\n <div\n key={integration.app}\n className=\"devic-int-card\"\n data-state={state.key}\n >\n <div className=\"devic-int-card-head\">\n {integration.logo &&\n !brokenLogos.includes(integration.app) ? (\n <img\n className=\"devic-int-logo\"\n src={integration.logo}\n alt=\"\"\n aria-hidden=\"true\"\n onError={() =>\n setBrokenLogos((prev) =>\n prev.includes(integration.app)\n ? prev\n : [...prev, integration.app]\n )\n }\n />\n ) : (\n <span className=\"devic-int-logo devic-int-logo-fallback\">\n {integration.name.charAt(0).toUpperCase()}\n </span>\n )}\n <div className=\"devic-int-card-text\">\n <div className=\"devic-int-name\">{integration.name}</div>\n <div className=\"devic-int-state\">{state.label}</div>\n </div>\n <button\n type=\"button\"\n className={`devic-int-btn${\n state.key === \"connected\" ? \"\" : \" devic-int-btn-primary\"\n }`}\n onClick={() => handleConnect(integration)}\n disabled={busy || !!busyApp}\n >\n {busy\n ? \"Waiting…\"\n : state.key === \"disconnected\"\n ? \"Connect\"\n : state.key === \"reconnect\"\n ? \"Reconnect\"\n : \"Add account\"}\n </button>\n </div>\n\n {integration.description && (\n <div className=\"devic-int-description\">\n {integration.description}\n </div>\n )}\n\n {integration.accounts.length > 0 && (\n <ul className=\"devic-int-accounts\">\n {integration.accounts.map((account) => (\n <li key={account.id} className=\"devic-int-account\">\n <span\n className=\"devic-int-dot\"\n data-ok={!account.needsReconnect}\n aria-hidden=\"true\"\n />\n <span className=\"devic-int-account-label\">\n {accountLabel(account)}\n {account.needsReconnect && (\n <span className=\"devic-int-account-warn\">\n {\" \"}\n · reconnect required\n </span>\n )}\n </span>\n <button\n type=\"button\"\n className=\"devic-int-btn devic-int-btn-small devic-int-btn-danger\"\n onClick={() =>\n handleDisconnect(integration.app, account)\n }\n disabled={!!busyApp}\n >\n Disconnect\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n })\n )}\n </div>\n\n <div className=\"devic-int-footer\">\n <span>Only you can see and use the accounts you connect here.</span>\n <button\n type=\"button\"\n className=\"devic-int-btn devic-int-btn-small\"\n onClick={() => void refresh(true)}\n disabled={loading || !!busyApp}\n >\n Refresh\n </button>\n </div>\n </div>\n </div>,\n document.body\n );\n}\n\nexport default IntegrationsModal;\n"],"names":["_jsxs","_jsx"],"mappings":"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"}
1
+ {"version":3,"file":"IntegrationsModal.js","sources":["../../../../src/components/IntegrationsModal/IntegrationsModal.tsx"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n type JSX,\n} from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type { Integration, IntegrationAccount } from \"../../api/types\";\nimport { isDarkTheme, themeVars, type DevicTheme } from \"../theme\";\nimport { IntegrationLogo } from \"./IntegrationLogo\";\nimport { useIntegrations, type IntegrationsState } from \"./useIntegrations\";\nimport \"./IntegrationsModal.css\";\n\n/** Message the OAuth callback page posts back to this window when it is done. */\ninterface CallbackMessage {\n source?: string;\n type?: string;\n app?: string;\n status?: string;\n returnTo?: string;\n}\n\nexport interface IntegrationsModalProps {\n /** Whether the modal is visible. */\n isOpen: boolean;\n onClose: () => void;\n /** Assistant whose offered apps are shown. */\n assistantId: string;\n /** Tenant of the end user (falls back to the provider's tenantId). */\n tenantId?: string;\n /** Subtenant of the end user (falls back to the provider's subtenantId). */\n subtenantId?: string;\n /** API key override (falls back to the provider's). */\n apiKey?: string;\n /** Base URL override (falls back to the provider's). */\n baseUrl?: string;\n /** Modal title. @default \"Connected apps\" */\n title?: string;\n /** Search field placeholder. @default \"Search connected apps\" */\n searchPlaceholder?: string;\n /** Called after an account is connected or disconnected. */\n onChange?: (integrations: Integration[]) => void;\n /**\n * Colours and font. Same names as the drawer's style options, and the drawer\n * passes its own down — a dialog opening in the default light palette over a\n * themed application is the one thing this must not do.\n */\n theme?: DevicTheme;\n /**\n * Listing loaded elsewhere (see `useIntegrations`). The drawer already has to\n * load it to decide whether its button exists, and passing it down is what\n * keeps opening the modal from asking for the very same thing again.\n */\n state?: IntegrationsState;\n}\n\nfunction PlugIcon(): JSX.Element {\n return (\n <svg\n width=\"16\"\n height=\"16\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M12 22v-5\" />\n <path d=\"M9 8V2\" />\n <path d=\"M15 8V2\" />\n <path d=\"M18 8v5a4 4 0 0 1-4 4h-4a4 4 0 0 1-4-4V8Z\" />\n </svg>\n );\n}\n\nfunction SearchIcon(): JSX.Element {\n return (\n <svg\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"11\" cy=\"11\" r=\"7\" />\n <path d=\"m20 20-3.5-3.5\" />\n </svg>\n );\n}\n\n/** A random value tying an OAuth round trip to the window that started it. */\nfunction newNonce(): string {\n const c = typeof crypto !== \"undefined\" ? crypto : undefined;\n if (c?.randomUUID) return c.randomUUID();\n return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2)}`;\n}\n\n/** How the card describes an app at a glance. */\nfunction stateOf(integration: Integration): {\n key: \"connected\" | \"reconnect\" | \"disconnected\";\n label: string;\n} {\n if (integration.connected) return { key: \"connected\", label: \"Connected\" };\n if (integration.accounts.some((a) => a.needsReconnect)) {\n return { key: \"reconnect\", label: \"Needs reconnection\" };\n }\n return { key: \"disconnected\", label: \"Not connected\" };\n}\n\nfunction accountLabel(account: IntegrationAccount): string {\n if (!account.connectedAt) return account.status.toLowerCase();\n const when = new Date(account.connectedAt);\n if (Number.isNaN(when.getTime())) return account.status.toLowerCase();\n return `connected ${when.toLocaleDateString()}`;\n}\n\nfunction matches(integration: Integration, query: string): boolean {\n const q = query.trim().toLowerCase();\n if (!q) return true;\n return (\n integration.name.toLowerCase().includes(q) ||\n integration.app.toLowerCase().includes(q) ||\n (integration.description ?? \"\").toLowerCase().includes(q)\n );\n}\n\n/**\n * Modal where the END USER of an application manages their *own* third-party\n * accounts: the apps the developer offered to tenants of this assistant, each\n * with the accounts this tenant has connected, and the buttons to add or\n * remove one.\n *\n * Backed by `/api/v1/tenant-integrations`, which resolves the tenant\n * server-side, so what is listed here is only ever this tenant's — never the\n * workspace-wide accounts an admin connected, and never another tenant's.\n *\n * Connecting opens the provider's consent screen in a popup. The popup is\n * opened empty *before* the request that produces its URL, because browsers\n * only honour `window.open` inside the gesture that triggered it: opening it\n * after the round trip is what gets it blocked. When it is blocked anyway, the\n * URL is offered as a link instead.\n */\nexport function IntegrationsModal({\n isOpen,\n onClose,\n assistantId,\n tenantId,\n subtenantId,\n apiKey,\n baseUrl,\n title = \"Connected apps\",\n searchPlaceholder = \"Search connected apps\",\n onChange,\n theme,\n state,\n}: IntegrationsModalProps): JSX.Element | null {\n // Hooks cannot be skipped, so the fallback is always built and only fetches\n // when nobody handed a listing down.\n const own = useIntegrations({\n assistantId,\n tenantId,\n subtenantId,\n apiKey,\n baseUrl,\n enabled: isOpen && !state,\n });\n const { integrations, loading, error: loadError, refresh, client, scope } =\n state ?? own;\n\n /** Errors from connecting or disconnecting, kept apart from load failures. */\n const [actionError, setActionError] = useState<string | null>(null);\n const error = actionError ?? loadError;\n /** App slug with a connect/disconnect in flight, so only its card is busy. */\n const [busyApp, setBusyApp] = useState<string | null>(null);\n /** Authorization URL surfaced as a link when the popup was blocked. */\n const [blockedUrl, setBlockedUrl] = useState<{ app: string; url: string } | null>(\n null\n );\n const [query, setQuery] = useState(\"\");\n\n const visible = useMemo(\n () => integrations.filter((i) => matches(i, query)),\n [integrations, query]\n );\n\n // Report the listing without making the caller's identity part of the\n // dependency: an inline arrow would fire this on every render.\n const onChangeRef = useRef(onChange);\n onChangeRef.current = onChange;\n useEffect(() => {\n if (integrations.length) onChangeRef.current?.(integrations);\n }, [integrations]);\n\n // Reopening starts clean, and re-reads: accounts may have been connected or\n // revoked elsewhere since the last look.\n const wasOpenRef = useRef(false);\n const openedBeforeRef = useRef(false);\n useEffect(() => {\n if (isOpen && !wasOpenRef.current) {\n setBlockedUrl(null);\n setActionError(null);\n setQuery(\"\");\n // The very first open of an uncontrolled modal is already covered by the\n // hook switching on; asking again here would double every first open.\n if (state || openedBeforeRef.current) void refresh();\n openedBeforeRef.current = true;\n }\n wasOpenRef.current = isOpen;\n // `state.refresh` is stable per scope; re-running on every render of the\n // owner is exactly what this must not do.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [isOpen]);\n\n // Escape closes\n useEffect(() => {\n if (!isOpen) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key === \"Escape\") onClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [isOpen, onClose]);\n\n /** The round trip currently in flight, if any. */\n const pendingRef = useRef<{ app: string; returnTo: string } | null>(null);\n const popupRef = useRef<Window | null>(null);\n const pollRef = useRef<number | null>(null);\n\n const finishConnect = useCallback(() => {\n if (pollRef.current !== null) {\n window.clearInterval(pollRef.current);\n pollRef.current = null;\n }\n pendingRef.current = null;\n popupRef.current = null;\n setBusyApp(null);\n void refresh(true);\n }, [refresh]);\n\n // The callback page tells us it is done. Treat the message as a nudge, never\n // as the result: what is displayed comes from re-reading the server, so a\n // forged message can at worst cause one redundant fetch.\n useEffect(() => {\n if (!isOpen) return;\n const onMessage = (event: MessageEvent) => {\n const data = event.data as CallbackMessage | undefined;\n if (!data || data.source !== \"devic\") return;\n if (data.type !== \"integration-connected\") return;\n const pending = pendingRef.current;\n if (!pending || data.returnTo !== pending.returnTo) return;\n popupRef.current?.close();\n finishConnect();\n };\n window.addEventListener(\"message\", onMessage);\n return () => window.removeEventListener(\"message\", onMessage);\n }, [isOpen, finishConnect]);\n\n // Stop polling if the modal goes away mid-flow.\n useEffect(\n () => () => {\n if (pollRef.current !== null) window.clearInterval(pollRef.current);\n },\n []\n );\n\n const handleConnect = async (integration: Integration) => {\n if (!client || busyApp) return;\n setActionError(null);\n setBlockedUrl(null);\n setBusyApp(integration.app);\n\n const nonce = newNonce();\n const returnTo = `${window.location.origin}/?devic_oauth=${nonce}`;\n // Opened empty inside the click, navigated once the URL is known.\n const popup = window.open(\n \"\",\n \"devic-oauth\",\n \"width=520,height=680,menubar=no,toolbar=no\"\n );\n\n try {\n const { authorizationUrl } = await client.connectIntegration(\n integration.app,\n { ...scope, returnTo }\n );\n pendingRef.current = { app: integration.app, returnTo };\n if (popup && !popup.closed) {\n popupRef.current = popup;\n popup.location.href = authorizationUrl;\n // The user may close the popup without the callback ever posting back\n // — a cancelled consent screen, or a provider that lands somewhere\n // else. Watching for the close is what keeps the card from staying\n // busy forever.\n pollRef.current = window.setInterval(() => {\n if (popup.closed) finishConnect();\n }, 700);\n } else {\n // Blocked (Safari, in-app browsers, extensions): hand over the URL.\n setBlockedUrl({ app: integration.app, url: authorizationUrl });\n setBusyApp(null);\n }\n } catch (err) {\n popup?.close();\n pendingRef.current = null;\n setActionError(err instanceof Error ? err.message : String(err));\n setBusyApp(null);\n }\n };\n\n const handleDisconnect = async (app: string, account: IntegrationAccount) => {\n if (!client || busyApp) return;\n setBusyApp(app);\n setActionError(null);\n try {\n await client.disconnectIntegration(account.id, scope);\n await refresh(true);\n } catch (err) {\n setActionError(err instanceof Error ? err.message : String(err));\n } finally {\n setBusyApp(null);\n }\n };\n\n if (!isOpen) return null;\n\n return createPortal(\n // The variables go on the overlay, not on the modal: the backdrop is part\n // of the dialog, and a portal inherits nothing from the drawer that opened\n // it.\n <div\n className=\"devic-int-overlay\"\n style={themeVars(theme)}\n data-dark={isDarkTheme(theme)}\n onClick={onClose}\n >\n <div\n className=\"devic-int-modal\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={title}\n onClick={(e) => e.stopPropagation()}\n >\n <div className=\"devic-int-header\">\n <h3 className=\"devic-int-title\">\n <PlugIcon />\n {title}\n </h3>\n <button\n className=\"devic-int-close\"\n onClick={onClose}\n type=\"button\"\n aria-label=\"Close\"\n >\n ×\n </button>\n </div>\n\n <div className=\"devic-int-search\">\n <SearchIcon />\n <input\n type=\"search\"\n value={query}\n onChange={(e) => setQuery(e.target.value)}\n placeholder={searchPlaceholder}\n aria-label={searchPlaceholder}\n autoComplete=\"off\"\n />\n </div>\n\n <div className=\"devic-int-body\">\n {error && <div className=\"devic-int-error\">{error}</div>}\n\n {blockedUrl && (\n <div className=\"devic-int-notice\">\n Your browser blocked the pop-up.{\" \"}\n <a\n href={blockedUrl.url}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n onClick={() => {\n pendingRef.current = null;\n setBlockedUrl(null);\n }}\n >\n Open the authorisation page\n </a>{\" \"}\n and come back — then use Refresh.\n </div>\n )}\n\n {loading && integrations.length === 0 ? (\n <div className=\"devic-int-loading\">Loading apps…</div>\n ) : integrations.length === 0 ? (\n <div className=\"devic-int-empty\">No apps available here yet.</div>\n ) : visible.length === 0 ? (\n <div className=\"devic-int-empty\">\n No apps match “{query.trim()}”.\n </div>\n ) : (\n <div className=\"devic-int-grid\">\n {visible.map((integration) => {\n const cardState = stateOf(integration);\n const busy = busyApp === integration.app;\n return (\n <div\n key={integration.app}\n className=\"devic-int-card\"\n data-state={cardState.key}\n >\n <div className=\"devic-int-card-head\">\n <IntegrationLogo integration={integration} />\n <span className=\"devic-int-state\">\n <span\n className=\"devic-int-dot\"\n data-ok={cardState.key === \"connected\"}\n data-off={cardState.key === \"disconnected\"}\n aria-hidden=\"true\"\n />\n {cardState.label}\n </span>\n </div>\n\n <div className=\"devic-int-name\" title={integration.name}>\n {integration.name}\n </div>\n\n {integration.description && (\n <div\n className=\"devic-int-description\"\n title={integration.description}\n >\n {integration.description}\n </div>\n )}\n\n <button\n type=\"button\"\n className={`devic-int-btn devic-int-btn-block${\n cardState.key === \"connected\"\n ? \"\"\n : \" devic-int-btn-primary\"\n }`}\n onClick={() => handleConnect(integration)}\n disabled={busy || !!busyApp}\n >\n {busy\n ? \"Waiting…\"\n : cardState.key === \"disconnected\"\n ? \"Connect\"\n : cardState.key === \"reconnect\"\n ? \"Reconnect\"\n : \"Add account\"}\n </button>\n\n {integration.accounts.length > 0 && (\n <ul className=\"devic-int-accounts\">\n {integration.accounts.map((account) => (\n <li key={account.id} className=\"devic-int-account\">\n <span\n className=\"devic-int-dot\"\n data-ok={!account.needsReconnect}\n aria-hidden=\"true\"\n />\n <span className=\"devic-int-account-label\">\n {accountLabel(account)}\n {account.needsReconnect && (\n <span className=\"devic-int-account-warn\">\n {\" \"}\n · reconnect required\n </span>\n )}\n </span>\n <button\n type=\"button\"\n className=\"devic-int-unlink\"\n onClick={() =>\n handleDisconnect(integration.app, account)\n }\n disabled={!!busyApp}\n title=\"Disconnect this account\"\n aria-label={`Disconnect ${integration.name}`}\n >\n ×\n </button>\n </li>\n ))}\n </ul>\n )}\n </div>\n );\n })}\n </div>\n )}\n </div>\n\n <div className=\"devic-int-footer\">\n <span>Only you can see and use the accounts you connect here.</span>\n <button\n type=\"button\"\n className=\"devic-int-btn devic-int-btn-small\"\n onClick={() => void refresh(true)}\n disabled={loading || !!busyApp}\n >\n Refresh\n </button>\n </div>\n </div>\n </div>,\n document.body\n );\n}\n\nexport default IntegrationsModal;\n"],"names":["_jsxs","_jsx"],"mappings":"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"}
@@ -1,2 +1,8 @@
1
1
  export { IntegrationsModal, default } from "./IntegrationsModal";
2
2
  export type { IntegrationsModalProps } from "./IntegrationsModal";
3
+ export { IntegrationsLauncher, DEFAULT_MAX_LOGOS } from "./IntegrationsLauncher";
4
+ export type { IntegrationsLauncherProps } from "./IntegrationsLauncher";
5
+ export { IntegrationLogo } from "./IntegrationLogo";
6
+ export type { IntegrationLogoProps } from "./IntegrationLogo";
7
+ export { useIntegrations } from "./useIntegrations";
8
+ export type { IntegrationsState, IntegrationsScope, UseIntegrationsOptions, } from "./useIntegrations";
@@ -0,0 +1,49 @@
1
+ import { DevicApiClient } from "../../api/client";
2
+ import type { Integration } from "../../api/types";
3
+ /** What identifies the end user in front of the widget. */
4
+ export interface IntegrationsScope {
5
+ assistantId: string;
6
+ tenantId?: string;
7
+ subtenantId?: string;
8
+ }
9
+ export interface UseIntegrationsOptions extends IntegrationsScope {
10
+ /** API key override (falls back to the provider's). */
11
+ apiKey?: string;
12
+ /** Base URL override (falls back to the provider's). */
13
+ baseUrl?: string;
14
+ /**
15
+ * Fetch when this turns true, and only then. The launcher passes the drawer's
16
+ * open state so a widget nobody opens costs no request at all.
17
+ */
18
+ enabled?: boolean;
19
+ }
20
+ export interface IntegrationsState {
21
+ integrations: Integration[];
22
+ loading: boolean;
23
+ error: string | null;
24
+ /**
25
+ * Whether this assistant offers connected apps to its tenants at all.
26
+ *
27
+ * False before the first answer arrives, and false again if the server said
28
+ * no — which it does for a disabled catalogue, a missing tenant, and an
29
+ * assistant that does not exist, deliberately without distinguishing them.
30
+ * Anything that hangs off this flag stays hidden until the server has said
31
+ * yes, so nothing ever appears and then disappears.
32
+ */
33
+ offered: boolean;
34
+ /** True once a first answer (or refusal) has arrived. */
35
+ settled: boolean;
36
+ refresh: (dropCache?: boolean) => Promise<void>;
37
+ client: DevicApiClient | null;
38
+ scope: IntegrationsScope;
39
+ }
40
+ /**
41
+ * The apps an assistant offers to its tenants, with the accounts THIS tenant
42
+ * has connected — loaded once and shared.
43
+ *
44
+ * Both the header button and the modal need the same listing: the button to
45
+ * know whether it should exist and which logos to show, the modal to fill
46
+ * itself. Fetching it in each of them would double every round trip and let the
47
+ * two disagree for a moment after connecting an account.
48
+ */
49
+ export declare function useIntegrations(options: UseIntegrationsOptions): IntegrationsState;