@app-connect/core 1.7.24 → 1.7.26
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.
- package/.env.test +5 -5
- package/README.md +441 -441
- package/connector/developerPortal.js +31 -42
- package/connector/mock.js +84 -77
- package/connector/proxy/engine.js +164 -163
- package/connector/proxy/index.js +500 -500
- package/connector/registry.js +252 -252
- package/docs/README.md +50 -50
- package/docs/architecture.md +93 -93
- package/docs/connectors.md +116 -117
- package/docs/handlers.md +125 -125
- package/docs/libraries.md +101 -101
- package/docs/models.md +144 -144
- package/docs/routes.md +115 -115
- package/docs/tests.md +73 -73
- package/handlers/admin.js +523 -523
- package/handlers/appointment.js +193 -0
- package/handlers/auth.js +296 -296
- package/handlers/calldown.js +99 -99
- package/handlers/contact.js +280 -280
- package/handlers/disposition.js +82 -80
- package/handlers/log.js +984 -973
- package/handlers/managedAuth.js +446 -446
- package/handlers/plugin.js +208 -208
- package/handlers/user.js +142 -142
- package/index.js +3140 -2652
- package/jest.config.js +56 -56
- package/lib/analytics.js +54 -54
- package/lib/authSession.js +109 -109
- package/lib/cacheCleanup.js +21 -0
- package/lib/callLogComposer.js +898 -898
- package/lib/callLogLookup.js +34 -0
- package/lib/constants.js +8 -8
- package/lib/debugTracer.js +177 -177
- package/lib/encode.js +30 -30
- package/lib/errorHandler.js +218 -206
- package/lib/generalErrorMessage.js +41 -41
- package/lib/jwt.js +18 -18
- package/lib/logger.js +190 -190
- package/lib/migrateCallLogsSchema.js +116 -0
- package/lib/ringcentral.js +266 -266
- package/lib/s3ErrorLogReport.js +65 -65
- package/lib/sharedSMSComposer.js +471 -471
- package/lib/util.js +67 -67
- package/mcp/README.md +412 -395
- package/mcp/lib/validator.js +91 -91
- package/mcp/mcpHandler.js +425 -425
- package/mcp/tools/cancelAppointment.js +101 -0
- package/mcp/tools/checkAuthStatus.js +105 -105
- package/mcp/tools/confirmAppointment.js +101 -0
- package/mcp/tools/createAppointment.js +157 -0
- package/mcp/tools/createCallLog.js +327 -316
- package/mcp/tools/createContact.js +117 -117
- package/mcp/tools/createMessageLog.js +287 -287
- package/mcp/tools/doAuth.js +60 -60
- package/mcp/tools/findContactByName.js +93 -93
- package/mcp/tools/findContactByPhone.js +101 -101
- package/mcp/tools/getCallLog.js +111 -102
- package/mcp/tools/getGoogleFilePicker.js +99 -99
- package/mcp/tools/getHelp.js +43 -43
- package/mcp/tools/getPublicConnectors.js +94 -94
- package/mcp/tools/getSessionInfo.js +90 -90
- package/mcp/tools/index.js +51 -41
- package/mcp/tools/listAppointments.js +163 -0
- package/mcp/tools/logout.js +96 -96
- package/mcp/tools/rcGetCallLogs.js +65 -65
- package/mcp/tools/updateAppointment.js +154 -0
- package/mcp/tools/updateCallLog.js +130 -126
- package/mcp/ui/App/App.tsx +358 -358
- package/mcp/ui/App/components/AuthInfoForm.tsx +113 -113
- package/mcp/ui/App/components/AuthSuccess.tsx +22 -22
- package/mcp/ui/App/components/ConnectorList.tsx +82 -82
- package/mcp/ui/App/components/DebugPanel.tsx +43 -43
- package/mcp/ui/App/components/OAuthConnect.tsx +270 -270
- package/mcp/ui/App/lib/callTool.ts +130 -130
- package/mcp/ui/App/lib/debugLog.ts +41 -41
- package/mcp/ui/App/lib/developerPortal.ts +111 -111
- package/mcp/ui/App/main.css +5 -5
- package/mcp/ui/App/root.tsx +13 -13
- package/mcp/ui/index.html +13 -13
- package/mcp/ui/package-lock.json +6356 -6356
- package/mcp/ui/package.json +25 -25
- package/mcp/ui/tsconfig.json +26 -26
- package/mcp/ui/vite.config.ts +16 -16
- package/models/accountDataModel.js +33 -33
- package/models/adminConfigModel.js +35 -35
- package/models/cacheModel.js +30 -26
- package/models/callDownListModel.js +34 -34
- package/models/callLogModel.js +33 -27
- package/models/dynamo/connectorSchema.js +146 -146
- package/models/dynamo/lockSchema.js +24 -24
- package/models/dynamo/noteCacheSchema.js +29 -29
- package/models/llmSessionModel.js +17 -17
- package/models/messageLogModel.js +25 -25
- package/models/sequelize.js +16 -16
- package/models/userModel.js +45 -45
- package/package.json +72 -72
- package/releaseNotes.json +1093 -1073
- package/test/connector/proxy/engine.test.js +126 -93
- package/test/connector/proxy/index.test.js +279 -279
- package/test/connector/proxy/sample.json +161 -161
- package/test/connector/registry.test.js +415 -415
- package/test/handlers/admin.test.js +616 -616
- package/test/handlers/auth.test.js +1018 -1015
- package/test/handlers/contact.test.js +1014 -1014
- package/test/handlers/log.test.js +1298 -1160
- package/test/handlers/managedAuth.test.js +458 -458
- package/test/handlers/plugin.test.js +380 -380
- package/test/index.test.js +105 -105
- package/test/lib/cacheCleanup.test.js +42 -0
- package/test/lib/callLogComposer.test.js +1231 -1231
- package/test/lib/debugTracer.test.js +328 -328
- package/test/lib/jwt.test.js +176 -176
- package/test/lib/logger.test.js +206 -206
- package/test/lib/oauth.test.js +359 -359
- package/test/lib/ringcentral.test.js +467 -467
- package/test/lib/sharedSMSComposer.test.js +1084 -1084
- package/test/lib/util.test.js +329 -329
- package/test/mcp/tools/checkAuthStatus.test.js +83 -82
- package/test/mcp/tools/createCallLog.test.js +436 -436
- package/test/mcp/tools/createContact.test.js +58 -58
- package/test/mcp/tools/createMessageLog.test.js +595 -595
- package/test/mcp/tools/doAuth.test.js +113 -113
- package/test/mcp/tools/findContactByName.test.js +275 -275
- package/test/mcp/tools/findContactByPhone.test.js +296 -296
- package/test/mcp/tools/getCallLog.test.js +298 -298
- package/test/mcp/tools/getGoogleFilePicker.test.js +281 -281
- package/test/mcp/tools/getPublicConnectors.test.js +107 -107
- package/test/mcp/tools/getSessionInfo.test.js +127 -127
- package/test/mcp/tools/logout.test.js +233 -233
- package/test/mcp/tools/rcGetCallLogs.test.js +56 -56
- package/test/mcp/tools/updateCallLog.test.js +360 -360
- package/test/models/accountDataModel.test.js +98 -98
- package/test/models/dynamo/connectorSchema.test.js +189 -189
- package/test/models/models.test.js +568 -539
- package/test/routes/managedAuthRoutes.test.js +104 -129
- package/test/setup.js +178 -178
|
@@ -1,113 +1,113 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import { Button } from '@openai/apps-sdk-ui/components/Button'
|
|
3
|
-
import { Input } from '@openai/apps-sdk-ui/components/Input'
|
|
4
|
-
|
|
5
|
-
interface Selection {
|
|
6
|
-
name: string
|
|
7
|
-
const: string
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
interface AuthInfoFormProps {
|
|
11
|
-
environmentType: 'dynamic' | 'selectable'
|
|
12
|
-
urlIdentifier?: string
|
|
13
|
-
instructions?: string
|
|
14
|
-
selections?: Selection[]
|
|
15
|
-
connectorDisplayName: string
|
|
16
|
-
onSubmit: (value: { hostname?: string; selection?: string }) => void
|
|
17
|
-
onBack: () => void
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function AuthInfoForm({
|
|
21
|
-
environmentType,
|
|
22
|
-
urlIdentifier,
|
|
23
|
-
instructions,
|
|
24
|
-
selections,
|
|
25
|
-
connectorDisplayName,
|
|
26
|
-
onSubmit,
|
|
27
|
-
onBack,
|
|
28
|
-
}: AuthInfoFormProps) {
|
|
29
|
-
const [hostname, setHostname] = useState('')
|
|
30
|
-
const [selectedName, setSelectedName] = useState<string | null>(null)
|
|
31
|
-
|
|
32
|
-
const handleSubmit = (e: React.FormEvent) => {
|
|
33
|
-
e.preventDefault()
|
|
34
|
-
if (environmentType === 'dynamic') {
|
|
35
|
-
onSubmit({ hostname })
|
|
36
|
-
} else if (selectedName) {
|
|
37
|
-
onSubmit({ selection: selectedName })
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
return (
|
|
42
|
-
<div className="w-full max-w-md">
|
|
43
|
-
<div className="mb-4">
|
|
44
|
-
<button
|
|
45
|
-
type="button"
|
|
46
|
-
onClick={onBack}
|
|
47
|
-
className="text-sm text-secondary hover:text-primary transition-colors mb-2 cursor-pointer"
|
|
48
|
-
>
|
|
49
|
-
← Back
|
|
50
|
-
</button>
|
|
51
|
-
<h2 className="heading-lg">Connect to {connectorDisplayName}</h2>
|
|
52
|
-
</div>
|
|
53
|
-
|
|
54
|
-
<form onSubmit={handleSubmit} className="space-y-4">
|
|
55
|
-
{environmentType === 'dynamic' && (
|
|
56
|
-
<div className="space-y-2">
|
|
57
|
-
<p className="text-secondary text-sm">
|
|
58
|
-
{instructions || `Enter your ${connectorDisplayName} hostname`}
|
|
59
|
-
</p>
|
|
60
|
-
{urlIdentifier && (
|
|
61
|
-
<p className="text-xs text-secondary opacity-70">
|
|
62
|
-
Example: {urlIdentifier}
|
|
63
|
-
</p>
|
|
64
|
-
)}
|
|
65
|
-
<Input
|
|
66
|
-
type="url"
|
|
67
|
-
value={hostname}
|
|
68
|
-
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setHostname(e.target.value)}
|
|
69
|
-
placeholder="https://your-instance.example.com"
|
|
70
|
-
size="md"
|
|
71
|
-
/>
|
|
72
|
-
</div>
|
|
73
|
-
)}
|
|
74
|
-
|
|
75
|
-
{environmentType === 'selectable' && selections && (
|
|
76
|
-
<div className="space-y-2">
|
|
77
|
-
<p className="text-secondary text-sm">
|
|
78
|
-
Select your {connectorDisplayName} environment
|
|
79
|
-
</p>
|
|
80
|
-
<div className="space-y-2">
|
|
81
|
-
{selections.map((sel) => (
|
|
82
|
-
<button
|
|
83
|
-
key={sel.name}
|
|
84
|
-
type="button"
|
|
85
|
-
onClick={() => setSelectedName(sel.name)}
|
|
86
|
-
className={`w-full text-left rounded-xl border p-3 transition-colors cursor-pointer ${
|
|
87
|
-
selectedName === sel.name
|
|
88
|
-
? 'border-primary bg-surface-hover'
|
|
89
|
-
: 'border-default bg-surface hover:border-primary'
|
|
90
|
-
}`}
|
|
91
|
-
>
|
|
92
|
-
<span className="font-medium text-sm">{sel.name}</span>
|
|
93
|
-
</button>
|
|
94
|
-
))}
|
|
95
|
-
</div>
|
|
96
|
-
</div>
|
|
97
|
-
)}
|
|
98
|
-
|
|
99
|
-
<Button
|
|
100
|
-
type="submit"
|
|
101
|
-
color="primary"
|
|
102
|
-
size="md"
|
|
103
|
-
disabled={
|
|
104
|
-
(environmentType === 'dynamic' && !hostname.trim()) ||
|
|
105
|
-
(environmentType === 'selectable' && !selectedName)
|
|
106
|
-
}
|
|
107
|
-
>
|
|
108
|
-
Continue
|
|
109
|
-
</Button>
|
|
110
|
-
</form>
|
|
111
|
-
</div>
|
|
112
|
-
)
|
|
113
|
-
}
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Button } from '@openai/apps-sdk-ui/components/Button'
|
|
3
|
+
import { Input } from '@openai/apps-sdk-ui/components/Input'
|
|
4
|
+
|
|
5
|
+
interface Selection {
|
|
6
|
+
name: string
|
|
7
|
+
const: string
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface AuthInfoFormProps {
|
|
11
|
+
environmentType: 'dynamic' | 'selectable'
|
|
12
|
+
urlIdentifier?: string
|
|
13
|
+
instructions?: string
|
|
14
|
+
selections?: Selection[]
|
|
15
|
+
connectorDisplayName: string
|
|
16
|
+
onSubmit: (value: { hostname?: string; selection?: string }) => void
|
|
17
|
+
onBack: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function AuthInfoForm({
|
|
21
|
+
environmentType,
|
|
22
|
+
urlIdentifier,
|
|
23
|
+
instructions,
|
|
24
|
+
selections,
|
|
25
|
+
connectorDisplayName,
|
|
26
|
+
onSubmit,
|
|
27
|
+
onBack,
|
|
28
|
+
}: AuthInfoFormProps) {
|
|
29
|
+
const [hostname, setHostname] = useState('')
|
|
30
|
+
const [selectedName, setSelectedName] = useState<string | null>(null)
|
|
31
|
+
|
|
32
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
33
|
+
e.preventDefault()
|
|
34
|
+
if (environmentType === 'dynamic') {
|
|
35
|
+
onSubmit({ hostname })
|
|
36
|
+
} else if (selectedName) {
|
|
37
|
+
onSubmit({ selection: selectedName })
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div className="w-full max-w-md">
|
|
43
|
+
<div className="mb-4">
|
|
44
|
+
<button
|
|
45
|
+
type="button"
|
|
46
|
+
onClick={onBack}
|
|
47
|
+
className="text-sm text-secondary hover:text-primary transition-colors mb-2 cursor-pointer"
|
|
48
|
+
>
|
|
49
|
+
← Back
|
|
50
|
+
</button>
|
|
51
|
+
<h2 className="heading-lg">Connect to {connectorDisplayName}</h2>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
55
|
+
{environmentType === 'dynamic' && (
|
|
56
|
+
<div className="space-y-2">
|
|
57
|
+
<p className="text-secondary text-sm">
|
|
58
|
+
{instructions || `Enter your ${connectorDisplayName} hostname`}
|
|
59
|
+
</p>
|
|
60
|
+
{urlIdentifier && (
|
|
61
|
+
<p className="text-xs text-secondary opacity-70">
|
|
62
|
+
Example: {urlIdentifier}
|
|
63
|
+
</p>
|
|
64
|
+
)}
|
|
65
|
+
<Input
|
|
66
|
+
type="url"
|
|
67
|
+
value={hostname}
|
|
68
|
+
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setHostname(e.target.value)}
|
|
69
|
+
placeholder="https://your-instance.example.com"
|
|
70
|
+
size="md"
|
|
71
|
+
/>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{environmentType === 'selectable' && selections && (
|
|
76
|
+
<div className="space-y-2">
|
|
77
|
+
<p className="text-secondary text-sm">
|
|
78
|
+
Select your {connectorDisplayName} environment
|
|
79
|
+
</p>
|
|
80
|
+
<div className="space-y-2">
|
|
81
|
+
{selections.map((sel) => (
|
|
82
|
+
<button
|
|
83
|
+
key={sel.name}
|
|
84
|
+
type="button"
|
|
85
|
+
onClick={() => setSelectedName(sel.name)}
|
|
86
|
+
className={`w-full text-left rounded-xl border p-3 transition-colors cursor-pointer ${
|
|
87
|
+
selectedName === sel.name
|
|
88
|
+
? 'border-primary bg-surface-hover'
|
|
89
|
+
: 'border-default bg-surface hover:border-primary'
|
|
90
|
+
}`}
|
|
91
|
+
>
|
|
92
|
+
<span className="font-medium text-sm">{sel.name}</span>
|
|
93
|
+
</button>
|
|
94
|
+
))}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
)}
|
|
98
|
+
|
|
99
|
+
<Button
|
|
100
|
+
type="submit"
|
|
101
|
+
color="primary"
|
|
102
|
+
size="md"
|
|
103
|
+
disabled={
|
|
104
|
+
(environmentType === 'dynamic' && !hostname.trim()) ||
|
|
105
|
+
(environmentType === 'selectable' && !selectedName)
|
|
106
|
+
}
|
|
107
|
+
>
|
|
108
|
+
Continue
|
|
109
|
+
</Button>
|
|
110
|
+
</form>
|
|
111
|
+
</div>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
interface AuthSuccessProps {
|
|
2
|
-
connectorDisplayName: string
|
|
3
|
-
userInfo?: { id?: string; name?: string }
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
export function AuthSuccess({ connectorDisplayName, userInfo }: AuthSuccessProps) {
|
|
7
|
-
return (
|
|
8
|
-
<div className="w-full max-w-md">
|
|
9
|
-
<div className="rounded-xl border border-green-200 bg-green-50 p-5 text-center space-y-2">
|
|
10
|
-
<div className="text-2xl text-green-700">✓</div>
|
|
11
|
-
<h2 className="heading-lg text-green-800">Connected</h2>
|
|
12
|
-
<p className="text-sm text-green-700">
|
|
13
|
-
Successfully connected to <strong>{connectorDisplayName}</strong>
|
|
14
|
-
{userInfo?.name ? ` as ${userInfo.name}` : ''}.
|
|
15
|
-
</p>
|
|
16
|
-
<p className="text-xs text-green-600 mt-2">
|
|
17
|
-
You can now use the AI assistant to interact with your CRM.
|
|
18
|
-
</p>
|
|
19
|
-
</div>
|
|
20
|
-
</div>
|
|
21
|
-
)
|
|
22
|
-
}
|
|
1
|
+
interface AuthSuccessProps {
|
|
2
|
+
connectorDisplayName: string
|
|
3
|
+
userInfo?: { id?: string; name?: string }
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function AuthSuccess({ connectorDisplayName, userInfo }: AuthSuccessProps) {
|
|
7
|
+
return (
|
|
8
|
+
<div className="w-full max-w-md">
|
|
9
|
+
<div className="rounded-xl border border-green-200 bg-green-50 p-5 text-center space-y-2">
|
|
10
|
+
<div className="text-2xl text-green-700">✓</div>
|
|
11
|
+
<h2 className="heading-lg text-green-800">Connected</h2>
|
|
12
|
+
<p className="text-sm text-green-700">
|
|
13
|
+
Successfully connected to <strong>{connectorDisplayName}</strong>
|
|
14
|
+
{userInfo?.name ? ` as ${userInfo.name}` : ''}.
|
|
15
|
+
</p>
|
|
16
|
+
<p className="text-xs text-green-600 mt-2">
|
|
17
|
+
You can now use the AI assistant to interact with your CRM.
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
</div>
|
|
21
|
+
)
|
|
22
|
+
}
|
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
import { useState } from 'react'
|
|
2
|
-
import { Button } from "@openai/apps-sdk-ui/components/Button"
|
|
3
|
-
import { Badge } from "@openai/apps-sdk-ui/components/Badge"
|
|
4
|
-
|
|
5
|
-
export interface Connector {
|
|
6
|
-
id: string
|
|
7
|
-
name: string
|
|
8
|
-
displayName: string
|
|
9
|
-
description?: string
|
|
10
|
-
status?: 'public' | 'private'
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
interface ConnectorListProps {
|
|
14
|
-
connectors: Connector[]
|
|
15
|
-
disabled?: boolean
|
|
16
|
-
onSelect: (connector: Connector) => void
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function ConnectorList({ connectors, disabled, onSelect }: ConnectorListProps) {
|
|
20
|
-
const [selectedName, setSelectedName] = useState<string | null>(null)
|
|
21
|
-
|
|
22
|
-
if (!connectors || connectors.length === 0) {
|
|
23
|
-
return (
|
|
24
|
-
<div className="p-4 text-center text-secondary">
|
|
25
|
-
<p>No connectors available</p>
|
|
26
|
-
</div>
|
|
27
|
-
)
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const handleSelect = (connector: Connector) => {
|
|
31
|
-
setSelectedName(connector.displayName)
|
|
32
|
-
onSelect(connector)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
return (
|
|
36
|
-
<div className="w-full max-w-md">
|
|
37
|
-
<div className="mb-4">
|
|
38
|
-
<h2 className="heading-lg">Available Connectors</h2>
|
|
39
|
-
<p className="text-secondary text-sm mt-1">
|
|
40
|
-
Select a CRM to connect with RingCentral
|
|
41
|
-
</p>
|
|
42
|
-
</div>
|
|
43
|
-
|
|
44
|
-
<div className="space-y-3">
|
|
45
|
-
{connectors.map((connector) => {
|
|
46
|
-
const isSelected = selectedName === connector.displayName
|
|
47
|
-
return (
|
|
48
|
-
<div
|
|
49
|
-
key={connector.displayName}
|
|
50
|
-
className="rounded-xl border border-default bg-surface p-4 hover:border-primary transition-colors"
|
|
51
|
-
>
|
|
52
|
-
<div className="flex items-center justify-between gap-3">
|
|
53
|
-
<div className="flex-1">
|
|
54
|
-
<div className="flex items-center gap-2">
|
|
55
|
-
<h3 className="font-medium">{connector.displayName}</h3>
|
|
56
|
-
{connector.status === 'private' && (
|
|
57
|
-
<Badge color="secondary" size="sm">Private</Badge>
|
|
58
|
-
)}
|
|
59
|
-
</div>
|
|
60
|
-
{connector.description && (
|
|
61
|
-
<p className="text-secondary text-sm mt-1">
|
|
62
|
-
{connector.description}
|
|
63
|
-
</p>
|
|
64
|
-
)}
|
|
65
|
-
</div>
|
|
66
|
-
<Button
|
|
67
|
-
color={isSelected ? 'secondary' : 'primary'}
|
|
68
|
-
size="sm"
|
|
69
|
-
onClick={() => handleSelect(connector)}
|
|
70
|
-
disabled={disabled || isSelected}
|
|
71
|
-
>
|
|
72
|
-
{isSelected ? 'Connecting...' : 'Connect'}
|
|
73
|
-
</Button>
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
)
|
|
77
|
-
})}
|
|
78
|
-
</div>
|
|
79
|
-
</div>
|
|
80
|
-
)
|
|
81
|
-
}
|
|
82
|
-
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { Button } from "@openai/apps-sdk-ui/components/Button"
|
|
3
|
+
import { Badge } from "@openai/apps-sdk-ui/components/Badge"
|
|
4
|
+
|
|
5
|
+
export interface Connector {
|
|
6
|
+
id: string
|
|
7
|
+
name: string
|
|
8
|
+
displayName: string
|
|
9
|
+
description?: string
|
|
10
|
+
status?: 'public' | 'private'
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ConnectorListProps {
|
|
14
|
+
connectors: Connector[]
|
|
15
|
+
disabled?: boolean
|
|
16
|
+
onSelect: (connector: Connector) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function ConnectorList({ connectors, disabled, onSelect }: ConnectorListProps) {
|
|
20
|
+
const [selectedName, setSelectedName] = useState<string | null>(null)
|
|
21
|
+
|
|
22
|
+
if (!connectors || connectors.length === 0) {
|
|
23
|
+
return (
|
|
24
|
+
<div className="p-4 text-center text-secondary">
|
|
25
|
+
<p>No connectors available</p>
|
|
26
|
+
</div>
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const handleSelect = (connector: Connector) => {
|
|
31
|
+
setSelectedName(connector.displayName)
|
|
32
|
+
onSelect(connector)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="w-full max-w-md">
|
|
37
|
+
<div className="mb-4">
|
|
38
|
+
<h2 className="heading-lg">Available Connectors</h2>
|
|
39
|
+
<p className="text-secondary text-sm mt-1">
|
|
40
|
+
Select a CRM to connect with RingCentral
|
|
41
|
+
</p>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
<div className="space-y-3">
|
|
45
|
+
{connectors.map((connector) => {
|
|
46
|
+
const isSelected = selectedName === connector.displayName
|
|
47
|
+
return (
|
|
48
|
+
<div
|
|
49
|
+
key={connector.displayName}
|
|
50
|
+
className="rounded-xl border border-default bg-surface p-4 hover:border-primary transition-colors"
|
|
51
|
+
>
|
|
52
|
+
<div className="flex items-center justify-between gap-3">
|
|
53
|
+
<div className="flex-1">
|
|
54
|
+
<div className="flex items-center gap-2">
|
|
55
|
+
<h3 className="font-medium">{connector.displayName}</h3>
|
|
56
|
+
{connector.status === 'private' && (
|
|
57
|
+
<Badge color="secondary" size="sm">Private</Badge>
|
|
58
|
+
)}
|
|
59
|
+
</div>
|
|
60
|
+
{connector.description && (
|
|
61
|
+
<p className="text-secondary text-sm mt-1">
|
|
62
|
+
{connector.description}
|
|
63
|
+
</p>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
<Button
|
|
67
|
+
color={isSelected ? 'secondary' : 'primary'}
|
|
68
|
+
size="sm"
|
|
69
|
+
onClick={() => handleSelect(connector)}
|
|
70
|
+
disabled={disabled || isSelected}
|
|
71
|
+
>
|
|
72
|
+
{isSelected ? 'Connecting...' : 'Connect'}
|
|
73
|
+
</Button>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
)
|
|
77
|
+
})}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
@@ -1,43 +1,43 @@
|
|
|
1
|
-
import { useState, useEffect } from 'react'
|
|
2
|
-
import { getEntries, subscribe, type DebugEntry } from '../lib/debugLog'
|
|
3
|
-
|
|
4
|
-
const LEVEL_STYLE: Record<DebugEntry['level'], string> = {
|
|
5
|
-
info: 'text-blue-700',
|
|
6
|
-
warn: 'text-yellow-700',
|
|
7
|
-
error: 'text-red-700 font-semibold',
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
export function DebugPanel() {
|
|
11
|
-
const [entries, setEntries] = useState(getEntries)
|
|
12
|
-
const [open, setOpen] = useState(false)
|
|
13
|
-
|
|
14
|
-
useEffect(() => subscribe(() => setEntries(getEntries())), [])
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
<div className="mt-4 border border-default rounded-lg overflow-hidden text-xs">
|
|
18
|
-
<button
|
|
19
|
-
type="button"
|
|
20
|
-
onClick={() => setOpen((v) => !v)}
|
|
21
|
-
className="w-full flex items-center justify-between px-3 py-2 bg-surface hover:bg-surface-hover cursor-pointer text-secondary"
|
|
22
|
-
>
|
|
23
|
-
<span>Debug log ({entries.length} entries)</span>
|
|
24
|
-
<span>{open ? '▲' : '▼'}</span>
|
|
25
|
-
</button>
|
|
26
|
-
|
|
27
|
-
{open && (
|
|
28
|
-
<div className="max-h-60 overflow-y-auto bg-black/5 p-2 space-y-0.5 font-mono">
|
|
29
|
-
{entries.length === 0 && (
|
|
30
|
-
<p className="text-secondary italic">No entries yet.</p>
|
|
31
|
-
)}
|
|
32
|
-
{entries.map((e, i) => (
|
|
33
|
-
<div key={i} className={`${LEVEL_STYLE[e.level]} leading-tight`}>
|
|
34
|
-
<span className="opacity-50 mr-1">{e.time}</span>
|
|
35
|
-
<span>[{e.level}]</span>{' '}
|
|
36
|
-
<span className="break-all">{e.msg}</span>
|
|
37
|
-
</div>
|
|
38
|
-
))}
|
|
39
|
-
</div>
|
|
40
|
-
)}
|
|
41
|
-
</div>
|
|
42
|
-
)
|
|
43
|
-
}
|
|
1
|
+
import { useState, useEffect } from 'react'
|
|
2
|
+
import { getEntries, subscribe, type DebugEntry } from '../lib/debugLog'
|
|
3
|
+
|
|
4
|
+
const LEVEL_STYLE: Record<DebugEntry['level'], string> = {
|
|
5
|
+
info: 'text-blue-700',
|
|
6
|
+
warn: 'text-yellow-700',
|
|
7
|
+
error: 'text-red-700 font-semibold',
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function DebugPanel() {
|
|
11
|
+
const [entries, setEntries] = useState(getEntries)
|
|
12
|
+
const [open, setOpen] = useState(false)
|
|
13
|
+
|
|
14
|
+
useEffect(() => subscribe(() => setEntries(getEntries())), [])
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<div className="mt-4 border border-default rounded-lg overflow-hidden text-xs">
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
onClick={() => setOpen((v) => !v)}
|
|
21
|
+
className="w-full flex items-center justify-between px-3 py-2 bg-surface hover:bg-surface-hover cursor-pointer text-secondary"
|
|
22
|
+
>
|
|
23
|
+
<span>Debug log ({entries.length} entries)</span>
|
|
24
|
+
<span>{open ? '▲' : '▼'}</span>
|
|
25
|
+
</button>
|
|
26
|
+
|
|
27
|
+
{open && (
|
|
28
|
+
<div className="max-h-60 overflow-y-auto bg-black/5 p-2 space-y-0.5 font-mono">
|
|
29
|
+
{entries.length === 0 && (
|
|
30
|
+
<p className="text-secondary italic">No entries yet.</p>
|
|
31
|
+
)}
|
|
32
|
+
{entries.map((e, i) => (
|
|
33
|
+
<div key={i} className={`${LEVEL_STYLE[e.level]} leading-tight`}>
|
|
34
|
+
<span className="opacity-50 mr-1">{e.time}</span>
|
|
35
|
+
<span>[{e.level}]</span>{' '}
|
|
36
|
+
<span className="break-all">{e.msg}</span>
|
|
37
|
+
</div>
|
|
38
|
+
))}
|
|
39
|
+
</div>
|
|
40
|
+
)}
|
|
41
|
+
</div>
|
|
42
|
+
)
|
|
43
|
+
}
|