@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.
- package/package.json +10 -10
- package/src/components/Breadcrumb.jsx +19 -11
- package/src/components/DataGrid.jsx +0 -5
- package/src/components/Field.jsx +58 -8
- package/src/components/Form.jsx +263 -101
- package/src/components/MultiCheckbox.jsx +335 -0
- package/src/components/MultiSelect.jsx +30 -1
- package/src/components/columns/ColumnRenderers.jsx +2 -38
- package/src/components/generic/GenericDetail.jsx +0 -10
- package/src/components/oauth/DataPreviewModal.jsx +372 -0
- package/src/components/oauth/DataSyncWizard.jsx +850 -0
- package/src/components/oauth/OAuthConnectionStatus.jsx +85 -0
- package/src/components/oauth/OAuthIntegrationsPage.jsx +185 -0
- package/src/components/oauth/OAuthManager.jsx +260 -0
- package/src/components/oauth/OAuthProviderCard.jsx +291 -0
- package/src/components/styles/DataPreviewModal.module.scss +658 -0
- package/src/components/styles/DataSyncWizard.module.scss +1211 -0
- package/src/components/styles/MultiCheckbox.module.scss +321 -0
- package/src/components/styles/OAuthConnectionStatus.module.scss +143 -0
- package/src/components/styles/OAuthManager.module.scss +119 -0
- package/src/components/styles/OAuthProviderCard.module.scss +760 -0
- package/src/index.js +14 -0
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
X,
|
|
4
|
+
Download,
|
|
5
|
+
RefreshCw,
|
|
6
|
+
Plus,
|
|
7
|
+
Edit,
|
|
8
|
+
AlertTriangle,
|
|
9
|
+
CheckCircle,
|
|
10
|
+
Search,
|
|
11
|
+
Filter,
|
|
12
|
+
Eye,
|
|
13
|
+
EyeOff
|
|
14
|
+
} from 'lucide-react';
|
|
15
|
+
import styles from '../styles/DataPreviewModal.module.scss';
|
|
16
|
+
|
|
17
|
+
const DataPreviewModal = ({
|
|
18
|
+
isOpen,
|
|
19
|
+
onClose,
|
|
20
|
+
provider,
|
|
21
|
+
dataType,
|
|
22
|
+
onConfirmSync
|
|
23
|
+
}) => {
|
|
24
|
+
const [previewData, setPreviewData] = useState(null);
|
|
25
|
+
const [loading, setLoading] = useState(false);
|
|
26
|
+
const [currentPage, setCurrentPage] = useState(1);
|
|
27
|
+
const [searchTerm, setSearchTerm] = useState('');
|
|
28
|
+
const [statusFilter, setStatusFilter] = useState('all');
|
|
29
|
+
const [showDetails, setShowDetails] = useState({});
|
|
30
|
+
const recordsPerPage = 20;
|
|
31
|
+
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
if (isOpen && provider && dataType) {
|
|
34
|
+
loadPreviewData();
|
|
35
|
+
}
|
|
36
|
+
}, [isOpen, provider, dataType]);
|
|
37
|
+
|
|
38
|
+
const loadPreviewData = async () => {
|
|
39
|
+
setLoading(true);
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(`/integrations/oauth/${provider.name}/preview-comprehensive`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: {
|
|
44
|
+
'Content-Type': 'application/json',
|
|
45
|
+
'X-Requested-With': 'XMLHttpRequest',
|
|
46
|
+
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'),
|
|
47
|
+
},
|
|
48
|
+
credentials: 'same-origin',
|
|
49
|
+
body: JSON.stringify({
|
|
50
|
+
data_type: dataType,
|
|
51
|
+
include_all: true,
|
|
52
|
+
include_status: true
|
|
53
|
+
}),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const data = await response.json();
|
|
61
|
+
setPreviewData(data);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Preview failed:', error);
|
|
64
|
+
setPreviewData({
|
|
65
|
+
success: false,
|
|
66
|
+
error: error.message
|
|
67
|
+
});
|
|
68
|
+
} finally {
|
|
69
|
+
setLoading(false);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const toggleDetails = (recordId) => {
|
|
74
|
+
setShowDetails(prev => ({
|
|
75
|
+
...prev,
|
|
76
|
+
[recordId]: !prev[recordId]
|
|
77
|
+
}));
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const getStatusIcon = (status) => {
|
|
81
|
+
switch (status) {
|
|
82
|
+
case 'new':
|
|
83
|
+
return <Plus size={16} className={styles.statusIconNew} />;
|
|
84
|
+
case 'update':
|
|
85
|
+
return <Edit size={16} className={styles.statusIconUpdate} />;
|
|
86
|
+
case 'skip':
|
|
87
|
+
return <AlertTriangle size={16} className={styles.statusIconSkip} />;
|
|
88
|
+
default:
|
|
89
|
+
return <CheckCircle size={16} className={styles.statusIconDefault} />;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const getStatusBadge = (status) => {
|
|
94
|
+
const baseClass = styles.statusBadge;
|
|
95
|
+
switch (status) {
|
|
96
|
+
case 'new':
|
|
97
|
+
return `${baseClass} ${styles.statusNew}`;
|
|
98
|
+
case 'update':
|
|
99
|
+
return `${baseClass} ${styles.statusUpdate}`;
|
|
100
|
+
case 'skip':
|
|
101
|
+
return `${baseClass} ${styles.statusSkip}`;
|
|
102
|
+
default:
|
|
103
|
+
return `${baseClass} ${styles.statusDefault}`;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const filterRecords = (records) => {
|
|
108
|
+
if (!records) return [];
|
|
109
|
+
|
|
110
|
+
let filtered = records;
|
|
111
|
+
|
|
112
|
+
// Filter by search term
|
|
113
|
+
if (searchTerm) {
|
|
114
|
+
filtered = filtered.filter(record =>
|
|
115
|
+
JSON.stringify(record.data).toLowerCase().includes(searchTerm.toLowerCase()) ||
|
|
116
|
+
(record.existing_data && JSON.stringify(record.existing_data).toLowerCase().includes(searchTerm.toLowerCase()))
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Filter by status
|
|
121
|
+
if (statusFilter !== 'all') {
|
|
122
|
+
filtered = filtered.filter(record => record.sync_status === statusFilter);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return filtered;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const paginateRecords = (records) => {
|
|
129
|
+
const startIndex = (currentPage - 1) * recordsPerPage;
|
|
130
|
+
return records.slice(startIndex, startIndex + recordsPerPage);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const handleConfirmSync = () => {
|
|
134
|
+
if (previewData?.summary?.total_changes > 0) {
|
|
135
|
+
onConfirmSync(dataType, {
|
|
136
|
+
confirmed: true,
|
|
137
|
+
preview_id: previewData.preview_id
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
onClose();
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
if (!isOpen) return null;
|
|
144
|
+
|
|
145
|
+
const filteredRecords = filterRecords(previewData?.records || []);
|
|
146
|
+
const paginatedRecords = paginateRecords(filteredRecords);
|
|
147
|
+
const totalPages = Math.ceil(filteredRecords.length / recordsPerPage);
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<div className={styles.modalOverlay} onClick={onClose}>
|
|
151
|
+
<div className={styles.modalContainer} onClick={(e) => e.stopPropagation()}>
|
|
152
|
+
{/* Header */}
|
|
153
|
+
<div className={styles.modalHeader}>
|
|
154
|
+
<div className={styles.headerContent}>
|
|
155
|
+
<h2>
|
|
156
|
+
<Download size={20} />
|
|
157
|
+
Data Preview: {dataType}
|
|
158
|
+
</h2>
|
|
159
|
+
<p className={styles.subtitle}>
|
|
160
|
+
Review all records before importing from {provider.display_name}
|
|
161
|
+
</p>
|
|
162
|
+
</div>
|
|
163
|
+
<button onClick={onClose} className={styles.closeButton}>
|
|
164
|
+
<X size={20} />
|
|
165
|
+
</button>
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
{/* Content */}
|
|
169
|
+
<div className={styles.modalContent}>
|
|
170
|
+
{loading ? (
|
|
171
|
+
<div className={styles.loadingState}>
|
|
172
|
+
<RefreshCw size={32} className={styles.spinner} />
|
|
173
|
+
<p>Analyzing all records...</p>
|
|
174
|
+
</div>
|
|
175
|
+
) : previewData?.success === false ? (
|
|
176
|
+
<div className={styles.errorState}>
|
|
177
|
+
<AlertTriangle size={32} />
|
|
178
|
+
<h3>Preview Failed</h3>
|
|
179
|
+
<p>{previewData.error}</p>
|
|
180
|
+
<button onClick={loadPreviewData} className={styles.retryButton}>
|
|
181
|
+
Try Again
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
) : previewData ? (
|
|
185
|
+
<>
|
|
186
|
+
{/* Summary Stats */}
|
|
187
|
+
<div className={styles.summarySection}>
|
|
188
|
+
<div className={styles.summaryGrid}>
|
|
189
|
+
<div className={styles.summaryCard}>
|
|
190
|
+
<div className={styles.summaryIcon}>
|
|
191
|
+
<Plus size={20} />
|
|
192
|
+
</div>
|
|
193
|
+
<div className={styles.summaryContent}>
|
|
194
|
+
<span className={styles.summaryNumber}>
|
|
195
|
+
{previewData.summary?.new_records || 0}
|
|
196
|
+
</span>
|
|
197
|
+
<span className={styles.summaryLabel}>New Records</span>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
|
|
201
|
+
<div className={styles.summaryCard}>
|
|
202
|
+
<div className={styles.summaryIcon}>
|
|
203
|
+
<Edit size={20} />
|
|
204
|
+
</div>
|
|
205
|
+
<div className={styles.summaryContent}>
|
|
206
|
+
<span className={styles.summaryNumber}>
|
|
207
|
+
{previewData.summary?.updates || 0}
|
|
208
|
+
</span>
|
|
209
|
+
<span className={styles.summaryLabel}>Updates</span>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
|
|
213
|
+
<div className={styles.summaryCard}>
|
|
214
|
+
<div className={styles.summaryIcon}>
|
|
215
|
+
<AlertTriangle size={20} />
|
|
216
|
+
</div>
|
|
217
|
+
<div className={styles.summaryContent}>
|
|
218
|
+
<span className={styles.summaryNumber}>
|
|
219
|
+
{previewData.summary?.skipped || 0}
|
|
220
|
+
</span>
|
|
221
|
+
<span className={styles.summaryLabel}>Skipped</span>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
|
|
227
|
+
{/* Filters and Search */}
|
|
228
|
+
<div className={styles.filtersSection}>
|
|
229
|
+
<div className={styles.searchBox}>
|
|
230
|
+
<Search size={16} />
|
|
231
|
+
<input
|
|
232
|
+
type="text"
|
|
233
|
+
placeholder="Search records..."
|
|
234
|
+
value={searchTerm}
|
|
235
|
+
onChange={(e) => setSearchTerm(e.target.value)}
|
|
236
|
+
/>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<div className={styles.filterBox}>
|
|
240
|
+
<Filter size={16} />
|
|
241
|
+
<select
|
|
242
|
+
value={statusFilter}
|
|
243
|
+
onChange={(e) => setStatusFilter(e.target.value)}
|
|
244
|
+
>
|
|
245
|
+
<option value="all">All Records</option>
|
|
246
|
+
<option value="new">New Only</option>
|
|
247
|
+
<option value="update">Updates Only</option>
|
|
248
|
+
<option value="skip">Skipped Only</option>
|
|
249
|
+
</select>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
|
|
253
|
+
{/* Records List */}
|
|
254
|
+
<div className={styles.recordsSection}>
|
|
255
|
+
{paginatedRecords.length === 0 ? (
|
|
256
|
+
<div className={styles.emptyState}>
|
|
257
|
+
<p>No records match your current filters</p>
|
|
258
|
+
</div>
|
|
259
|
+
) : (
|
|
260
|
+
paginatedRecords.map((record, index) => (
|
|
261
|
+
<div key={record.id || index} className={styles.recordCard}>
|
|
262
|
+
<div className={styles.recordHeader}>
|
|
263
|
+
<div className={styles.recordInfo}>
|
|
264
|
+
<span className={getStatusBadge(record.sync_status)}>
|
|
265
|
+
{getStatusIcon(record.sync_status)}
|
|
266
|
+
{record.sync_status?.toUpperCase() || 'UNKNOWN'}
|
|
267
|
+
</span>
|
|
268
|
+
<span className={styles.recordTitle}>
|
|
269
|
+
{record.display_name || record.data?.name || record.data?.email || `Record ${index + 1}`}
|
|
270
|
+
</span>
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<button
|
|
274
|
+
onClick={() => toggleDetails(record.id || index)}
|
|
275
|
+
className={styles.toggleButton}
|
|
276
|
+
>
|
|
277
|
+
{showDetails[record.id || index] ? (
|
|
278
|
+
<>Hide Details <EyeOff size={14} /></>
|
|
279
|
+
) : (
|
|
280
|
+
<>Show Details <Eye size={14} /></>
|
|
281
|
+
)}
|
|
282
|
+
</button>
|
|
283
|
+
</div>
|
|
284
|
+
|
|
285
|
+
{record.sync_status === 'update' && record.changes && (
|
|
286
|
+
<div className={styles.changesPreview}>
|
|
287
|
+
<strong>Changes:</strong>
|
|
288
|
+
<ul>
|
|
289
|
+
{record.changes.map((change, i) => (
|
|
290
|
+
<li key={i}>
|
|
291
|
+
<span className={styles.fieldName}>{change.field}:</span>
|
|
292
|
+
<span className={styles.oldValue}>{change.old_value}</span>
|
|
293
|
+
→
|
|
294
|
+
<span className={styles.newValue}>{change.new_value}</span>
|
|
295
|
+
</li>
|
|
296
|
+
))}
|
|
297
|
+
</ul>
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
|
|
301
|
+
{showDetails[record.id || index] && (
|
|
302
|
+
<div className={styles.recordDetails}>
|
|
303
|
+
<div className={styles.dataSection}>
|
|
304
|
+
<h4>External Data (from {provider.display_name})</h4>
|
|
305
|
+
<pre className={styles.jsonData}>
|
|
306
|
+
{JSON.stringify(record.data, null, 2)}
|
|
307
|
+
</pre>
|
|
308
|
+
</div>
|
|
309
|
+
|
|
310
|
+
{record.existing_data && (
|
|
311
|
+
<div className={styles.dataSection}>
|
|
312
|
+
<h4>Current CRM Data</h4>
|
|
313
|
+
<pre className={styles.jsonData}>
|
|
314
|
+
{JSON.stringify(record.existing_data, null, 2)}
|
|
315
|
+
</pre>
|
|
316
|
+
</div>
|
|
317
|
+
)}
|
|
318
|
+
</div>
|
|
319
|
+
)}
|
|
320
|
+
</div>
|
|
321
|
+
))
|
|
322
|
+
)}
|
|
323
|
+
</div>
|
|
324
|
+
|
|
325
|
+
{/* Pagination */}
|
|
326
|
+
{totalPages > 1 && (
|
|
327
|
+
<div className={styles.paginationSection}>
|
|
328
|
+
<button
|
|
329
|
+
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
|
330
|
+
disabled={currentPage === 1}
|
|
331
|
+
className={styles.paginationButton}
|
|
332
|
+
>
|
|
333
|
+
Previous
|
|
334
|
+
</button>
|
|
335
|
+
|
|
336
|
+
<span className={styles.paginationInfo}>
|
|
337
|
+
Page {currentPage} of {totalPages} ({filteredRecords.length} records)
|
|
338
|
+
</span>
|
|
339
|
+
|
|
340
|
+
<button
|
|
341
|
+
onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
|
|
342
|
+
disabled={currentPage === totalPages}
|
|
343
|
+
className={styles.paginationButton}
|
|
344
|
+
>
|
|
345
|
+
Next
|
|
346
|
+
</button>
|
|
347
|
+
</div>
|
|
348
|
+
)}
|
|
349
|
+
</>
|
|
350
|
+
) : null}
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
{/* Footer */}
|
|
354
|
+
<div className={styles.modalFooter}>
|
|
355
|
+
<button onClick={onClose} className={styles.cancelButton}>
|
|
356
|
+
Cancel
|
|
357
|
+
</button>
|
|
358
|
+
<button
|
|
359
|
+
onClick={handleConfirmSync}
|
|
360
|
+
className={styles.confirmButton}
|
|
361
|
+
disabled={!previewData?.summary?.total_changes || loading}
|
|
362
|
+
>
|
|
363
|
+
<Download size={16} />
|
|
364
|
+
Import {previewData?.summary?.total_changes || 0} Changes
|
|
365
|
+
</button>
|
|
366
|
+
</div>
|
|
367
|
+
</div>
|
|
368
|
+
</div>
|
|
369
|
+
);
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
export default DataPreviewModal;
|