@devicai/ui 0.45.0 → 0.46.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.
@@ -2,6 +2,9 @@
2
2
 
3
3
  var jsxRuntime = require('react/jsx-runtime');
4
4
  var React = require('react');
5
+ var reactDom = require('react-dom');
6
+ var theme = require('../theme.js');
7
+ var IntegrationLogo = require('./IntegrationLogo.js');
5
8
 
6
9
  /** Whether a field belongs in the form, given the server's shortlist. */
7
10
  function wanted(field, only) {
@@ -14,11 +17,15 @@ function wanted(field, only) {
14
17
  * API key the user creates at the provider — so without this they cannot be
15
18
  * connected at all, no matter how many popups are opened.
16
19
  *
17
- * Rendered from the fields the server describes rather than from anything
18
- * per-app: there are hundreds of them, and their inputs are the provider's to
19
- * decide. Values go straight to the connection and are never kept here.
20
+ * A dialog of its own rather than something inside the app's card: a provider
21
+ * explains where to find a key in a paragraph, and three of those do not fit
22
+ * in one column of a grid without turning into a stack of clipped text. The
23
+ * fields are rendered from what the server describes, never from anything
24
+ * per-app — there are hundreds of them.
25
+ *
26
+ * Values go straight into the connection and are never kept here.
20
27
  */
21
- function ConnectFieldsForm({ integration, schemes, initialScheme, onlyFields, submitting, onCancel, onSubmit, }) {
28
+ function ConnectFieldsForm({ integration, schemes, initialScheme, onlyFields, submitting, error, theme: theme$1, onCancel, onSubmit, }) {
22
29
  const [mode, setMode] = React.useState(initialScheme ?? schemes[0]?.mode ?? "");
23
30
  const [values, setValues] = React.useState({});
24
31
  const [touched, setTouched] = React.useState(false);
@@ -43,17 +50,23 @@ function ConnectFieldsForm({ integration, schemes, initialScheme, onlyFields, su
43
50
  }
44
51
  onSubmit({ authScheme: scheme.mode, accountFields });
45
52
  };
46
- return (jsxRuntime.jsxs("form", { className: "devic-int-connect-form", onSubmit: handleSubmit, children: [jsxRuntime.jsxs("div", { className: "devic-int-connect-head", children: [jsxRuntime.jsxs("strong", { children: ["Connect ", integration.name] }), scheme.guideUrl && (jsxRuntime.jsx("a", { href: scheme.guideUrl, target: "_blank", rel: "noopener noreferrer", children: "Where do I find this?" }))] }), schemes.length > 1 && (jsxRuntime.jsxs("label", { className: "devic-int-connect-field", children: [jsxRuntime.jsx("span", { children: "Connect with" }), jsxRuntime.jsx("select", { value: scheme.mode, onChange: (e) => {
47
- setMode(e.target.value);
48
- setValues({});
49
- setTouched(false);
50
- }, children: schemes.map((s) => (jsxRuntime.jsx("option", { value: s.mode, children: s.mode }, s.mode))) })] })), fields.map((field) => {
51
- const invalid = touched && field.required && !valueOf(field).trim();
52
- return (jsxRuntime.jsxs("label", { className: "devic-int-connect-field", children: [jsxRuntime.jsxs("span", { children: [field.label, !field.required && jsxRuntime.jsx("em", { children: " (optional)" })] }), jsxRuntime.jsx("input", { type: field.secret ? "password" : "text", value: valueOf(field), onChange: (e) => setValues((prev) => ({ ...prev, [field.name]: e.target.value })), placeholder: field.default, autoComplete: field.secret ? "new-password" : "off", spellCheck: false, "aria-invalid": invalid || undefined, "data-invalid": invalid || undefined }), field.description && (
53
- // Titled as well as shown: providers explain where to find a key
54
- // in a paragraph, and the card clamps it to three lines.
55
- jsxRuntime.jsx("small", { className: "devic-int-connect-hint", title: field.description, children: field.description }))] }, field.name));
56
- }), scheme.redirect && (jsxRuntime.jsxs("small", { className: "devic-int-connect-hint", children: ["You'll be sent to ", integration.name, " to finish authorising."] })), jsxRuntime.jsxs("div", { className: "devic-int-connect-actions", children: [jsxRuntime.jsx("button", { type: "button", className: "devic-int-btn devic-int-btn-small", onClick: onCancel, disabled: submitting, children: "Cancel" }), jsxRuntime.jsx("button", { type: "submit", className: "devic-int-btn devic-int-btn-small devic-int-btn-primary", disabled: submitting, children: submitting ? "Connecting…" : scheme.redirect ? "Continue" : "Connect" })] })] }));
53
+ return reactDom.createPortal(jsxRuntime.jsx("div", { className: "devic-int-overlay devic-int-overlay-stacked", style: theme.themeVars(theme$1), "data-dark": theme.isDarkTheme(theme$1), onClick: onCancel, children: jsxRuntime.jsxs("form", { className: "devic-int-connect-dialog", role: "dialog", "aria-modal": "true", "aria-label": `Connect ${integration.name}`, onClick: (e) => e.stopPropagation(), onSubmit: handleSubmit, children: [jsxRuntime.jsxs("div", { className: "devic-int-connect-header", children: [jsxRuntime.jsx(IntegrationLogo.IntegrationLogo, { integration: integration }), jsxRuntime.jsxs("div", { className: "devic-int-connect-heading", children: [jsxRuntime.jsxs("strong", { children: ["Connect ", integration.name] }), jsxRuntime.jsx("span", { children: scheme.redirect
54
+ ? `You'll be sent to ${integration.name} to finish authorising.`
55
+ : "Your credentials go straight to the app — only you can use this account." })] }), jsxRuntime.jsx("button", { type: "button", className: "devic-int-close", onClick: onCancel, "aria-label": "Close", children: "\u00D7" })] }), jsxRuntime.jsxs("div", { className: "devic-int-connect-body", children: [error && jsxRuntime.jsx("div", { className: "devic-int-error", children: error }), schemes.length > 1 && (jsxRuntime.jsxs("label", { className: "devic-int-connect-field", children: [jsxRuntime.jsx("span", { className: "devic-int-connect-label", children: "Connect with" }), jsxRuntime.jsx("select", { value: scheme.mode, onChange: (e) => {
56
+ setMode(e.target.value);
57
+ setValues({});
58
+ setTouched(false);
59
+ }, children: schemes.map((s) => (jsxRuntime.jsx("option", { value: s.mode, children: s.mode }, s.mode))) })] })), fields.map((field, index) => {
60
+ const invalid = touched && field.required && !valueOf(field).trim();
61
+ return (jsxRuntime.jsxs("label", { className: "devic-int-connect-field", children: [jsxRuntime.jsxs("span", { className: "devic-int-connect-label", children: [field.label, !field.required && jsxRuntime.jsx("em", { children: " (optional)" })] }), jsxRuntime.jsx("input", { autoFocus: index === 0, type: field.secret ? "password" : "text", value: valueOf(field), onChange: (e) => setValues((prev) => ({
62
+ ...prev,
63
+ [field.name]: e.target.value,
64
+ })), placeholder: field.default, autoComplete: field.secret ? "new-password" : "off", spellCheck: false, "aria-invalid": invalid || undefined, "data-invalid": invalid || undefined }), field.description && (jsxRuntime.jsx("small", { className: "devic-int-connect-hint", children: field.description }))] }, field.name));
65
+ }), scheme.guideUrl && (jsxRuntime.jsx("a", { className: "devic-int-connect-guide", href: scheme.guideUrl, target: "_blank", rel: "noopener noreferrer", children: "Where do I find this?" }))] }), jsxRuntime.jsxs("div", { className: "devic-int-connect-actions", children: [jsxRuntime.jsx("button", { type: "button", className: "devic-int-btn", onClick: onCancel, disabled: submitting, children: "Cancel" }), jsxRuntime.jsx("button", { type: "submit", className: "devic-int-btn devic-int-btn-primary", disabled: submitting, children: submitting
66
+ ? "Connecting…"
67
+ : scheme.redirect
68
+ ? "Continue"
69
+ : "Connect" })] })] }) }), document.body);
57
70
  }
58
71
 
59
72
  exports.ConnectFieldsForm = ConnectFieldsForm;
@@ -1 +1 @@
1
- {"version":3,"file":"ConnectFieldsForm.js","sources":["../../../../../src/components/IntegrationsModal/ConnectFieldsForm.tsx"],"sourcesContent":["import { useMemo, useState, type FormEvent, type JSX } from \"react\";\nimport type {\n Integration,\n IntegrationAuthField,\n IntegrationAuthScheme,\n} from \"../../api/types\";\n\nexport interface ConnectFieldsFormProps {\n integration: Integration;\n /** Every way this app can be connected, from `GET /:app/auth`. */\n schemes: IntegrationAuthScheme[];\n /** The scheme the server asked about, when it named one. */\n initialScheme?: string;\n /** Only these are asked for, when the server named what was missing. */\n onlyFields?: IntegrationAuthField[];\n submitting?: boolean;\n onCancel: () => void;\n onSubmit: (values: {\n authScheme: string;\n accountFields: Record<string, string>;\n }) => void;\n}\n\n/** Whether a field belongs in the form, given the server's shortlist. */\nfunction wanted(\n field: IntegrationAuthField,\n only?: IntegrationAuthField[]\n): boolean {\n return !only?.length || only.some((f) => f.name === field.name);\n}\n\n/**\n * The credentials an app asks the end user for.\n *\n * Most apps do not authenticate with credentials Devic holds — they take an\n * API key the user creates at the provider — so without this they cannot be\n * connected at all, no matter how many popups are opened.\n *\n * Rendered from the fields the server describes rather than from anything\n * per-app: there are hundreds of them, and their inputs are the provider's to\n * decide. Values go straight to the connection and are never kept here.\n */\nexport function ConnectFieldsForm({\n integration,\n schemes,\n initialScheme,\n onlyFields,\n submitting,\n onCancel,\n onSubmit,\n}: ConnectFieldsFormProps): JSX.Element | null {\n const [mode, setMode] = useState(\n initialScheme ?? schemes[0]?.mode ?? \"\"\n );\n const [values, setValues] = useState<Record<string, string>>({});\n const [touched, setTouched] = useState(false);\n\n const scheme = useMemo(\n () => schemes.find((s) => s.mode === mode) ?? schemes[0],\n [schemes, mode]\n );\n\n const fields = useMemo(\n () => (scheme?.accountFields ?? []).filter((f) => wanted(f, onlyFields)),\n [scheme, onlyFields]\n );\n\n if (!scheme) return null;\n\n const valueOf = (field: IntegrationAuthField) =>\n values[field.name] ?? field.default ?? \"\";\n\n const missing = fields.filter((f) => f.required && !valueOf(f).trim());\n\n const handleSubmit = (event: FormEvent) => {\n event.preventDefault();\n setTouched(true);\n if (missing.length || submitting) return;\n\n // Only what this scheme declares, trimmed. A value left blank is left out\n // rather than sent empty: the provider would store it and fail later.\n const accountFields: Record<string, string> = {};\n for (const field of scheme.accountFields) {\n const value = valueOf(field).trim();\n if (value) accountFields[field.name] = value;\n }\n onSubmit({ authScheme: scheme.mode, accountFields });\n };\n\n return (\n <form className=\"devic-int-connect-form\" onSubmit={handleSubmit}>\n <div className=\"devic-int-connect-head\">\n <strong>Connect {integration.name}</strong>\n {scheme.guideUrl && (\n <a href={scheme.guideUrl} target=\"_blank\" rel=\"noopener noreferrer\">\n Where do I find this?\n </a>\n )}\n </div>\n\n {/* Only when there is a real choice — a picker with one option is just\n another thing to read before typing a key. */}\n {schemes.length > 1 && (\n <label className=\"devic-int-connect-field\">\n <span>Connect with</span>\n <select\n value={scheme.mode}\n onChange={(e) => {\n setMode(e.target.value);\n setValues({});\n setTouched(false);\n }}\n >\n {schemes.map((s) => (\n <option key={s.mode} value={s.mode}>\n {s.mode}\n </option>\n ))}\n </select>\n </label>\n )}\n\n {fields.map((field) => {\n const invalid = touched && field.required && !valueOf(field).trim();\n return (\n <label key={field.name} className=\"devic-int-connect-field\">\n <span>\n {field.label}\n {!field.required && <em> (optional)</em>}\n </span>\n <input\n type={field.secret ? \"password\" : \"text\"}\n value={valueOf(field)}\n onChange={(e) =>\n setValues((prev) => ({ ...prev, [field.name]: e.target.value }))\n }\n placeholder={field.default}\n autoComplete={field.secret ? \"new-password\" : \"off\"}\n spellCheck={false}\n aria-invalid={invalid || undefined}\n data-invalid={invalid || undefined}\n />\n {field.description && (\n // Titled as well as shown: providers explain where to find a key\n // in a paragraph, and the card clamps it to three lines.\n <small className=\"devic-int-connect-hint\" title={field.description}>\n {field.description}\n </small>\n )}\n </label>\n );\n })}\n\n {scheme.redirect && (\n <small className=\"devic-int-connect-hint\">\n You'll be sent to {integration.name} to finish authorising.\n </small>\n )}\n\n <div className=\"devic-int-connect-actions\">\n <button\n type=\"button\"\n className=\"devic-int-btn devic-int-btn-small\"\n onClick={onCancel}\n disabled={submitting}\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n className=\"devic-int-btn devic-int-btn-small devic-int-btn-primary\"\n disabled={submitting}\n >\n {submitting ? \"Connecting…\" : scheme.redirect ? \"Continue\" : \"Connect\"}\n </button>\n </div>\n </form>\n );\n}\n"],"names":["useState","useMemo","_jsxs","_jsx"],"mappings":"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"}
1
+ {"version":3,"file":"ConnectFieldsForm.js","sources":["../../../../../src/components/IntegrationsModal/ConnectFieldsForm.tsx"],"sourcesContent":["import { useMemo, useState, type FormEvent, type JSX } from \"react\";\nimport { createPortal } from \"react-dom\";\nimport type {\n Integration,\n IntegrationAuthField,\n IntegrationAuthScheme,\n} from \"../../api/types\";\nimport { isDarkTheme, themeVars, type DevicTheme } from \"../theme\";\nimport { IntegrationLogo } from \"./IntegrationLogo\";\n\nexport interface ConnectFieldsFormProps {\n integration: Integration;\n /** Every way this app can be connected, from `GET /:app/auth`. */\n schemes: IntegrationAuthScheme[];\n /** The scheme to start on, when the server named one. */\n initialScheme?: string;\n /** Only these are asked for, when the server named what was missing. */\n onlyFields?: IntegrationAuthField[];\n submitting?: boolean;\n /** Failure from the last attempt, shown above the fields. */\n error?: string | null;\n /** Passed down from the dialog that opened this one. */\n theme?: DevicTheme;\n onCancel: () => void;\n onSubmit: (values: {\n authScheme: string;\n accountFields: Record<string, string>;\n }) => void;\n}\n\n/** Whether a field belongs in the form, given the server's shortlist. */\nfunction wanted(\n field: IntegrationAuthField,\n only?: IntegrationAuthField[]\n): boolean {\n return !only?.length || only.some((f) => f.name === field.name);\n}\n\n/**\n * The credentials an app asks the end user for.\n *\n * Most apps do not authenticate with credentials Devic holds — they take an\n * API key the user creates at the provider — so without this they cannot be\n * connected at all, no matter how many popups are opened.\n *\n * A dialog of its own rather than something inside the app's card: a provider\n * explains where to find a key in a paragraph, and three of those do not fit\n * in one column of a grid without turning into a stack of clipped text. The\n * fields are rendered from what the server describes, never from anything\n * per-app — there are hundreds of them.\n *\n * Values go straight into the connection and are never kept here.\n */\nexport function ConnectFieldsForm({\n integration,\n schemes,\n initialScheme,\n onlyFields,\n submitting,\n error,\n theme,\n onCancel,\n onSubmit,\n}: ConnectFieldsFormProps): JSX.Element | null {\n const [mode, setMode] = useState(initialScheme ?? schemes[0]?.mode ?? \"\");\n const [values, setValues] = useState<Record<string, string>>({});\n const [touched, setTouched] = useState(false);\n\n const scheme = useMemo(\n () => schemes.find((s) => s.mode === mode) ?? schemes[0],\n [schemes, mode]\n );\n\n const fields = useMemo(\n () => (scheme?.accountFields ?? []).filter((f) => wanted(f, onlyFields)),\n [scheme, onlyFields]\n );\n\n if (!scheme) return null;\n\n const valueOf = (field: IntegrationAuthField) =>\n values[field.name] ?? field.default ?? \"\";\n\n const missing = fields.filter((f) => f.required && !valueOf(f).trim());\n\n const handleSubmit = (event: FormEvent) => {\n event.preventDefault();\n setTouched(true);\n if (missing.length || submitting) return;\n\n // Only what this scheme declares, trimmed. A value left blank is left out\n // rather than sent empty: the provider would store it and fail later.\n const accountFields: Record<string, string> = {};\n for (const field of scheme.accountFields) {\n const value = valueOf(field).trim();\n if (value) accountFields[field.name] = value;\n }\n onSubmit({ authScheme: scheme.mode, accountFields });\n };\n\n return createPortal(\n <div\n className=\"devic-int-overlay devic-int-overlay-stacked\"\n style={themeVars(theme)}\n data-dark={isDarkTheme(theme)}\n onClick={onCancel}\n >\n <form\n className=\"devic-int-connect-dialog\"\n role=\"dialog\"\n aria-modal=\"true\"\n aria-label={`Connect ${integration.name}`}\n onClick={(e) => e.stopPropagation()}\n onSubmit={handleSubmit}\n >\n <div className=\"devic-int-connect-header\">\n <IntegrationLogo integration={integration} />\n <div className=\"devic-int-connect-heading\">\n <strong>Connect {integration.name}</strong>\n <span>\n {scheme.redirect\n ? `You'll be sent to ${integration.name} to finish authorising.`\n : \"Your credentials go straight to the app — only you can use this account.\"}\n </span>\n </div>\n <button\n type=\"button\"\n className=\"devic-int-close\"\n onClick={onCancel}\n aria-label=\"Close\"\n >\n ×\n </button>\n </div>\n\n <div className=\"devic-int-connect-body\">\n {error && <div className=\"devic-int-error\">{error}</div>}\n\n {/* Only when there is a real choice — a picker with one option is\n just another thing to read before typing a key. */}\n {schemes.length > 1 && (\n <label className=\"devic-int-connect-field\">\n <span className=\"devic-int-connect-label\">Connect with</span>\n <select\n value={scheme.mode}\n onChange={(e) => {\n setMode(e.target.value);\n setValues({});\n setTouched(false);\n }}\n >\n {schemes.map((s) => (\n <option key={s.mode} value={s.mode}>\n {s.mode}\n </option>\n ))}\n </select>\n </label>\n )}\n\n {fields.map((field, index) => {\n const invalid = touched && field.required && !valueOf(field).trim();\n return (\n <label key={field.name} className=\"devic-int-connect-field\">\n <span className=\"devic-int-connect-label\">\n {field.label}\n {!field.required && <em> (optional)</em>}\n </span>\n <input\n autoFocus={index === 0}\n type={field.secret ? \"password\" : \"text\"}\n value={valueOf(field)}\n onChange={(e) =>\n setValues((prev) => ({\n ...prev,\n [field.name]: e.target.value,\n }))\n }\n placeholder={field.default}\n autoComplete={field.secret ? \"new-password\" : \"off\"}\n spellCheck={false}\n aria-invalid={invalid || undefined}\n data-invalid={invalid || undefined}\n />\n {field.description && (\n <small className=\"devic-int-connect-hint\">\n {field.description}\n </small>\n )}\n </label>\n );\n })}\n\n {scheme.guideUrl && (\n <a\n className=\"devic-int-connect-guide\"\n href={scheme.guideUrl}\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n >\n Where do I find this?\n </a>\n )}\n </div>\n\n <div className=\"devic-int-connect-actions\">\n <button\n type=\"button\"\n className=\"devic-int-btn\"\n onClick={onCancel}\n disabled={submitting}\n >\n Cancel\n </button>\n <button\n type=\"submit\"\n className=\"devic-int-btn devic-int-btn-primary\"\n disabled={submitting}\n >\n {submitting\n ? \"Connecting…\"\n : scheme.redirect\n ? \"Continue\"\n : \"Connect\"}\n </button>\n </div>\n </form>\n </div>,\n document.body\n );\n}\n"],"names":["theme","useState","useMemo","createPortal","_jsx","themeVars","isDarkTheme","_jsxs","IntegrationLogo"],"mappings":"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"}
@@ -38,6 +38,21 @@ function accountLabel(account) {
38
38
  return account.status.toLowerCase();
39
39
  return `connected ${when.toLocaleDateString()}`;
40
40
  }
41
+ /**
42
+ * The scheme the server would pick, given no explicit choice: the one asking
43
+ * for least setup.
44
+ *
45
+ * Mirrors the engine's own preference deliberately. It is only used to decide
46
+ * whether to open a popup or a form before asking, and being wrong is
47
+ * recoverable — the form sends its scheme explicitly, and a redirect that
48
+ * arrives anyway is still honoured.
49
+ */
50
+ function preferredScheme(schemes) {
51
+ if (!schemes?.length)
52
+ return undefined;
53
+ const friction = (s) => (s.composioManaged ? 0 : 1);
54
+ return [...schemes].sort((a, b) => friction(a) - friction(b))[0];
55
+ }
41
56
  function matches(integration, query) {
42
57
  const q = query.trim().toLowerCase();
43
58
  if (!q)
@@ -81,12 +96,48 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
81
96
  const [busyApp, setBusyApp] = React.useState(null);
82
97
  /** Authorization URL surfaced as a link when the popup was blocked. */
83
98
  const [blockedUrl, setBlockedUrl] = React.useState(null);
84
- /** The app whose credentials are being asked for, with what the server asked
85
- * and every way it can be connected. Rendered inside that app's own card,
86
- * where the user pressed Connect. */
99
+ /** The app whose credentials are being asked for, and every way it can be
100
+ * connected. `setup` is present when the server named what was missing. */
87
101
  const [setupPrompt, setSetupPrompt] = React.useState(null);
102
+ /**
103
+ * How each offered app can be connected, read once the listing arrives.
104
+ *
105
+ * Loaded ahead of the click rather than on it: knowing whether an app needs
106
+ * a browser is what decides between opening a popup and opening a form, and
107
+ * that decision has to be made *inside* the user's gesture — a popup opened
108
+ * after an await is blocked.
109
+ */
110
+ const [authByApp, setAuthByApp] = React.useState({});
111
+ /** Failure from the last submit, shown inside the credentials dialog. */
112
+ const [formError, setFormError] = React.useState(null);
88
113
  const [query, setQuery] = React.useState("");
89
114
  const visible = React.useMemo(() => integrations.filter((i) => matches(i, query)), [integrations, query]);
115
+ // Resolved from the listing rather than stored in the prompt: the dialog
116
+ // stays open across a refresh, and a copy taken when it opened would go
117
+ // stale — showing "Not connected" on an app that just connected.
118
+ const promptIntegration = React.useMemo(() => integrations.find((i) => i.app === setupPrompt?.app), [integrations, setupPrompt?.app]);
119
+ // One request per offered app, once the listing is in. They are small,
120
+ // cached server-side, and nothing waits on them: a click that lands before
121
+ // its answer simply falls back to asking the server, as it did before.
122
+ React.useEffect(() => {
123
+ if (!isOpen || !client || !integrations.length)
124
+ return;
125
+ let cancelled = false;
126
+ void Promise.all(integrations.map(async (integration) => {
127
+ try {
128
+ const { schemes } = await client.getIntegrationAuth(integration.app, scope);
129
+ if (!cancelled && schemes?.length) {
130
+ setAuthByApp((prev) => ({ ...prev, [integration.app]: schemes }));
131
+ }
132
+ }
133
+ catch {
134
+ // Not knowing is survivable — the click path handles it.
135
+ }
136
+ }));
137
+ return () => {
138
+ cancelled = true;
139
+ };
140
+ }, [isOpen, client, integrations, scope]);
90
141
  // Report the listing without making the caller's identity part of the
91
142
  // dependency: an inline arrow would fire this on every render.
92
143
  const onChangeRef = React.useRef(onChange);
@@ -116,17 +167,24 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
116
167
  // owner is exactly what this must not do.
117
168
  // eslint-disable-next-line react-hooks/exhaustive-deps
118
169
  }, [isOpen]);
119
- // Escape closes
170
+ // Escape closes — the credentials dialog first, when one is open. Closing
171
+ // everything from under a half-typed key would be its own small disaster.
120
172
  React.useEffect(() => {
121
173
  if (!isOpen)
122
174
  return;
123
175
  const onKey = (e) => {
124
- if (e.key === "Escape")
125
- onClose();
176
+ if (e.key !== "Escape")
177
+ return;
178
+ if (setupPrompt) {
179
+ setSetupPrompt(null);
180
+ setFormError(null);
181
+ return;
182
+ }
183
+ onClose();
126
184
  };
127
185
  document.addEventListener("keydown", onKey);
128
186
  return () => document.removeEventListener("keydown", onKey);
129
- }, [isOpen, onClose]);
187
+ }, [isOpen, onClose, setupPrompt]);
130
188
  /** The round trip currently in flight, if any. */
