@hed-hog/core 0.0.374 → 0.0.376

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.
@@ -19,8 +19,7 @@ import {
19
19
  AlertDialogHeader,
20
20
  AlertDialogTitle,
21
21
  } from '@/components/ui/alert-dialog';
22
- import { DoNodePoolPicker } from '@/components/integration-profile/do-node-pool-picker';
23
- import { Avatar, AvatarFallback } from '@/components/ui/avatar';
22
+ import { IntegrationProfileSheet } from '@/components/integration-profile/integration-profile-sheet';
24
23
  import { Badge } from '@/components/ui/badge';
25
24
  import { Button } from '@/components/ui/button';
26
25
  import { Checkbox } from '@/components/ui/checkbox';
@@ -32,10 +31,7 @@ import {
32
31
  DialogHeader,
33
32
  DialogTitle,
34
33
  } from '@/components/ui/dialog';
35
- import { Input } from '@/components/ui/input';
36
34
  import { IntegrationLogo } from '@/components/ui/integration-logo';
37
- import { Label } from '@/components/ui/label';
38
- import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
39
35
  import {
40
36
  Select,
41
37
  SelectContent,
@@ -43,13 +39,6 @@ import {
43
39
  SelectTrigger,
44
40
  SelectValue,
45
41
  } from '@/components/ui/select';
46
- import {
47
- Sheet,
48
- SheetDescription,
49
- SheetHeader,
50
- SheetTitle,
51
- } from '@/components/ui/sheet';
52
- import { Switch } from '@/components/ui/switch';
53
42
  import {
54
43
  Table,
55
44
  TableBody,
@@ -63,17 +52,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
63
52
  import { usePagination } from '@/hooks/use-pagination';
64
53
  import { formatDate } from '@/lib/format-date';
65
54
  import { useApp } from '@hed-hog/next-app-provider';
66
- import {
67
- Check,
68
- Copy,
69
- Eye,
70
- EyeOff,
71
- Pencil,
72
- PlugZap,
73
- Plus,
74
- Trash2,
75
- X,
76
- } from 'lucide-react';
55
+ import { Pencil, PlugZap, Trash2 } from 'lucide-react';
77
56
  import { useTranslations } from 'next-intl';
78
57
  import { useSearchParams } from 'next/navigation';
79
58
  import { useEffect, useRef, useState } from 'react';
@@ -112,28 +91,6 @@ type IntegrationProfile = {
112
91
  integration_provider?: IntegrationProvider;
113
92
  };
114
93
 
115
- type ProfileForm = {
116
- name: string;
117
- slug: string;
118
- type_id: string;
119
- provider_id: string;
120
- config: Record<string, string | boolean | number | string[]>;
121
- is_active: boolean;
122
- };
123
-
124
- type FieldDef = {
125
- key: string;
126
- labelKey: string;
127
- type: 'text' | 'password' | 'number' | 'boolean' | 'email' | 'url' | 'select';
128
- required: boolean;
129
- placeholder?: string;
130
- colSpan?: boolean;
131
- options?: Array<{
132
- value: string;
133
- labelKey: string;
134
- }>;
135
- };
136
-
137
94
  type ImportValidatedProfile = {
138
95
  index: number;
139
96
  slug: string;
@@ -166,597 +123,6 @@ type ImportValidationResponse = {
166
123
  }>;
167
124
  };
168
125
 
169
- // ─── Provider field definitions ───────────────────────────────────────────────
170
-
171
- /** Placeholder devolvido pelo backend no lugar de um segredo armazenado. */
172
- const SECRET_MASK = '********';
173
-
174
- const PROVIDER_FIELDS: Record<string, FieldDef[]> = {
175
- smtp: [
176
- {
177
- key: 'host',
178
- labelKey: 'fieldHost',
179
- type: 'text',
180
- required: true,
181
- placeholder: 'smtp.yourdomain.com',
182
- },
183
- {
184
- key: 'port',
185
- labelKey: 'fieldPort',
186
- type: 'number',
187
- required: true,
188
- placeholder: '587',
189
- },
190
- {
191
- key: 'username',
192
- labelKey: 'fieldUsername',
193
- type: 'text',
194
- required: true,
195
- },
196
- {
197
- key: 'password',
198
- labelKey: 'fieldPassword',
199
- type: 'password',
200
- required: true,
201
- },
202
- {
203
- key: 'from_email',
204
- labelKey: 'fieldFromEmail',
205
- type: 'email',
206
- required: true,
207
- },
208
- {
209
- key: 'from_name',
210
- labelKey: 'fieldFromName',
211
- type: 'text',
212
- required: false,
213
- },
214
- {
215
- key: 'reply_to_email',
216
- labelKey: 'fieldReplyTo',
217
- type: 'email',
218
- required: false,
219
- },
220
- {
221
- key: 'secure',
222
- labelKey: 'fieldSecure',
223
- type: 'boolean',
224
- required: false,
225
- colSpan: true,
226
- },
227
- ],
228
- gmail: [
229
- {
230
- key: 'client_id',
231
- labelKey: 'fieldClientId',
232
- type: 'text',
233
- required: true,
234
- },
235
- {
236
- key: 'client_secret',
237
- labelKey: 'fieldClientSecret',
238
- type: 'password',
239
- required: true,
240
- },
241
- {
242
- key: 'refresh_token',
243
- labelKey: 'fieldRefreshToken',
244
- type: 'password',
245
- required: true,
246
- colSpan: true,
247
- },
248
- {
249
- key: 'from_email',
250
- labelKey: 'fieldFromEmail',
251
- type: 'email',
252
- required: true,
253
- },
254
- {
255
- key: 'from_name',
256
- labelKey: 'fieldFromName',
257
- type: 'text',
258
- required: false,
259
- },
260
- ],
261
- ses: [
262
- {
263
- key: 'access_key_id',
264
- labelKey: 'fieldAccessKeyId',
265
- type: 'text',
266
- required: true,
267
- },
268
- {
269
- key: 'secret_access_key',
270
- labelKey: 'fieldSecretAccessKey',
271
- type: 'password',
272
- required: true,
273
- },
274
- {
275
- key: 'region',
276
- labelKey: 'fieldRegion',
277
- type: 'text',
278
- required: true,
279
- placeholder: 'us-east-1',
280
- },
281
- {
282
- key: 'from_email',
283
- labelKey: 'fieldFromEmail',
284
- type: 'email',
285
- required: true,
286
- },
287
- {
288
- key: 'from_name',
289
- labelKey: 'fieldFromName',
290
- type: 'text',
291
- required: false,
292
- },
293
- ],
294
- 'evolution-api': [
295
- {
296
- key: 'host',
297
- labelKey: 'fieldHost',
298
- type: 'url',
299
- required: true,
300
- placeholder: 'https://evolution.yourdomain.com',
301
- colSpan: true,
302
- },
303
- { key: 'token', labelKey: 'fieldToken', type: 'password', required: true },
304
- {
305
- key: 'instance_name',
306
- labelKey: 'fieldInstanceName',
307
- type: 'text',
308
- required: true,
309
- },
310
- ],
311
- 'whatsapp-official': [
312
- {
313
- key: 'phone_number_id',
314
- labelKey: 'fieldPhoneNumberId',
315
- type: 'text',
316
- required: true,
317
- },
318
- {
319
- key: 'access_token',
320
- labelKey: 'fieldAccessToken',
321
- type: 'password',
322
- required: true,
323
- },
324
- {
325
- key: 'business_id',
326
- labelKey: 'fieldBusinessId',
327
- type: 'text',
328
- required: false,
329
- },
330
- ],
331
- local: [
332
- {
333
- key: 'base_path',
334
- labelKey: 'fieldBasePath',
335
- type: 'text',
336
- required: true,
337
- placeholder: '/var/uploads',
338
- colSpan: true,
339
- },
340
- ],
341
- s3: [
342
- {
343
- key: 'access_key_id',
344
- labelKey: 'fieldAccessKeyId',
345
- type: 'text',
346
- required: true,
347
- },
348
- {
349
- key: 'secret_access_key',
350
- labelKey: 'fieldSecretAccessKey',
351
- type: 'password',
352
- required: true,
353
- },
354
- {
355
- key: 'region',
356
- labelKey: 'fieldRegion',
357
- type: 'text',
358
- required: true,
359
- placeholder: 'us-east-1',
360
- },
361
- { key: 'bucket', labelKey: 'fieldBucket', type: 'text', required: true },
362
- ],
363
- gcs: [
364
- {
365
- key: 'project_id',
366
- labelKey: 'fieldProjectId',
367
- type: 'text',
368
- required: true,
369
- },
370
- { key: 'bucket', labelKey: 'fieldBucket', type: 'text', required: true },
371
- {
372
- key: 'key_file_json',
373
- labelKey: 'fieldKeyFileJson',
374
- type: 'password',
375
- required: true,
376
- colSpan: true,
377
- },
378
- ],
379
- 'azure-blob': [
380
- {
381
- key: 'account',
382
- labelKey: 'fieldAccount',
383
- type: 'text',
384
- required: true,
385
- placeholder: 'mystorageaccount',
386
- },
387
- {
388
- key: 'key',
389
- labelKey: 'fieldAccountKey',
390
- type: 'password',
391
- required: true,
392
- },
393
- {
394
- key: 'container',
395
- labelKey: 'fieldContainer',
396
- type: 'text',
397
- required: true,
398
- },
399
- ],
400
- 's3-compatible': [
401
- {
402
- key: 'endpoint',
403
- labelKey: 'fieldEndpoint',
404
- type: 'url',
405
- required: true,
406
- colSpan: true,
407
- },
408
- {
409
- key: 'access_key_id',
410
- labelKey: 'fieldAccessKeyId',
411
- type: 'text',
412
- required: true,
413
- },
414
- {
415
- key: 'secret_access_key',
416
- labelKey: 'fieldSecretAccessKey',
417
- type: 'password',
418
- required: true,
419
- },
420
- { key: 'bucket', labelKey: 'fieldBucket', type: 'text', required: true },
421
- {
422
- key: 'region',
423
- labelKey: 'fieldRegion',
424
- type: 'text',
425
- required: false,
426
- placeholder: 'us-east-1',
427
- },
428
- ],
429
- openai: [
430
- {
431
- key: 'api_key',
432
- labelKey: 'fieldApiKey',
433
- type: 'password',
434
- required: true,
435
- colSpan: true,
436
- },
437
- {
438
- key: 'organization',
439
- labelKey: 'fieldOrganization',
440
- type: 'text',
441
- required: false,
442
- },
443
- ],
444
- gemini: [
445
- {
446
- key: 'api_key',
447
- labelKey: 'fieldApiKey',
448
- type: 'password',
449
- required: true,
450
- colSpan: true,
451
- },
452
- ],
453
- claude: [
454
- {
455
- key: 'api_key',
456
- labelKey: 'fieldApiKey',
457
- type: 'password',
458
- required: true,
459
- colSpan: true,
460
- },
461
- ],
462
- 'google-oauth': [
463
- {
464
- key: 'client_id',
465
- labelKey: 'fieldClientId',
466
- type: 'text',
467
- required: true,
468
- },
469
- {
470
- key: 'client_secret',
471
- labelKey: 'fieldClientSecret',
472
- type: 'password',
473
- required: true,
474
- },
475
- ],
476
- 'github-oauth': [
477
- {
478
- key: 'client_id',
479
- labelKey: 'fieldClientId',
480
- type: 'text',
481
- required: true,
482
- },
483
- {
484
- key: 'client_secret',
485
- labelKey: 'fieldClientSecret',
486
- type: 'password',
487
- required: true,
488
- },
489
- ],
490
- 'microsoft-oauth': [
491
- {
492
- key: 'client_id',
493
- labelKey: 'fieldClientId',
494
- type: 'text',
495
- required: true,
496
- },
497
- {
498
- key: 'client_secret',
499
- labelKey: 'fieldClientSecret',
500
- type: 'password',
501
- required: true,
502
- },
503
- {
504
- key: 'tenant_id',
505
- labelKey: 'fieldTenantId',
506
- type: 'text',
507
- required: true,
508
- placeholder: 'common',
509
- },
510
- ],
511
- 'facebook-oauth': [
512
- {
513
- key: 'client_id',
514
- labelKey: 'fieldClientId',
515
- type: 'text',
516
- required: true,
517
- },
518
- {
519
- key: 'client_secret',
520
- labelKey: 'fieldClientSecret',
521
- type: 'password',
522
- required: true,
523
- },
524
- ],
525
- 'microsoft-entra-id-oauth': [
526
- {
527
- key: 'client_id',
528
- labelKey: 'fieldClientId',
529
- type: 'text',
530
- required: true,
531
- },
532
- {
533
- key: 'client_secret',
534
- labelKey: 'fieldClientSecret',
535
- type: 'password',
536
- required: true,
537
- },
538
- {
539
- key: 'tenant_id',
540
- labelKey: 'fieldTenantId',
541
- type: 'text',
542
- required: true,
543
- },
544
- ],
545
- 'apple-oauth': [
546
- {
547
- key: 'client_id',
548
- labelKey: 'fieldClientId',
549
- type: 'text',
550
- required: true,
551
- placeholder: 'com.yourdomain.service',
552
- },
553
- {
554
- key: 'team_id',
555
- labelKey: 'fieldTeamId',
556
- type: 'text',
557
- required: true,
558
- },
559
- {
560
- key: 'key_id',
561
- labelKey: 'fieldKeyId',
562
- type: 'text',
563
- required: true,
564
- },
565
- {
566
- key: 'private_key',
567
- labelKey: 'fieldPrivateKey',
568
- type: 'password',
569
- required: true,
570
- placeholder: '-----BEGIN PRIVATE KEY-----...',
571
- },
572
- ],
573
- 'linkedin-oauth': [
574
- {
575
- key: 'client_id',
576
- labelKey: 'fieldClientId',
577
- type: 'text',
578
- required: true,
579
- },
580
- {
581
- key: 'client_secret',
582
- labelKey: 'fieldClientSecret',
583
- type: 'password',
584
- required: true,
585
- },
586
- ],
587
- stripe: [
588
- {
589
- key: 'publishable_key',
590
- labelKey: 'fieldPublishableKey',
591
- type: 'text',
592
- required: false,
593
- },
594
- {
595
- key: 'secret_key',
596
- labelKey: 'fieldSecretKey',
597
- type: 'password',
598
- required: true,
599
- },
600
- {
601
- key: 'webhook_secret',
602
- labelKey: 'fieldWebhookSecret',
603
- type: 'password',
604
- required: false,
605
- },
606
- {
607
- key: 'mode',
608
- labelKey: 'fieldMode',
609
- type: 'select',
610
- required: false,
611
- options: [
612
- { value: 'sandbox', labelKey: 'fieldModeSandbox' },
613
- { value: 'production', labelKey: 'fieldModeProduction' },
614
- ],
615
- },
616
- ],
617
- mercado_pago: [
618
- {
619
- key: 'public_key',
620
- labelKey: 'fieldPublicKey',
621
- type: 'text',
622
- required: false,
623
- },
624
- {
625
- key: 'access_token',
626
- labelKey: 'fieldAccessToken',
627
- type: 'password',
628
- required: true,
629
- },
630
- {
631
- key: 'webhook_secret',
632
- labelKey: 'fieldWebhookSecret',
633
- type: 'password',
634
- required: false,
635
- },
636
- {
637
- key: 'mode',
638
- labelKey: 'fieldMode',
639
- type: 'select',
640
- required: false,
641
- options: [
642
- { value: 'sandbox', labelKey: 'fieldModeSandbox' },
643
- { value: 'production', labelKey: 'fieldModeProduction' },
644
- ],
645
- },
646
- ],
647
- recaptcha: [
648
- {
649
- key: 'site_key',
650
- labelKey: 'fieldSiteKey',
651
- type: 'text',
652
- required: true,
653
- colSpan: true,
654
- },
655
- {
656
- key: 'secret_key',
657
- labelKey: 'fieldSecretKey',
658
- type: 'password',
659
- required: true,
660
- colSpan: true,
661
- },
662
- {
663
- key: 'score_threshold',
664
- labelKey: 'fieldScoreThreshold',
665
- type: 'number',
666
- required: false,
667
- placeholder: '0.5',
668
- },
669
- ],
670
- 'cloudflare-turnstile': [
671
- {
672
- key: 'site_key',
673
- labelKey: 'fieldSiteKey',
674
- type: 'text',
675
- required: true,
676
- colSpan: true,
677
- },
678
- {
679
- key: 'secret_key',
680
- labelKey: 'fieldSecretKey',
681
- type: 'password',
682
- required: true,
683
- colSpan: true,
684
- },
685
- ],
686
- altcha: [
687
- {
688
- key: 'hmac_key',
689
- labelKey: 'fieldHmacKey',
690
- type: 'password',
691
- required: true,
692
- colSpan: true,
693
- },
694
- {
695
- key: 'api_url',
696
- labelKey: 'fieldApiUrl',
697
- type: 'url',
698
- required: false,
699
- colSpan: true,
700
- placeholder: 'https://eu.altcha.org/api/v1',
701
- },
702
- ],
703
- // O node pool de vídeo NÃO é um campo de texto: é escolhido por nome via picker
704
- // dedicado (carrega os pools da DO API com o token + cluster_id). Ver DoNodePoolPicker.
705
- digitalocean: [
706
- {
707
- key: 'api_token',
708
- labelKey: 'fieldApiToken',
709
- type: 'password',
710
- required: true,
711
- colSpan: true,
712
- },
713
- {
714
- key: 'cluster_id',
715
- labelKey: 'fieldClusterId',
716
- type: 'text',
717
- required: true,
718
- },
719
- {
720
- key: 'region',
721
- labelKey: 'fieldRegion',
722
- type: 'text',
723
- required: true,
724
- placeholder: 'nyc1',
725
- },
726
- ],
727
- kubernetes: [
728
- {
729
- key: 'api_server',
730
- labelKey: 'fieldApiServer',
731
- type: 'url',
732
- required: true,
733
- colSpan: true,
734
- placeholder: 'https://10.0.0.1:6443',
735
- },
736
- {
737
- key: 'token',
738
- labelKey: 'fieldToken',
739
- type: 'password',
740
- required: true,
741
- colSpan: true,
742
- },
743
- {
744
- key: 'ca_cert',
745
- labelKey: 'fieldCaCert',
746
- type: 'password',
747
- required: false,
748
- colSpan: true,
749
- },
750
- {
751
- key: 'namespace',
752
- labelKey: 'fieldNamespace',
753
- type: 'text',
754
- required: false,
755
- placeholder: 'hcode',
756
- },
757
- ],
758
- };
759
-
760
126
  // ─── Helpers ─────────────────────────────────────────────────────────────────
761
127
 
762
128
  function getLocaleName(locales: LocaleEntry[], code: string): string {
@@ -765,24 +131,6 @@ function getLocaleName(locales: LocaleEntry[], code: string): string {
765
131
  );
766
132
  }
767
133
 
768
- function toSlug(name: string): string {
769
- return name
770
- .toLowerCase()
771
- .normalize('NFD')
772
- .replace(/[̀-ͯ]/g, '')
773
- .replace(/[^a-z0-9]+/g, '-')
774
- .replace(/^-+|-+$/g, '');
775
- }
776
-
777
- const defaultForm = (): ProfileForm => ({
778
- name: '',
779
- slug: '',
780
- type_id: '',
781
- provider_id: '',
782
- config: {},
783
- is_active: true,
784
- });
785
-
786
134
  // ─── Tab list component ───────────────────────────────────────────────────────
787
135
 
788
136
  function TypeTab({
@@ -1050,14 +398,9 @@ export default function IntegrationProfilesPage() {
1050
398
  const [isSheetOpen, setIsSheetOpen] = useState(false);
1051
399
  const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
1052
400
  const [profileToDelete, setProfileToDelete] = useState<number | null>(null);
1053
- const [editingProfile, setEditingProfile] =
1054
- useState<IntegrationProfile | null>(null);
1055
- const [formData, setFormData] = useState<ProfileForm>(defaultForm());
1056
- const [isSubmitting, setIsSubmitting] = useState(false);
1057
- const [visibleFields, setVisibleFields] = useState<Record<string, boolean>>(
1058
- {}
401
+ const [editingProfileId, setEditingProfileId] = useState<number | null>(
402
+ null
1059
403
  );
1060
- const [copiedUrlIndex, setCopiedUrlIndex] = useState<number | null>(null);
1061
404
  const [selectedProfileIds, setSelectedProfileIds] = useState<Set<number>>(
1062
405
  new Set()
1063
406
  );
@@ -1074,40 +417,6 @@ export default function IntegrationProfilesPage() {
1074
417
  Record<string, 'ignore' | 'replace' | 'rename_auto'>
1075
418
  >({});
1076
419
  const fileInputRef = useRef<HTMLInputElement>(null);
1077
- const slugAutoRef = useRef(true);
1078
-
1079
- // Providers filtered by the type currently selected in the form
1080
- const formTypeProviders = providers.filter(
1081
- (p) => String(p.type_id) === formData.type_id
1082
- );
1083
- const selectedProvider = formTypeProviders.find(
1084
- (p) => String(p.id) === formData.provider_id
1085
- );
1086
- const providerFields = selectedProvider
1087
- ? (PROVIDER_FIELDS[selectedProvider.slug] ?? [])
1088
- : [];
1089
-
1090
- const isOAuthProvider = (selectedProvider?.slug ?? '').endsWith('-oauth');
1091
- const oauthProviderName = isOAuthProvider
1092
- ? selectedProvider!.slug.replace(/-oauth$/, '')
1093
- : '';
1094
- const appUrl =
1095
- typeof window !== 'undefined'
1096
- ? window.location.origin
1097
- : String(getSettingValue?.('url') ?? '').replace(/\/+$/, '');
1098
- const defaultCallbackUrls =
1099
- isOAuthProvider && appUrl
1100
- ? [
1101
- `${appUrl}/callback/${oauthProviderName}/login`,
1102
- `${appUrl}/callback/${oauthProviderName}/register`,
1103
- `${appUrl}/callback/${oauthProviderName}/connect`,
1104
- ]
1105
- : [];
1106
- const callbackUrls: string[] = (() => {
1107
- const saved = formData.config.callback_urls;
1108
- if (Array.isArray(saved)) return saved as string[];
1109
- return defaultCallbackUrls;
1110
- })();
1111
420
 
1112
421
  useEffect(() => {
1113
422
  request<IntegrationType[]>({
@@ -1159,58 +468,12 @@ export default function IntegrationProfilesPage() {
1159
468
  }, [requestedType, types]);
1160
469
 
1161
470
  const handleCreate = () => {
1162
- const activeType = types.find((ty) => ty.slug === activeTab);
1163
- setEditingProfile(null);
1164
- slugAutoRef.current = true;
1165
- setFormData({
1166
- ...defaultForm(),
1167
- type_id: activeType ? String(activeType.id) : '',
1168
- });
471
+ setEditingProfileId(null);
1169
472
  setIsSheetOpen(true);
1170
473
  };
1171
474
 
1172
- const handleEdit = async (profile: IntegrationProfile) => {
1173
- setEditingProfile(profile);
1174
- slugAutoRef.current = false;
1175
- try {
1176
- const { data } = await request<IntegrationProfile>({
1177
- url: `/integration-profile/${profile.id}`,
1178
- method: 'GET',
1179
- });
1180
- const loadedConfig =
1181
- (data.config as Record<string, string | boolean | number | string[]>) ??
1182
- {};
1183
- const providerSlug =
1184
- providers.find((p) => p.id === data.provider_id)?.slug ?? '';
1185
- // Segredos chegam mascarados (********) do backend e PERMANECEM mascarados no
1186
- // form: o campo fica desabilitado mostrando ********; o botão "trocar" limpa e
1187
- // reabilita para digitar um novo valor. Em branco ou mascarado, o backend
1188
- // preserva o segredo armazenado.
1189
- if (
1190
- providerSlug.endsWith('-oauth') &&
1191
- !Array.isArray(loadedConfig.callback_urls)
1192
- ) {
1193
- const pName = providerSlug.replace(/-oauth$/, '');
1194
- if (appUrl) {
1195
- loadedConfig.callback_urls = [
1196
- `${appUrl}/callback/${pName}/login`,
1197
- `${appUrl}/callback/${pName}/register`,
1198
- `${appUrl}/callback/${pName}/connect`,
1199
- ];
1200
- }
1201
- }
1202
- setFormData({
1203
- name: data.name,
1204
- slug: data.slug,
1205
- type_id: String(data.type_id),
1206
- provider_id: String(data.provider_id),
1207
- config: loadedConfig,
1208
- is_active: data.is_active,
1209
- });
1210
- } catch {
1211
- toast.error(t('loadError'));
1212
- return;
1213
- }
475
+ const handleEdit = (profile: IntegrationProfile) => {
476
+ setEditingProfileId(profile.id);
1214
477
  setIsSheetOpen(true);
1215
478
  };
1216
479
 
@@ -1394,116 +657,6 @@ export default function IntegrationProfilesPage() {
1394
657
  }
1395
658
  };
1396
659
 
1397
- const handleNameChange = (name: string) => {
1398
- setFormData((p) => ({
1399
- ...p,
1400
- name,
1401
- slug: slugAutoRef.current ? toSlug(name) : p.slug,
1402
- }));
1403
- };
1404
-
1405
- const handleSlugChange = (slug: string) => {
1406
- slugAutoRef.current = false;
1407
- setFormData((p) => ({ ...p, slug }));
1408
- };
1409
-
1410
- const handleTypeChange = (type_id: string) => {
1411
- setFormData((p) => ({ ...p, type_id, provider_id: '', config: {} }));
1412
- };
1413
-
1414
- const handleProviderChange = (provider_id: string) => {
1415
- const provider = formTypeProviders.find(
1416
- (p) => String(p.id) === provider_id
1417
- );
1418
- const initialConfig: Record<string, string | boolean | number | string[]> =
1419
- {};
1420
- if (provider?.slug?.endsWith('-oauth')) {
1421
- const pName = provider.slug.replace(/-oauth$/, '');
1422
- initialConfig.callback_urls = appUrl
1423
- ? [
1424
- `${appUrl}/callback/${pName}/login`,
1425
- `${appUrl}/callback/${pName}/register`,
1426
- `${appUrl}/callback/${pName}/connect`,
1427
- ]
1428
- : [];
1429
- }
1430
- setFormData((p) => ({ ...p, provider_id, config: initialConfig }));
1431
- setVisibleFields({});
1432
- };
1433
-
1434
- const updateConfig = (
1435
- key: string,
1436
- value: string | boolean | number | string[]
1437
- ) => {
1438
- setFormData((p) => ({ ...p, config: { ...p.config, [key]: value } }));
1439
- };
1440
-
1441
- const updateCallbackUrl = (index: number, value: string) => {
1442
- const urls = [...callbackUrls];
1443
- urls[index] = value;
1444
- setFormData((p) => ({
1445
- ...p,
1446
- config: { ...p.config, callback_urls: urls },
1447
- }));
1448
- };
1449
-
1450
- const addCallbackUrl = () => {
1451
- setFormData((p) => ({
1452
- ...p,
1453
- config: { ...p.config, callback_urls: [...callbackUrls, ''] },
1454
- }));
1455
- };
1456
-
1457
- const removeCallbackUrl = (index: number) => {
1458
- setFormData((p) => ({
1459
- ...p,
1460
- config: {
1461
- ...p.config,
1462
- callback_urls: callbackUrls.filter((_, i) => i !== index),
1463
- },
1464
- }));
1465
- };
1466
-
1467
- const handleSubmit = async (event: React.FormEvent) => {
1468
- event.preventDefault();
1469
- if (!formData.type_id || !formData.provider_id) return;
1470
-
1471
- setIsSubmitting(true);
1472
- const payload = {
1473
- slug: formData.slug,
1474
- name: formData.name,
1475
- type_id: Number(formData.type_id),
1476
- provider_id: Number(formData.provider_id),
1477
- config: isOAuthProvider
1478
- ? { ...formData.config, callback_urls: callbackUrls }
1479
- : formData.config,
1480
- is_active: formData.is_active,
1481
- };
1482
-
1483
- try {
1484
- if (editingProfile) {
1485
- await request({
1486
- url: `/integration-profile/${editingProfile.id}`,
1487
- method: 'PATCH',
1488
- data: payload,
1489
- });
1490
- toast.success(t('updateSuccess'));
1491
- } else {
1492
- await request({
1493
- url: '/integration-profile',
1494
- method: 'POST',
1495
- data: payload,
1496
- });
1497
- toast.success(t('createSuccess'));
1498
- }
1499
- setIsSheetOpen(false);
1500
- setRefreshKey((k) => k + 1);
1501
- } catch {
1502
- toast.error(editingProfile ? t('updateError') : t('createError'));
1503
- } finally {
1504
- setIsSubmitting(false);
1505
- }
1506
- };
1507
660
 
1508
661
  return (
1509
662
  <Page>
@@ -1758,420 +911,13 @@ export default function IntegrationProfilesPage() {
1758
911
  </DialogContent>
1759
912
  </Dialog>
1760
913
 
1761
- {/* ── Create / Edit Sheet ── */}
1762
- <Sheet open={isSheetOpen} onOpenChange={setIsSheetOpen}>
1763
- <ResizableSheetContent
1764
- sheetId="integration-profile-form"
1765
- defaultWidth={680}
1766
- >
1767
- <SheetHeader>
1768
- <SheetTitle>
1769
- {editingProfile ? t('editProfile') : t('newProfileTitle')}
1770
- </SheetTitle>
1771
- <SheetDescription>
1772
- {editingProfile
1773
- ? t('editProfileDescription')
1774
- : t('newProfileDescription')}
1775
- </SheetDescription>
1776
- </SheetHeader>
1777
-
1778
- <form
1779
- onSubmit={handleSubmit}
1780
- className="flex flex-col gap-6 px-3 pb-8 sm:px-4"
1781
- >
1782
- {/* ── Identity ── */}
1783
- <div className="grid gap-4 sm:grid-cols-2">
1784
- <div className="space-y-2">
1785
- <Label htmlFor="ip-name">
1786
- {t('nameLabel')} <span className="text-destructive">*</span>
1787
- </Label>
1788
- <Input
1789
- id="ip-name"
1790
- value={formData.name}
1791
- onChange={(e) => handleNameChange(e.target.value)}
1792
- placeholder={t('namePlaceholder')}
1793
- required
1794
- autoFocus
1795
- />
1796
- </div>
1797
-
1798
- <div className="space-y-2">
1799
- <Label htmlFor="ip-slug">
1800
- {t('slugLabel')} <span className="text-destructive">*</span>
1801
- </Label>
1802
- <Input
1803
- id="ip-slug"
1804
- value={formData.slug}
1805
- onChange={(e) => handleSlugChange(e.target.value)}
1806
- placeholder={t('slugPlaceholder')}
1807
- required
1808
- className="font-mono text-sm"
1809
- />
1810
- </div>
1811
- </div>
1812
-
1813
- {/* ── Type + Provider ── */}
1814
- <div className="grid gap-4 sm:grid-cols-2">
1815
- <div className="space-y-2">
1816
- <Label htmlFor="ip-type">
1817
- {t('typeLabel')} <span className="text-destructive">*</span>
1818
- </Label>
1819
- <Select
1820
- value={formData.type_id}
1821
- onValueChange={handleTypeChange}
1822
- disabled={!!editingProfile}
1823
- >
1824
- <SelectTrigger className="w-full" id="ip-type">
1825
- <SelectValue placeholder={t('typePlaceholder')} />
1826
- </SelectTrigger>
1827
- <SelectContent>
1828
- {types.map((ty) => (
1829
- <SelectItem key={ty.id} value={String(ty.id)}>
1830
- {getLocaleName(
1831
- ty.integration_type_locale,
1832
- currentLocaleCode
1833
- )}
1834
- </SelectItem>
1835
- ))}
1836
- </SelectContent>
1837
- </Select>
1838
- </div>
1839
-
1840
- <div className="space-y-2">
1841
- <Label htmlFor="ip-provider">
1842
- {t('providerLabel')}{' '}
1843
- <span className="text-destructive">*</span>
1844
- </Label>
1845
- <Select
1846
- value={formData.provider_id}
1847
- onValueChange={handleProviderChange}
1848
- disabled={!formData.type_id || !!editingProfile}
1849
- >
1850
- <SelectTrigger className="w-full" id="ip-provider">
1851
- <SelectValue
1852
- placeholder={
1853
- !formData.type_id
1854
- ? t('providerSelectTypFirst')
1855
- : t('providerPlaceholder')
1856
- }
1857
- />
1858
- </SelectTrigger>
1859
- <SelectContent>
1860
- {formTypeProviders.map((prov) => (
1861
- <SelectItem key={prov.id} value={String(prov.id)}>
1862
- <span className="flex items-center gap-2">
1863
- <Avatar className="size-6 border bg-muted/40">
1864
- <AvatarFallback className="text-[10px] font-semibold">
1865
- <IntegrationLogo
1866
- provider={prov.slug}
1867
- size={14}
1868
- decorative
1869
- />
1870
- </AvatarFallback>
1871
- </Avatar>
1872
- {getLocaleName(
1873
- prov.integration_provider_locale,
1874
- currentLocaleCode
1875
- )}
1876
- </span>
1877
- </SelectItem>
1878
- ))}
1879
- </SelectContent>
1880
- </Select>
1881
- </div>
1882
- </div>
1883
-
1884
- {/* ── Dynamic credential fields ── */}
1885
- {providerFields.length > 0 && (
1886
- <div className="rounded-lg border">
1887
- <div className="border-b px-4 py-3">
1888
- <p className="text-sm font-semibold">
1889
- {t('configSectionTitle')}
1890
- </p>
1891
- </div>
1892
- <div className="grid gap-4 p-4 sm:grid-cols-2">
1893
- {providerFields.map((field) => {
1894
- if (field.type === 'boolean') {
1895
- return (
1896
- <div
1897
- key={field.key}
1898
- className="col-span-full flex items-center justify-between rounded-md border px-4 py-3"
1899
- >
1900
- <p className="text-sm font-medium">
1901
- {t(field.labelKey as any)}
1902
- </p>
1903
- <Switch
1904
- checked={Boolean(formData.config[field.key])}
1905
- onCheckedChange={(checked) =>
1906
- updateConfig(field.key, checked)
1907
- }
1908
- />
1909
- </div>
1910
- );
1911
- }
1912
-
1913
- if (field.type === 'select') {
1914
- return (
1915
- <div
1916
- key={field.key}
1917
- className={
1918
- field.colSpan
1919
- ? 'col-span-full space-y-2'
1920
- : 'space-y-2'
1921
- }
1922
- >
1923
- <Label htmlFor={`ip-cfg-${field.key}`}>
1924
- {t(field.labelKey as any)}
1925
- {field.required && (
1926
- <span className="text-destructive"> *</span>
1927
- )}
1928
- </Label>
1929
- <Select
1930
- value={String(formData.config[field.key] ?? '')}
1931
- onValueChange={(value) =>
1932
- updateConfig(field.key, value)
1933
- }
1934
- >
1935
- <SelectTrigger
1936
- id={`ip-cfg-${field.key}`}
1937
- className="w-full"
1938
- >
1939
- <SelectValue placeholder={field.placeholder} />
1940
- </SelectTrigger>
1941
- <SelectContent>
1942
- {field.options?.map((option) => (
1943
- <SelectItem
1944
- key={option.value}
1945
- value={option.value}
1946
- >
1947
- {t(option.labelKey as any)}
1948
- </SelectItem>
1949
- ))}
1950
- </SelectContent>
1951
- </Select>
1952
- </div>
1953
- );
1954
- }
1955
-
1956
- const isPassword = field.type === 'password';
1957
- const isVisible = visibleFields[field.key] ?? false;
1958
- const currentValue = String(
1959
- formData.config[field.key] ?? ''
1960
- );
1961
- // Segredo já armazenado: campo desabilitado mostrando ******** +
1962
- // botão "trocar" que limpa o valor e reabilita para digitar um novo.
1963
- const isLockedSecret =
1964
- isPassword &&
1965
- Boolean(editingProfile) &&
1966
- currentValue === SECRET_MASK;
1967
-
1968
- return (
1969
- <div
1970
- key={field.key}
1971
- className={
1972
- field.colSpan
1973
- ? 'col-span-full space-y-2'
1974
- : 'space-y-2'
1975
- }
1976
- >
1977
- <Label htmlFor={`ip-cfg-${field.key}`}>
1978
- {t(field.labelKey as any)}
1979
- {field.required && (
1980
- <span className="text-destructive"> *</span>
1981
- )}
1982
- </Label>
1983
- <div className={isPassword ? 'relative' : undefined}>
1984
- <Input
1985
- id={`ip-cfg-${field.key}`}
1986
- type={
1987
- isPassword
1988
- ? isVisible && !isLockedSecret
1989
- ? 'text'
1990
- : 'password'
1991
- : field.type === 'number'
1992
- ? 'number'
1993
- : 'text'
1994
- }
1995
- value={currentValue}
1996
- onChange={(e) =>
1997
- updateConfig(field.key, e.target.value)
1998
- }
1999
- placeholder={
2000
- isPassword && editingProfile
2001
- ? t('leaveBlankToKeep')
2002
- : field.placeholder
2003
- }
2004
- required={
2005
- field.required &&
2006
- !(isPassword && Boolean(editingProfile))
2007
- }
2008
- autoComplete={
2009
- isPassword ? 'new-password' : undefined
2010
- }
2011
- className={isPassword ? 'pr-10' : undefined}
2012
- disabled={isLockedSecret}
2013
- />
2014
- {isPassword &&
2015
- (isLockedSecret ? (
2016
- <Button
2017
- type="button"
2018
- variant="ghost"
2019
- size="icon"
2020
- className="absolute right-1 top-1/2 h-8 w-8 -translate-y-1/2"
2021
- onClick={() => {
2022
- updateConfig(field.key, '');
2023
- setVisibleFields((p) => ({
2024
- ...p,
2025
- [field.key]: true,
2026
- }));
2027
- }}
2028
- aria-label={t('changeSecret')}
2029
- title={t('changeSecret')}
2030
- >
2031
- <Pencil className="h-4 w-4" />
2032
- </Button>
2033
- ) : (
2034
- <Button
2035
- type="button"
2036
- variant="ghost"
2037
- size="icon"
2038
- className="absolute right-1 top-1/2 h-8 w-8 -translate-y-1/2"
2039
- onClick={() =>
2040
- setVisibleFields((p) => ({
2041
- ...p,
2042
- [field.key]: !p[field.key],
2043
- }))
2044
- }
2045
- aria-label={
2046
- isVisible
2047
- ? t('hidePassword')
2048
- : t('showPassword')
2049
- }
2050
- >
2051
- {isVisible ? (
2052
- <EyeOff className="h-4 w-4" />
2053
- ) : (
2054
- <Eye className="h-4 w-4" />
2055
- )}
2056
- </Button>
2057
- ))}
2058
- </div>
2059
- </div>
2060
- );
2061
- })}
2062
- </div>
2063
- </div>
2064
- )}
2065
-
2066
- {/* ── DigitalOcean: node pool de vídeo (escolha por nome via API) ── */}
2067
- {selectedProvider?.slug === 'digitalocean' && (
2068
- <DoNodePoolPicker
2069
- apiToken={String(formData.config.api_token ?? '')}
2070
- clusterId={String(formData.config.cluster_id ?? '')}
2071
- value={String(formData.config.video_node_pool_name ?? '')}
2072
- profileId={editingProfile?.id}
2073
- onChange={(name) =>
2074
- updateConfig('video_node_pool_name', name)
2075
- }
2076
- />
2077
- )}
2078
-
2079
- {/* ── Callback URLs (OAuth only) ── */}
2080
- {isOAuthProvider && (
2081
- <div className="rounded-lg border">
2082
- <div className="flex items-center justify-between border-b px-4 py-3">
2083
- <div>
2084
- <p className="text-sm font-semibold">
2085
- {t('callbackUrlsSectionTitle')}
2086
- </p>
2087
- <p className="mt-0.5 text-xs text-muted-foreground">
2088
- {t('callbackUrlsDescription')}
2089
- </p>
2090
- </div>
2091
- <Button
2092
- type="button"
2093
- variant="outline"
2094
- size="sm"
2095
- onClick={addCallbackUrl}
2096
- >
2097
- <Plus className="mr-1.5 h-3.5 w-3.5" />
2098
- {t('addCallbackUrl')}
2099
- </Button>
2100
- </div>
2101
- <div className="space-y-2 p-4">
2102
- {callbackUrls.length === 0 && (
2103
- <p className="py-1 text-center text-sm text-muted-foreground">
2104
- {t('noCallbackUrls')}
2105
- </p>
2106
- )}
2107
- {callbackUrls.map((url, index) => (
2108
- <div key={index} className="flex gap-2">
2109
- <Input
2110
- value={url}
2111
- onChange={(e) =>
2112
- updateCallbackUrl(index, e.target.value)
2113
- }
2114
- placeholder={t('callbackUrlPlaceholder')}
2115
- className="font-mono text-sm"
2116
- />
2117
- <Button
2118
- type="button"
2119
- variant="ghost"
2120
- size="icon"
2121
- className="shrink-0"
2122
- disabled={!url}
2123
- onClick={() => {
2124
- navigator.clipboard.writeText(url);
2125
- setCopiedUrlIndex(index);
2126
- setTimeout(() => setCopiedUrlIndex(null), 1500);
2127
- }}
2128
- >
2129
- {copiedUrlIndex === index ? (
2130
- <Check className="h-4 w-4 text-green-500" />
2131
- ) : (
2132
- <Copy className="h-4 w-4" />
2133
- )}
2134
- </Button>
2135
- <Button
2136
- type="button"
2137
- variant="ghost"
2138
- size="icon"
2139
- onClick={() => removeCallbackUrl(index)}
2140
- className="shrink-0"
2141
- >
2142
- <X className="h-4 w-4" />
2143
- </Button>
2144
- </div>
2145
- ))}
2146
- </div>
2147
- </div>
2148
- )}
2149
-
2150
- {/* ── Active ── */}
2151
- <div className="flex items-center justify-between rounded-md border px-4 py-3">
2152
- <p className="text-sm font-medium">{t('isActiveLabel')}</p>
2153
- <Switch
2154
- checked={formData.is_active}
2155
- onCheckedChange={(checked) =>
2156
- setFormData((p) => ({ ...p, is_active: checked }))
2157
- }
2158
- />
2159
- </div>
2160
-
2161
- <div className="flex justify-end border-t pt-4">
2162
- <Button
2163
- type="submit"
2164
- className="min-w-40"
2165
- disabled={
2166
- isSubmitting || !formData.type_id || !formData.provider_id
2167
- }
2168
- >
2169
- {editingProfile ? t('saveChanges') : t('createProfile')}
2170
- </Button>
2171
- </div>
2172
- </form>
2173
- </ResizableSheetContent>
2174
- </Sheet>
914
+ <IntegrationProfileSheet
915
+ open={isSheetOpen}
916
+ onOpenChange={setIsSheetOpen}
917
+ profileId={editingProfileId}
918
+ initialTypeSlug={activeTab}
919
+ onSaved={() => setRefreshKey((k) => k + 1)}
920
+ />
2175
921
 
2176
922
  {/* ── Delete confirmation ── */}
2177
923
  <AlertDialog