@visns-studio/visns-components 5.15.10 → 5.15.12

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.
@@ -0,0 +1,85 @@
1
+ import React from 'react';
2
+ import styles from '../styles/OAuthConnectionStatus.module.scss';
3
+
4
+ const OAuthConnectionStatus = ({ provider, compact = false }) => {
5
+ const { connection_status: status } = provider;
6
+
7
+ const formatDate = (dateString) => {
8
+ if (!dateString) return null;
9
+ return new Date(dateString).toLocaleString();
10
+ };
11
+
12
+ const getStatusIcon = () => {
13
+ switch (status?.status) {
14
+ case 'connected':
15
+ return '✅';
16
+ case 'expired':
17
+ return '⚠️';
18
+ case 'error':
19
+ return '❌';
20
+ default:
21
+ return '⚪';
22
+ }
23
+ };
24
+
25
+ const getStatusClass = () => {
26
+ switch (status?.status) {
27
+ case 'connected':
28
+ return styles.connected;
29
+ case 'expired':
30
+ return styles.expired;
31
+ case 'error':
32
+ return styles.error;
33
+ default:
34
+ return styles.disconnected;
35
+ }
36
+ };
37
+
38
+ if (compact) {
39
+ return (
40
+ <div className={`${styles.connectionStatus} ${styles.compact} ${getStatusClass()}`}>
41
+ <span className={styles.icon}>{getStatusIcon()}</span>
42
+ <span className={styles.provider}>{provider.display_name}</span>
43
+ <span className={styles.status}>{status?.message || 'Not connected'}</span>
44
+ </div>
45
+ );
46
+ }
47
+
48
+ return (
49
+ <div className={`${styles.connectionStatus} ${getStatusClass()}`}>
50
+ <div className={styles.statusHeader}>
51
+ <span className={styles.icon}>{getStatusIcon()}</span>
52
+ <span className={styles.message}>{status?.message || 'Not connected'}</span>
53
+ </div>
54
+
55
+ {status && (
56
+ <div className={styles.statusDetails}>
57
+ {status.expires_at && (
58
+ <div className={styles.detail}>
59
+ <span className={styles.label}>Expires:</span>
60
+ <span className={styles.value}>{formatDate(status.expires_at)}</span>
61
+ </div>
62
+ )}
63
+
64
+ {status.last_sync_at && (
65
+ <div className={styles.detail}>
66
+ <span className={styles.label}>Last Sync:</span>
67
+ <span className={styles.value}>{formatDate(status.last_sync_at)}</span>
68
+ </div>
69
+ )}
70
+
71
+ {status.sync_status && (
72
+ <div className={styles.detail}>
73
+ <span className={styles.label}>Sync Status:</span>
74
+ <span className={`${styles.value} ${styles[status.sync_status]}`}>
75
+ {status.sync_status}
76
+ </span>
77
+ </div>
78
+ )}
79
+ </div>
80
+ )}
81
+ </div>
82
+ );
83
+ };
84
+
85
+ export default OAuthConnectionStatus;
@@ -0,0 +1,185 @@
1
+ /**
2
+ * OAuthIntegrationsPage Component
3
+ *
4
+ * A complete OAuth integrations page component that provides:
5
+ * - Consistent breadcrumb navigation using project styling patterns
6
+ * - Full OAuth provider management (connect, sync, disconnect)
7
+ * - Step-by-step sync wizard with preview functionality
8
+ * - Toast notification integration (optional)
9
+ * - Configurable via props for different projects
10
+ *
11
+ * Usage:
12
+ * ```jsx
13
+ * import { OAuthIntegrationsPage } from '@visns-studio/visns-components';
14
+ * import { toast } from 'react-toastify';
15
+ *
16
+ * const MyOAuthPage = () => (
17
+ * <OAuthIntegrationsPage
18
+ * breadcrumbConfig={{
19
+ * title: 'OAuth Integrations',
20
+ * parentTitle: 'Admin',
21
+ * parentUrl: '/admin'
22
+ * }}
23
+ * showSuccessToast={toast.success}
24
+ * showErrorToast={toast.error}
25
+ * showInfoToast={toast.info}
26
+ * />
27
+ * );
28
+ * ```
29
+ */
30
+
31
+ import React from 'react';
32
+ import Breadcrumb from '../Breadcrumb';
33
+ import OAuthManager from './OAuthManager';
34
+ import styles from '../styles/GenericDetail.module.scss';
35
+
36
+ const OAuthIntegrationsPage = ({
37
+ // Breadcrumb configuration
38
+ breadcrumbConfig = {
39
+ title: 'OAuth Integrations',
40
+ parentTitle: 'Admin',
41
+ parentUrl: '/admin'
42
+ },
43
+
44
+ // Page configuration
45
+ pageTitle = 'OAuth Integrations',
46
+ pageDescription = 'Connect your CRM with third-party services to automatically sync data and streamline your workflow.',
47
+
48
+ // Event handlers - these can be passed from the consuming app
49
+ onAuthComplete,
50
+ onSyncComplete,
51
+ onError,
52
+
53
+ // Toast notification function (optional) - pass toast.success, toast.error etc.
54
+ showSuccessToast,
55
+ showErrorToast,
56
+ showInfoToast,
57
+
58
+ // Optional custom styling
59
+ className = '',
60
+ layout = 'full'
61
+ }) => {
62
+
63
+ // Enhanced event handlers that integrate with toast notifications
64
+ const handleAuthComplete = (provider, action) => {
65
+ console.log(`OAuth ${action} completed for ${provider}`);
66
+
67
+ if (action === 'connected' && showSuccessToast) {
68
+ showSuccessToast(`Successfully connected to ${provider}`, {
69
+ position: "top-right",
70
+ autoClose: 5000,
71
+ hideProgressBar: false,
72
+ closeOnClick: true,
73
+ pauseOnHover: true,
74
+ draggable: true,
75
+ });
76
+ } else if (action === 'disconnected' && showInfoToast) {
77
+ showInfoToast(`Successfully disconnected from ${provider}`, {
78
+ position: "top-right",
79
+ autoClose: 5000,
80
+ hideProgressBar: false,
81
+ closeOnClick: true,
82
+ pauseOnHover: true,
83
+ draggable: true,
84
+ });
85
+ }
86
+
87
+ // Call custom handler if provided
88
+ if (onAuthComplete) {
89
+ onAuthComplete(provider, action);
90
+ }
91
+ };
92
+
93
+ const handleSyncComplete = (provider, dataType, result) => {
94
+ console.log(`Sync completed for ${provider}.${dataType}`, result);
95
+
96
+ if (result.success && showSuccessToast) {
97
+ const totalSynced = result.result?.total || 0;
98
+ showSuccessToast(`Successfully synced ${totalSynced} ${dataType} from ${provider}`, {
99
+ position: "top-right",
100
+ autoClose: 5000,
101
+ hideProgressBar: false,
102
+ closeOnClick: true,
103
+ pauseOnHover: true,
104
+ draggable: true,
105
+ });
106
+ } else if (!result.success && showErrorToast) {
107
+ showErrorToast(`Sync failed: ${result.message || 'Unknown error'}`, {
108
+ position: "top-right",
109
+ autoClose: 7000,
110
+ hideProgressBar: false,
111
+ closeOnClick: true,
112
+ pauseOnHover: true,
113
+ draggable: true,
114
+ });
115
+ }
116
+
117
+ // Call custom handler if provided
118
+ if (onSyncComplete) {
119
+ onSyncComplete(provider, dataType, result);
120
+ }
121
+ };
122
+
123
+ const handleError = (error) => {
124
+ console.error('OAuth Integration Error:', error);
125
+
126
+ if (showErrorToast) {
127
+ showErrorToast(`Error: ${error.message || 'An unexpected error occurred'}`, {
128
+ position: "top-right",
129
+ autoClose: 7000,
130
+ hideProgressBar: false,
131
+ closeOnClick: true,
132
+ pauseOnHover: true,
133
+ draggable: true,
134
+ });
135
+ }
136
+
137
+ // Call custom handler if provided
138
+ if (onError) {
139
+ onError(error);
140
+ }
141
+ };
142
+
143
+ return (
144
+ <div className={className}>
145
+ {/* Header Section with Breadcrumb */}
146
+ <div className={styles.grid}>
147
+ <div className={styles.grid__row}>
148
+ <div className={`${styles.grid__full} ${
149
+ layout === 'full'
150
+ ? styles.crmtitle
151
+ : styles['crmtitle--alternate']
152
+ }`}>
153
+ <Breadcrumb
154
+ data={{}}
155
+ page={breadcrumbConfig}
156
+ />
157
+ {/* Description positioned on the right side */}
158
+ {pageDescription && (
159
+ <div style={{
160
+ color: '#666',
161
+ fontSize: '0.95rem',
162
+ fontWeight: '400',
163
+ maxWidth: '60%',
164
+ textAlign: 'right'
165
+ }}>
166
+ {pageDescription}
167
+ </div>
168
+ )}
169
+ </div>
170
+ </div>
171
+ </div>
172
+
173
+ {/* OAuth Manager */}
174
+ <div style={{ padding: '32px 0 20px 0' }}>
175
+ <OAuthManager
176
+ onAuthComplete={handleAuthComplete}
177
+ onSyncComplete={handleSyncComplete}
178
+ onError={handleError}
179
+ />
180
+ </div>
181
+ </div>
182
+ );
183
+ };
184
+
185
+ export default OAuthIntegrationsPage;
@@ -0,0 +1,260 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import OAuthProviderCard from './OAuthProviderCard';
3
+ import OAuthConnectionStatus from './OAuthConnectionStatus';
4
+ import DataSyncWizard from './DataSyncWizard';
5
+ import styles from '../styles/OAuthManager.module.scss';
6
+
7
+ const OAuthManager = ({
8
+ onAuthComplete = () => {},
9
+ onSyncComplete = () => {},
10
+ onError = () => {},
11
+ className = ''
12
+ }) => {
13
+ const [providers, setProviders] = useState([]);
14
+ const [loading, setLoading] = useState(true);
15
+ const [error, setError] = useState(null);
16
+ const [syncingProviders, setSyncingProviders] = useState(new Set());
17
+ const [wizardState, setWizardState] = useState({
18
+ isOpen: false,
19
+ provider: null,
20
+ dataType: null
21
+ });
22
+
23
+ useEffect(() => {
24
+ loadProviders();
25
+ }, []);
26
+
27
+ const loadProviders = async () => {
28
+ try {
29
+ setLoading(true);
30
+ const response = await fetch('/integrations/oauth/providers', {
31
+ headers: {
32
+ 'Content-Type': 'application/json',
33
+ 'X-Requested-With': 'XMLHttpRequest',
34
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content')
35
+ },
36
+ credentials: 'same-origin'
37
+ });
38
+
39
+ if (!response.ok) {
40
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
41
+ }
42
+
43
+ const data = await response.json();
44
+
45
+ if (data.success) {
46
+ setProviders(Object.values(data.providers));
47
+ setError(null);
48
+ } else {
49
+ throw new Error(data.message || 'Failed to load providers');
50
+ }
51
+ } catch (err) {
52
+ setError(err.message);
53
+ onError(err);
54
+ } finally {
55
+ setLoading(false);
56
+ }
57
+ };
58
+
59
+ const handleConnect = (provider) => {
60
+ window.location.href = `/integrations/oauth/${provider.name}/authorize`;
61
+ };
62
+
63
+ const handleDisconnect = async (provider) => {
64
+ try {
65
+ const response = await fetch(`/integrations/oauth/${provider.name}/disconnect`, {
66
+ method: 'POST',
67
+ headers: {
68
+ 'Content-Type': 'application/json',
69
+ 'X-Requested-With': 'XMLHttpRequest',
70
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'),
71
+ },
72
+ credentials: 'same-origin'
73
+ });
74
+
75
+ if (!response.ok) {
76
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
77
+ }
78
+
79
+ const data = await response.json();
80
+
81
+ if (data.success) {
82
+ await loadProviders(); // Refresh the list
83
+ onAuthComplete(provider.name, 'disconnected');
84
+ } else {
85
+ throw new Error(data.message || 'Failed to disconnect');
86
+ }
87
+ } catch (err) {
88
+ setError(err.message);
89
+ onError(err);
90
+ }
91
+ };
92
+
93
+ const handleTest = async (provider) => {
94
+ try {
95
+ const response = await fetch(`/integrations/oauth/${provider.name}/test`, {
96
+ method: 'POST',
97
+ headers: {
98
+ 'Content-Type': 'application/json',
99
+ 'X-Requested-With': 'XMLHttpRequest',
100
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'),
101
+ },
102
+ credentials: 'same-origin'
103
+ });
104
+
105
+ if (!response.ok) {
106
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
107
+ }
108
+
109
+ const data = await response.json();
110
+
111
+ if (data.success) {
112
+ await loadProviders(); // Refresh status
113
+ } else {
114
+ throw new Error(data.message || 'Connection test failed');
115
+ }
116
+
117
+ return data;
118
+ } catch (err) {
119
+ setError(err.message);
120
+ onError(err);
121
+ return { success: false, message: err.message };
122
+ }
123
+ };
124
+
125
+ const handleSync = async (provider, dataType, options = {}) => {
126
+ const providerId = provider.name;
127
+
128
+ try {
129
+ setSyncingProviders(prev => new Set([...prev, providerId]));
130
+
131
+ const response = await fetch(`/integrations/oauth/${providerId}/sync`, {
132
+ method: 'POST',
133
+ headers: {
134
+ 'Content-Type': 'application/json',
135
+ 'X-Requested-With': 'XMLHttpRequest',
136
+ 'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'),
137
+ },
138
+ credentials: 'same-origin',
139
+ body: JSON.stringify({
140
+ type: dataType,
141
+ options: options
142
+ }),
143
+ });
144
+
145
+ if (!response.ok) {
146
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
147
+ }
148
+
149
+ const data = await response.json();
150
+
151
+ if (data.success) {
152
+ await loadProviders(); // Refresh status
153
+ onSyncComplete(providerId, dataType, data.result || data);
154
+ } else {
155
+ throw new Error(data.message || 'Sync failed');
156
+ }
157
+
158
+ return data;
159
+ } catch (err) {
160
+ setError(err.message);
161
+ onError(err);
162
+ return { success: false, message: err.message };
163
+ } finally {
164
+ setSyncingProviders(prev => {
165
+ const newSet = new Set(prev);
166
+ newSet.delete(providerId);
167
+ return newSet;
168
+ });
169
+ }
170
+ };
171
+
172
+ if (loading) {
173
+ return (
174
+ <div className={`${styles.oauthManager} ${className}`}>
175
+ <div className={styles.loading}>
176
+ <div className={styles.spinner}></div>
177
+ <p>Loading integrations...</p>
178
+ </div>
179
+ </div>
180
+ );
181
+ }
182
+
183
+ if (error) {
184
+ return (
185
+ <div className={`${styles.oauthManager} ${className}`}>
186
+ <div className={styles.error}>
187
+ <p>Error loading integrations: {error}</p>
188
+ <button onClick={loadProviders} className={styles.retryButton}>
189
+ Retry
190
+ </button>
191
+ </div>
192
+ </div>
193
+ );
194
+ }
195
+
196
+ const handleWizardOpen = (provider, dataType) => {
197
+ setWizardState({
198
+ isOpen: true,
199
+ provider: provider,
200
+ dataType: dataType
201
+ });
202
+ };
203
+
204
+ const handleWizardClose = () => {
205
+ setWizardState({
206
+ isOpen: false,
207
+ provider: null,
208
+ dataType: null
209
+ });
210
+ };
211
+
212
+ const handleWizardComplete = async (dataType, options) => {
213
+ if (wizardState.provider) {
214
+ await handleSync(wizardState.provider, dataType, options);
215
+ }
216
+ handleWizardClose();
217
+ };
218
+
219
+ // If wizard is open, show it full screen
220
+ if (wizardState.isOpen && wizardState.provider) {
221
+ return (
222
+ <DataSyncWizard
223
+ provider={wizardState.provider}
224
+ dataType={wizardState.dataType}
225
+ onCancel={handleWizardClose}
226
+ onComplete={handleWizardComplete}
227
+ />
228
+ );
229
+ }
230
+
231
+ return (
232
+ <div className={`${styles.oauthManager} ${className}`}>
233
+ {providers.length === 0 ? (
234
+ <div className={styles.empty}>
235
+ <p>No integrations available.</p>
236
+ </div>
237
+ ) : (
238
+ <div className={styles.providersGrid}>
239
+ {providers.map((provider) => (
240
+ <OAuthProviderCard
241
+ key={provider.name}
242
+ provider={provider}
243
+ isConnected={provider.is_connected}
244
+ connectionStatus={provider.connection_status}
245
+ isSyncing={syncingProviders.has(provider.name)}
246
+ onConnect={() => handleConnect(provider)}
247
+ onDisconnect={() => handleDisconnect(provider)}
248
+ onTest={() => handleTest(provider)}
249
+ onSync={(dataType, options) => handleSync(provider, dataType, options)}
250
+ onOpenWizard={handleWizardOpen}
251
+ compact={true}
252
+ />
253
+ ))}
254
+ </div>
255
+ )}
256
+ </div>
257
+ );
258
+ };
259
+
260
+ export default OAuthManager;