131
189
  const pendingRef = React.useRef(null);
132
190
  const popupRef = React.useRef(null);
@@ -179,13 +237,26 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
179
237
  return;
180
238
  setActionError(null);
181
239
  setBlockedUrl(null);
240
+ // Ask before doing anything when the app is known to take credentials and
241
+ // no browser is involved. Connecting first would flash an empty popup and
242
+ // spend a request that can only fail — the user has not been asked yet.
243
+ const known = authByApp[integration.app];
244
+ const scheme = values
245
+ ? known?.find((s) => s.mode === values.authScheme)
246
+ : preferredScheme(known);
247
+ if (!values && scheme && !scheme.redirect && scheme.accountFields.length) {
248
+ setSetupPrompt({ app: integration.app, schemes: known });
249
+ return;
250
+ }
182
251
  setBusyApp(integration.app);
183
252
  const nonce = newNonce();
184
253
  const returnTo = `${window.location.origin}/?devic_oauth=${nonce}`;
185
- // Opened empty inside the click, navigated once the URL is known. Both
186
- // entry points are gestures the card's button and the form's submit —
187
- // so this holds on the retry too.
188
- const popup = window.open("", "devic-oauth", "width=520,height=680,menubar=no,toolbar=no");
254
+ // Opened empty inside the click, navigated once the URL is known — browsers
255
+ // only honour `window.open` inside the gesture that triggered it. Skipped
256
+ // when the scheme in hand needs no browser at all.
257
+ const popup = scheme && !scheme.redirect
258
+ ? null
259
+ : window.open("", "devic-oauth", "width=520,height=680,menubar=no,toolbar=no");
189
260
  try {
190
261
  const { connected, authorizationUrl } = await client.connectIntegration(integration.app, { ...scope, returnTo, ...values });
191
262
  // Nothing to authorise: the key the user typed is the account. Close the
@@ -220,12 +291,18 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
220
291
  popup?.close();
221
292
  pendingRef.current = null;
222
293
  setBusyApp(null);
294
+ const message = err instanceof Error ? err.message : String(err);
223
295
  const setup = err?.setupRequired;
224
296
  if (setup) {
225
297
  await openSetupForm(integration, setup);
226
298
  return;
227
299
  }
228
- setActionError(err instanceof Error ? err.message : String(err));
300
+ // While the form is up its own failures belong in it, next to the fields
301
+ // that caused them — a message behind a dialog is a message nobody reads.
302
+ if (setupPrompt?.app === integration.app)
303
+ setFormError(message);
304
+ else
305
+ setActionError(message);
229
306
  }
230
307
  };
231
308
  /**
@@ -244,9 +321,12 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
244
321
  return;
245
322
  }
246
323
  try {
247
- const { schemes } = await client.getIntegrationAuth(integration.app, scope);
324
+ const schemes = authByApp[integration.app] ??
325
+ (await client.getIntegrationAuth(integration.app, scope)).schemes;
248
326
  if (!schemes.length)
249
327
  throw new Error(setup.message);
328
+ setAuthByApp((prev) => ({ ...prev, [integration.app]: schemes }));
329
+ setFormError(null);
250
330
  setSetupPrompt({ app: integration.app, setup, schemes });
251
331
  }
252
332
  catch (err) {
@@ -275,27 +355,30 @@ function IntegrationsModal({ isOpen, onClose, assistantId, tenantId, subtenantId
275
355
  // The variables go on the overlay, not on the modal: the backdrop is part
276
356
  // of the dialog, and a portal inherits nothing from the drawer that opened
277
357
  // it.
278
- jsxRuntime.jsx("div", { className: "devic-int-overlay", style: theme.themeVars(theme$1), "data-dark": theme.isDarkTheme(theme$1), onClick: onClose, children: jsxRuntime.jsxs("div", { className: "devic-int-modal", role: "dialog", "aria-modal": "true", "aria-label": title, onClick: (e) => e.stopPropagation(), children: [jsxRuntime.jsxs("div", { className: "devic-int-header", children: [jsxRuntime.jsxs("h3", { className: "devic-int-title", children: [jsxRuntime.jsx(PlugIcon, {}), title] }), jsxRuntime.jsx("button", { className: "devic-int-close", onClick: onClose, type: "button", "aria-label": "Close", children: "\u00D7" })] }), jsxRuntime.jsxs("div", { className: "devic-int-search", children: [jsxRuntime.jsx(SearchIcon, {}), jsxRuntime.jsx("input", { type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: searchPlaceholder, "aria-label": searchPlaceholder, autoComplete: "off" })] }), jsxRuntime.jsxs("div", { className: "devic-int-body", children: [error && jsxRuntime.jsx("div", { className: "devic-int-error", children: error }), blockedUrl && (jsxRuntime.jsxs("div", { className: "devic-int-notice", children: ["Your browser blocked the pop-up.", " ", jsxRuntime.jsx("a", { href: blockedUrl.url, target: "_blank", rel: "noopener noreferrer", onClick: () => {
279
- pendingRef.current = null;
280
- setBlockedUrl(null);
281
- }, children: "Open the authorisation page" }), " ", "and come back \u2014 then use Refresh."] })), loading && integrations.length === 0 ? (jsxRuntime.jsx("div", { className: "devic-int-loading", children: "Loading apps\u2026" })) : integrations.length === 0 ? (jsxRuntime.jsx("div", { className: "devic-int-empty", children: "No apps available here yet." })) : visible.length === 0 ? (jsxRuntime.jsxs("div", { className: "devic-int-empty", children: ["No apps match \u201C", query.trim(), "\u201D."] })) : (jsxRuntime.jsx("div", { className: "devic-int-grid", children: visible.map((integration) => {
282
- const cardState = stateOf(integration);
283
- const busy = busyApp === integration.app;
284
- return (jsxRuntime.jsxs("div", { className: "devic-int-card", "data-state": cardState.key, children: [jsxRuntime.jsxs("div", { className: "devic-int-card-head", children: [jsxRuntime.jsx(IntegrationLogo.IntegrationLogo, { integration: integration }), jsxRuntime.jsxs("span", { className: "devic-int-state", children: [jsxRuntime.jsx("span", { className: "devic-int-dot", "data-ok": cardState.key === "connected", "data-off": cardState.key === "disconnected", "aria-hidden": "true" }), cardState.label] })] }), jsxRuntime.jsx("div", { className: "devic-int-name", title: integration.name, children: integration.name }), integration.description && (jsxRuntime.jsx("div", { className: "devic-int-description", title: integration.description, children: integration.description })), setupPrompt?.app === integration.app ? (jsxRuntime.jsx(ConnectFieldsForm.ConnectFieldsForm, { integration: integration, schemes: setupPrompt.schemes, initialScheme: setupPrompt.setup.authScheme, onlyFields: setupPrompt.setup.fields, submitting: busy, onCancel: () => setSetupPrompt(null), onSubmit: (values) => handleConnect(integration, values) })) : (jsxRuntime.jsx("button", { type: "button", className: `devic-int-btn devic-int-btn-block${cardState.key === "connected"
285
- ? ""
286
- : " devic-int-btn-primary"}`, onClick: () => handleConnect(integration), disabled: busy || !!busyApp, title: cardState.key === "connected"
287
- ? "Sign in with a different account. The one connected now is replaced."
288
- : undefined, children: busy
289
- ? "Waiting…"
290
- : cardState.key === "disconnected"
291
- ? "Connect"
292
- : cardState.key === "reconnect"
293
- ? "Reconnect"
294
- : // Not "Add account": one account per app is all
295
- // the assistant can use, and connecting again
296
- // retires the previous one.
297
- "Switch account" })), integration.accounts.length > 0 && (jsxRuntime.jsx("ul", { className: "devic-int-accounts", children: integration.accounts.map((account) => (jsxRuntime.jsxs("li", { className: "devic-int-account", children: [jsxRuntime.jsx("span", { className: "devic-int-dot", "data-ok": !account.needsReconnect, "aria-hidden": "true" }), jsxRuntime.jsxs("span", { className: "devic-int-account-label", children: [accountLabel(account), account.needsReconnect && (jsxRuntime.jsxs("span", { className: "devic-int-account-warn", children: [" ", "\u00B7 reconnect required"] }))] }), jsxRuntime.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));
298
- }) }))] }), jsxRuntime.jsxs("div", { className: "devic-int-footer", children: [jsxRuntime.jsx("span", { children: "Only you can see and use the accounts you connect here." }), jsxRuntime.jsx("button", { type: "button", className: "devic-int-btn devic-int-btn-small", onClick: () => void refresh(true), disabled: loading || !!busyApp, children: "Refresh" })] })] }) }), document.body);
358
+ jsxRuntime.jsxs("div", { className: "devic-int-overlay", style: theme.themeVars(theme$1), "data-dark": theme.isDarkTheme(theme$1), onClick: onClose, children: [jsxRuntime.jsxs("div", { className: "devic-int-modal", role: "dialog", "aria-modal": "true", "aria-label": title, onClick: (e) => e.stopPropagation(), children: [jsxRuntime.jsxs("div", { className: "devic-int-header", children: [jsxRuntime.jsxs("h3", { className: "devic-int-title", children: [jsxRuntime.jsx(PlugIcon, {}), title] }), jsxRuntime.jsx("button", { className: "devic-int-close", onClick: onClose, type: "button", "aria-label": "Close", children: "\u00D7" })] }), jsxRuntime.jsxs("div", { className: "devic-int-search", children: [jsxRuntime.jsx(SearchIcon, {}), jsxRuntime.jsx("input", { type: "search", value: query, onChange: (e) => setQuery(e.target.value), placeholder: searchPlaceholder, "aria-label": searchPlaceholder, autoComplete: "off" })] }), jsxRuntime.jsxs("div", { className: "devic-int-body", children: [error && jsxRuntime.jsx("div", { className: "devic-int-error", children: error }), blockedUrl && (jsxRuntime.jsxs("div", { className: "devic-int-notice", children: ["Your browser blocked the pop-up.", " ", jsxRuntime.jsx("a", { href: blockedUrl.url, target: "_blank", rel: "noopener noreferrer", onClick: () => {
359
+ pendingRef.current = null;
360
+ setBlockedUrl(null);
361
+ }, children: "Open the authorisation page" }), " ", "and come back \u2014 then use Refresh."] })), loading && integrations.length === 0 ? (jsxRuntime.jsx("div", { className: "devic-int-loading", children: "Loading apps\u2026" })) : integrations.length === 0 ? (jsxRuntime.jsx("div", { className: "devic-int-empty", children: "No apps available here yet." })) : visible.length === 0 ? (jsxRuntime.jsxs("div", { className: "devic-int-empty", children: ["No apps match \u201C", query.trim(), "\u201D."] })) : (jsxRuntime.jsx("div", { className: "devic-int-grid", children: visible.map((integration) => {
362
+ const cardState = stateOf(integration);
363
+ const busy = busyApp === integration.app;
364
+ return (jsxRuntime.jsxs("div", { className: "devic-int-card", "data-state": cardState.key, children: [jsxRuntime.jsxs("div", { className: "devic-int-card-head", children: [jsxRuntime.jsx(IntegrationLogo.IntegrationLogo, { integration: integration }), jsxRuntime.jsxs("span", { className: "devic-int-state", children: [jsxRuntime.jsx("span", { className: "devic-int-dot", "data-ok": cardState.key === "connected", "data-off": cardState.key === "disconnected", "aria-hidden": "true" }), cardState.label] })] }), jsxRuntime.jsx("div", { className: "devic-int-name", title: integration.name, children: integration.name }), integration.description && (jsxRuntime.jsx("div", { className: "devic-int-description", title: integration.description, children: integration.description })), jsxRuntime.jsx("button", { type: "button", className: `devic-int-btn devic-int-btn-block${cardState.key === "connected"
365
+ ? ""
366
+ : " devic-int-btn-primary"}`, onClick: () => handleConnect(integration), disabled: busy || !!busyApp, title: cardState.key === "connected"
367
+ ? "Sign in with a different account. The one connected now is replaced."
368
+ : undefined, children: busy
369
+ ? "Waiting…"
370
+ : cardState.key === "disconnected"
371
+ ? "Connect"
372
+ : cardState.key === "reconnect"
373
+ ? "Reconnect"
374
+ : // Not "Add account": one account per app is all
375
+ // the assistant can use, and connecting again
376
+ // retires the previous one.
377
+ "Switch account" }), integration.accounts.length > 0 && (jsxRuntime.jsx("ul", { className: "devic-int-accounts", children: integration.accounts.map((account) => (jsxRuntime.jsxs("li", { className: "devic-int-account", children: [jsxRuntime.jsx("span", { className: "devic-int-dot", "data-ok": !account.needsReconnect, "aria-hidden": "true" }), jsxRuntime.jsxs("span", { className: "devic-int-account-label", children: [accountLabel(account), account.needsReconnect && (jsxRuntime.jsxs("span", { className: "devic-int-account-warn", children: [" ", "\u00B7 reconnect required"] }))] }), jsxRuntime.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));
378
+ }) }))] }), jsxRuntime.jsxs("div", { className: "devic-int-footer", children: [jsxRuntime.jsx("span", { children: "Only you can see and use the accounts you connect here." }), jsxRuntime.jsx("button", { type: "button", className: "devic-int-btn devic-int-btn-small", onClick: () => void refresh(true), disabled: loading || !!busyApp, children: "Refresh" })] })] }), setupPrompt && promptIntegration && (jsxRuntime.jsx(ConnectFieldsForm.ConnectFieldsForm, { integration: promptIntegration, schemes: setupPrompt.schemes, initialScheme: setupPrompt.setup?.authScheme, onlyFields: setupPrompt.setup?.fields, submitting: busyApp === setupPrompt.app, error: formError, theme: theme$1, onCancel: () => {
379
+ setSetupPrompt(null);
380
+ setFormError(null);
381
+ }, onSubmit: (values) => handleConnect(promptIntegration, values) }))] }), document.body);
299
382
  }
300
383
 
301
384
  exports.IntegrationsModal = 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 type { DevicApiError } from \"../../api/client\";\nimport type {\n Integration,\n IntegrationAccount,\n IntegrationAuthScheme,\n IntegrationSetupRequired,\n} from \"../../api/types\";\nimport { isDarkTheme, themeVars, type DevicTheme } from \"../theme\";\nimport { ConnectFieldsForm } from \"./ConnectFieldsForm\";\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 /** The app whose credentials are being asked for, with what the server asked\n * and every way it can be connected. Rendered inside that app's own card,\n * where the user pressed Connect. */\n const [setupPrompt, setSetupPrompt] = useState<{\n app: string;\n setup: IntegrationSetupRequired;\n schemes: IntegrationAuthScheme[];\n } | null>(null);\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 setSetupPrompt(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 /**\n * Connects an app, asking for credentials only if it needs them.\n *\n * Optimistic: the request goes out first, and the form appears only when the\n * server answers that something is missing. Most apps take an API key rather\n * than an OAuth round trip, so this is the path that used to fail outright.\n */\n const handleConnect = async (\n integration: Integration,\n values?: { authScheme: string; accountFields: Record<string, string> }\n ) => {\n if (!client || (busyApp && busyApp !== integration.app)) 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. Both\n // entry points are gestures — the card's button and the form's submit —\n // so this holds on the retry too.\n const popup = window.open(\n \"\",\n \"devic-oauth\",\n \"width=520,height=680,menubar=no,toolbar=no\"\n );\n\n try {\n const { connected, authorizationUrl } = await client.connectIntegration(\n integration.app,\n { ...scope, returnTo, ...values }\n );\n\n // Nothing to authorise: the key the user typed is the account. Close the\n // window that was opened in case it was needed and re-read the listing.\n if (connected || !authorizationUrl) {\n popup?.close();\n setSetupPrompt(null);\n finishConnect();\n return;\n }\n\n pendingRef.current = { app: integration.app, returnTo };\n setSetupPrompt(null);\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 setBusyApp(null);\n const setup = (err as DevicApiError)?.setupRequired;\n if (setup) {\n await openSetupForm(integration, setup);\n return;\n }\n setActionError(err instanceof Error ? err.message : String(err));\n }\n };\n\n /**\n * Opens the credentials form for an app that needs one.\n *\n * `stage: \"app\"` is not the end user's to fix — it means the developer who\n * embedded this widget has not registered an application with the provider\n * yet. Asking a stranger for someone else's client secret would be both\n * useless and a good way to teach them to hand credentials to a form, so it\n * is reported as unavailable instead.\n */\n const openSetupForm = async (\n integration: Integration,\n setup: IntegrationSetupRequired\n ) => {\n if (setup.stage === \"app\") {\n setActionError(\n `${integration.name} is not available yet — it still needs to be set up ` +\n `by the app's provider.`\n );\n return;\n }\n try {\n const { schemes } = await client!.getIntegrationAuth(\n integration.app,\n scope\n );\n if (!schemes.length) throw new Error(setup.message);\n setSetupPrompt({ app: integration.app, setup, schemes });\n } catch (err) {\n setActionError(err instanceof Error ? err.message : String(err));\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 {/* The form replaces this app's button rather than opening\n a dialog of its own: it belongs to the card the user\n pressed, and the modal is already a dialog. */}\n {setupPrompt?.app === integration.app ? (\n <ConnectFieldsForm\n integration={integration}\n schemes={setupPrompt.schemes}\n initialScheme={setupPrompt.setup.authScheme}\n onlyFields={setupPrompt.setup.fields}\n submitting={busy}\n onCancel={() => setSetupPrompt(null)}\n onSubmit={(values) => handleConnect(integration, values)}\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 title={\n cardState.key === \"connected\"\n ? \"Sign in with a different account. The one connected now is replaced.\"\n : undefined\n }\n >\n {busy\n ? \"Waiting…\"\n : cardState.key === \"disconnected\"\n ? \"Connect\"\n : cardState.key === \"reconnect\"\n ? \"Reconnect\"\n : // Not \"Add account\": one account per app is all\n // the assistant can use, and connecting again\n // retires the previous one.\n \"Switch account\"}\n </button>\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-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","theme","useIntegrations","useState","useMemo","useRef","useEffect","useCallback","createPortal","themeVars","isDarkTheme","IntegrationLogo","ConnectFieldsForm"],"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 { DevicApiError } from \"../../api/client\";\nimport type {\n Integration,\n IntegrationAccount,\n IntegrationAuthScheme,\n IntegrationSetupRequired,\n} from \"../../api/types\";\nimport { isDarkTheme, themeVars, type DevicTheme } from \"../theme\";\nimport { ConnectFieldsForm } from \"./ConnectFieldsForm\";\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\n/**\n * The scheme the server would pick, given no explicit choice: the one asking\n * for least setup.\n *\n * Mirrors the engine's own preference deliberately. It is only used to decide\n * whether to open a popup or a form before asking, and being wrong is\n * recoverable — the form sends its scheme explicitly, and a redirect that\n * arrives anyway is still honoured.\n */\nfunction preferredScheme(\n schemes?: IntegrationAuthScheme[]\n): IntegrationAuthScheme | undefined {\n if (!schemes?.length) return undefined;\n const friction = (s: IntegrationAuthScheme) => (s.composioManaged ? 0 : 1);\n return [...schemes].sort((a, b) => friction(a) - friction(b))[0];\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 /** The app whose credentials are being asked for, and every way it can be\n * connected. `setup` is present when the server named what was missing. */\n const [setupPrompt, setSetupPrompt] = useState<{\n app: string;\n schemes: IntegrationAuthScheme[];\n setup?: IntegrationSetupRequired;\n } | null>(null);\n /**\n * How each offered app can be connected, read once the listing arrives.\n *\n * Loaded ahead of the click rather than on it: knowing whether an app needs\n * a browser is what decides between opening a popup and opening a form, and\n * that decision has to be made *inside* the user's gesture — a popup opened\n * after an await is blocked.\n */\n const [authByApp, setAuthByApp] = useState<\n Record<string, IntegrationAuthScheme[]>\n >({});\n /** Failure from the last submit, shown inside the credentials dialog. */\n const [formError, setFormError] = useState<string | null>(null);\n const [query, setQuery] = useState(\"\");\n\n const visible = useMemo(\n () => integrations.filter((i) => matches(i, query)),\n [integrations, query]\n );\n\n // Resolved from the listing rather than stored in the prompt: the dialog\n // stays open across a refresh, and a copy taken when it opened would go\n // stale — showing \"Not connected\" on an app that just connected.\n const promptIntegration = useMemo(\n () => integrations.find((i) => i.app === setupPrompt?.app),\n [integrations, setupPrompt?.app]\n );\n\n // One request per offered app, once the listing is in. They are small,\n // cached server-side, and nothing waits on them: a click that lands before\n // its answer simply falls back to asking the server, as it did before.\n useEffect(() => {\n if (!isOpen || !client || !integrations.length) return;\n let cancelled = false;\n void Promise.all(\n integrations.map(async (integration) => {\n try {\n const { schemes } = await client.getIntegrationAuth(\n integration.app,\n scope\n );\n if (!cancelled && schemes?.length) {\n setAuthByApp((prev) => ({ ...prev, [integration.app]: schemes }));\n }\n } catch {\n // Not knowing is survivable — the click path handles it.\n }\n })\n );\n return () => {\n cancelled = true;\n };\n }, [isOpen, client, integrations, scope]);\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 setSetupPrompt(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 — the credentials dialog first, when one is open. Closing\n // everything from under a half-typed key would be its own small disaster.\n useEffect(() => {\n if (!isOpen) return;\n const onKey = (e: KeyboardEvent) => {\n if (e.key !== \"Escape\") return;\n if (setupPrompt) {\n setSetupPrompt(null);\n setFormError(null);\n return;\n }\n onClose();\n };\n document.addEventListener(\"keydown\", onKey);\n return () => document.removeEventListener(\"keydown\", onKey);\n }, [isOpen, onClose, setupPrompt]);\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 /**\n * Connects an app, asking for credentials only if it needs them.\n *\n * Optimistic: the request goes out first, and the form appears only when the\n * server answers that something is missing. Most apps take an API key rather\n * than an OAuth round trip, so this is the path that used to fail outright.\n */\n const handleConnect = async (\n integration: Integration,\n values?: { authScheme: string; accountFields: Record<string, string> }\n ) => {\n if (!client || (busyApp && busyApp !== integration.app)) return;\n setActionError(null);\n setBlockedUrl(null);\n\n // Ask before doing anything when the app is known to take credentials and\n // no browser is involved. Connecting first would flash an empty popup and\n // spend a request that can only fail — the user has not been asked yet.\n const known = authByApp[integration.app];\n const scheme = values\n ? known?.find((s) => s.mode === values.authScheme)\n : preferredScheme(known);\n if (!values && scheme && !scheme.redirect && scheme.accountFields.length) {\n setSetupPrompt({ app: integration.app, schemes: known! });\n return;\n }\n\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 — browsers\n // only honour `window.open` inside the gesture that triggered it. Skipped\n // when the scheme in hand needs no browser at all.\n const popup =\n scheme && !scheme.redirect\n ? null\n : window.open(\n \"\",\n \"devic-oauth\",\n \"width=520,height=680,menubar=no,toolbar=no\"\n );\n\n try {\n const { connected, authorizationUrl } = await client.connectIntegration(\n integration.app,\n { ...scope, returnTo, ...values }\n );\n\n // Nothing to authorise: the key the user typed is the account. Close the\n // window that was opened in case it was needed and re-read the listing.\n if (connected || !authorizationUrl) {\n popup?.close();\n setSetupPrompt(null);\n finishConnect();\n return;\n }\n\n pendingRef.current = { app: integration.app, returnTo };\n setSetupPrompt(null);\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 setBusyApp(null);\n const message = err instanceof Error ? err.message : String(err);\n const setup = (err as DevicApiError)?.setupRequired;\n if (setup) {\n await openSetupForm(integration, setup);\n return;\n }\n // While the form is up its own failures belong in it, next to the fields\n // that caused them — a message behind a dialog is a message nobody reads.\n if (setupPrompt?.app === integration.app) setFormError(message);\n else setActionError(message);\n }\n };\n\n /**\n * Opens the credentials form for an app that needs one.\n *\n * `stage: \"app\"` is not the end user's to fix — it means the developer who\n * embedded this widget has not registered an application with the provider\n * yet. Asking a stranger for someone else's client secret would be both\n * useless and a good way to teach them to hand credentials to a form, so it\n * is reported as unavailable instead.\n */\n const openSetupForm = async (\n integration: Integration,\n setup: IntegrationSetupRequired\n ) => {\n if (setup.stage === \"app\") {\n setActionError(\n `${integration.name} is not available yet — it still needs to be set up ` +\n `by the app's provider.`\n );\n return;\n }\n try {\n const schemes =\n authByApp[integration.app] ??\n (await client!.getIntegrationAuth(integration.app, scope)).schemes;\n if (!schemes.length) throw new Error(setup.message);\n setAuthByApp((prev) => ({ ...prev, [integration.app]: schemes }));\n setFormError(null);\n setSetupPrompt({ app: integration.app, setup, schemes });\n } catch (err) {\n setActionError(err instanceof Error ? err.message : String(err));\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 title={\n cardState.key === \"connected\"\n ? \"Sign in with a different account. The one connected now is replaced.\"\n : undefined\n }\n >\n {busy\n ? \"Waiting…\"\n : cardState.key === \"disconnected\"\n ? \"Connect\"\n : cardState.key === \"reconnect\"\n ? \"Reconnect\"\n : // Not \"Add account\": one account per app is all\n // the assistant can use, and connecting again\n // retires the previous one.\n \"Switch 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\n {/* Its own dialog, over this one: the fields and a provider's\n explanation of where to find a key do not fit in a card of a grid. */}\n {setupPrompt && promptIntegration && (\n <ConnectFieldsForm\n integration={promptIntegration}\n schemes={setupPrompt.schemes}\n initialScheme={setupPrompt.setup?.authScheme}\n onlyFields={setupPrompt.setup?.fields}\n submitting={busyApp === setupPrompt.app}\n error={formError}\n theme={theme}\n onCancel={() => {\n setSetupPrompt(null);\n setFormError(null);\n }}\n onSubmit={(values) => handleConnect(promptIntegration, values)}\n />\n )}\n </div>,\n document.body\n );\n}\n\nexport default IntegrationsModal;\n"],"names":["_jsxs","_jsx","theme","useIntegrations","useState","useMemo","useEffect","useRef","useCallback","createPortal","themeVars","isDarkTheme","IntegrationLogo","ConnectFieldsForm"],"mappings":"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"}