@hed-hog/core 0.0.401 → 0.0.402
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/dist/access-log/access-log.interceptor.d.ts +10 -0
- package/dist/access-log/access-log.interceptor.d.ts.map +1 -1
- package/dist/access-log/access-log.interceptor.js +15 -1
- package/dist/access-log/access-log.interceptor.js.map +1 -1
- package/dist/file/file-upload-ticket.service.d.ts +1 -1
- package/dist/file/file.controller.d.ts +3 -3
- package/dist/file/file.service.d.ts +121 -12
- package/dist/file/file.service.d.ts.map +1 -1
- package/dist/file/file.service.js +245 -3
- package/dist/file/file.service.js.map +1 -1
- package/dist/file/provider/abstract.provider.d.ts +33 -0
- package/dist/file/provider/abstract.provider.d.ts.map +1 -1
- package/dist/file/provider/abstract.provider.js +9 -0
- package/dist/file/provider/abstract.provider.js.map +1 -1
- package/dist/file/provider/s3-multipart.util.d.ts +17 -0
- package/dist/file/provider/s3-multipart.util.d.ts.map +1 -0
- package/dist/file/provider/s3-multipart.util.js +88 -0
- package/dist/file/provider/s3-multipart.util.js.map +1 -0
- package/dist/file/provider/s3.provider.d.ts +9 -1
- package/dist/file/provider/s3.provider.d.ts.map +1 -1
- package/dist/file/provider/s3.provider.js +16 -0
- package/dist/file/provider/s3.provider.js.map +1 -1
- package/dist/file/provider/spaces.provider.d.ts +10 -1
- package/dist/file/provider/spaces.provider.d.ts.map +1 -1
- package/dist/file/provider/spaces.provider.js +17 -0
- package/dist/file/provider/spaces.provider.js.map +1 -1
- package/dist/integration-profile/integration-profile.controller.d.ts +1 -0
- package/dist/integration-profile/integration-profile.controller.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.secrets.js +7 -2
- package/dist/integration-profile/integration-profile.secrets.js.map +1 -1
- package/dist/integration-profile/integration-profile.service.d.ts +1 -0
- package/dist/integration-profile/integration-profile.service.d.ts.map +1 -1
- package/dist/integration-profile/integration-profile.service.js +3 -3
- package/dist/integration-profile/integration-profile.service.js.map +1 -1
- package/dist/integration-profile/testers/http-probe.d.ts.map +1 -1
- package/dist/integration-profile/testers/http-probe.js +5 -3
- package/dist/integration-profile/testers/http-probe.js.map +1 -1
- package/dist/integration-profile/testers/integration-tester.types.d.ts +5 -0
- package/dist/integration-profile/testers/integration-tester.types.d.ts.map +1 -1
- package/dist/mail/mail.service.d.ts.map +1 -1
- package/dist/mail/mail.service.js +16 -1
- package/dist/mail/mail.service.js.map +1 -1
- package/dist/mcp/mcp-server.service.d.ts.map +1 -1
- package/dist/mcp/mcp-server.service.js +10 -1
- package/dist/mcp/mcp-server.service.js.map +1 -1
- package/dist/pdf/document/pdf-document.css.d.ts.map +1 -1
- package/dist/pdf/document/pdf-document.css.js +88 -0
- package/dist/pdf/document/pdf-document.css.js.map +1 -1
- package/dist/pdf/document/pdf-document.renderer.d.ts.map +1 -1
- package/dist/pdf/document/pdf-document.renderer.js +73 -0
- package/dist/pdf/document/pdf-document.renderer.js.map +1 -1
- package/dist/pdf/document/pdf-document.types.d.ts +56 -0
- package/dist/pdf/document/pdf-document.types.d.ts.map +1 -1
- package/dist/pdf/pdf.service.d.ts +6 -0
- package/dist/pdf/pdf.service.d.ts.map +1 -1
- package/dist/pdf/pdf.service.js +3 -0
- package/dist/pdf/pdf.service.js.map +1 -1
- package/dist/setting/setting.service.d.ts.map +1 -1
- package/dist/setting/setting.service.js +3 -0
- package/dist/setting/setting.service.js.map +1 -1
- package/dist/task/task.service.d.ts +6 -0
- package/dist/task/task.service.d.ts.map +1 -1
- package/dist/task/task.service.js +44 -13
- package/dist/task/task.service.js.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts +35 -2
- package/dist/whatsapp-sender/whatsapp-sender.service.d.ts.map +1 -1
- package/dist/whatsapp-sender/whatsapp-sender.service.js +83 -21
- package/dist/whatsapp-sender/whatsapp-sender.service.js.map +1 -1
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.test.tsx.ejs +85 -2
- package/hedhog/frontend/app/configurations/[slug]/components/setting-field.tsx.ejs +576 -491
- package/hedhog/frontend/app/users/simulate-access-dialog.test.tsx.ejs +29 -50
- package/hedhog/frontend/app/users/simulate-access-dialog.tsx.ejs +146 -50
- package/hedhog/frontend/messages/en.json +46 -20
- package/hedhog/frontend/messages/pt.json +46 -20
- package/hedhog/table/file.yaml +3 -1
- package/package.json +4 -4
- package/src/access-log/access-log.interceptor.spec.ts +50 -0
- package/src/access-log/access-log.interceptor.ts +18 -0
- package/src/file/file-direct-upload.spec.ts +344 -0
- package/src/file/file.service.ts +363 -2
- package/src/file/provider/abstract.provider.ts +57 -0
- package/src/file/provider/s3-multipart.spec.ts +123 -0
- package/src/file/provider/s3-multipart.util.ts +126 -0
- package/src/file/provider/s3.provider.ts +65 -1
- package/src/file/provider/spaces.provider.ts +67 -1
- package/src/integration-profile/integration-profile.secrets.ts +7 -2
- package/src/integration-profile/integration-profile.service.ts +1 -0
- package/src/integration-profile/testers/http-probe.ts +5 -3
- package/src/integration-profile/testers/integration-tester.types.ts +5 -0
- package/src/integration-profile/testers/oauth.testers.spec.ts +13 -0
- package/src/integration-profile/testers/tester-coverage.spec.ts +2 -0
- package/src/mail/mail.service.spec.ts +25 -0
- package/src/mail/mail.service.ts +18 -1
- package/src/mcp/mcp-server.service.spec.ts +16 -0
- package/src/mcp/mcp-server.service.ts +10 -1
- package/src/pdf/document/pdf-document.css.ts +88 -0
- package/src/pdf/document/pdf-document.renderer.spec.ts +43 -0
- package/src/pdf/document/pdf-document.renderer.ts +102 -0
- package/src/pdf/document/pdf-document.types.ts +52 -0
- package/src/pdf/pdf.service.ts +11 -0
- package/src/setting/setting.service.ts +3 -0
- package/src/task/task.service.spec.ts +77 -1
- package/src/task/task.service.ts +58 -14
- package/src/whatsapp-sender/whatsapp-sender.service.spec.ts +191 -1
- package/src/whatsapp-sender/whatsapp-sender.service.ts +122 -14
|
@@ -225,6 +225,13 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
|
|
|
225
225
|
const debounceTimerRef = useRef<NodeJS.Timeout | undefined>(undefined);
|
|
226
226
|
const isTextInputRef = useRef<boolean>(false);
|
|
227
227
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
228
|
+
/** Envolve os campos digitados: saber se o foco está neles decide quando salvar. */
|
|
229
|
+
const fieldRef = useRef<HTMLDivElement>(null);
|
|
230
|
+
/** O último valor gravado (o `setting.value` da prop não muda depois de salvar). */
|
|
231
|
+
const savedValueRef = useRef(setting.value);
|
|
232
|
+
/** O valor em edição, para salvar no blur e na desmontagem sem esperar render. */
|
|
233
|
+
const localValueRef = useRef(localValue);
|
|
234
|
+
localValueRef.current = localValue;
|
|
228
235
|
|
|
229
236
|
const isThemeSetting = (slug: string) => {
|
|
230
237
|
return slug.startsWith('theme-') || slug === 'menu-width';
|
|
@@ -245,34 +252,50 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
|
|
|
245
252
|
}
|
|
246
253
|
};
|
|
247
254
|
|
|
248
|
-
|
|
249
|
-
if (!isTextInputRef.current) return;
|
|
250
|
-
|
|
255
|
+
const saveValue = (value: typeof setting.value) => {
|
|
251
256
|
if (debounceTimerRef.current) {
|
|
252
257
|
clearTimeout(debounceTimerRef.current);
|
|
258
|
+
debounceTimerRef.current = undefined;
|
|
253
259
|
}
|
|
260
|
+
if (value === savedValueRef.current) return;
|
|
261
|
+
|
|
262
|
+
const previous = savedValueRef.current;
|
|
263
|
+
savedValueRef.current = value;
|
|
264
|
+
setSettingValue(setting.slug, value)
|
|
265
|
+
.then(async () => {
|
|
266
|
+
showToastHandler('success', t('settingUpdated'));
|
|
267
|
+
|
|
268
|
+
if (['icon-url'].includes(setting.slug)) {
|
|
269
|
+
await revalidateSystemIcon();
|
|
270
|
+
showToastHandler('success', t('iconRevalidated'));
|
|
271
|
+
window.location.reload();
|
|
272
|
+
}
|
|
254
273
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
274
|
+
if (isThemeSetting(setting.slug)) {
|
|
275
|
+
await applyThemeSettings();
|
|
276
|
+
}
|
|
277
|
+
})
|
|
278
|
+
.catch(() => {
|
|
279
|
+
savedValueRef.current = previous;
|
|
280
|
+
showToastHandler('error', t('settingUpdateFailed'));
|
|
281
|
+
});
|
|
282
|
+
};
|
|
260
283
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
284
|
+
/**
|
|
285
|
+
* Salvamento automático dos campos digitados. Enquanto o foco está no campo,
|
|
286
|
+
* nada é gravado: salvar a cada pausa gravava o valor pela metade (digitando
|
|
287
|
+
* "10" GB, o "1" ia para o banco). Grava ao sair do campo, no Enter e ao
|
|
288
|
+
* desmontar (trocar de aba). Mudança sem foco no campo — arquivo enviado,
|
|
289
|
+
* perfil aplicado — segue com o debounce de 500 ms.
|
|
290
|
+
*/
|
|
291
|
+
useEffect(() => {
|
|
292
|
+
if (!isTextInputRef.current) return;
|
|
293
|
+
if (fieldRef.current?.contains(document.activeElement)) return;
|
|
266
294
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
.catch(() => {
|
|
272
|
-
showToastHandler('error', t('settingUpdateFailed'));
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
}, 500);
|
|
295
|
+
if (debounceTimerRef.current) {
|
|
296
|
+
clearTimeout(debounceTimerRef.current);
|
|
297
|
+
}
|
|
298
|
+
debounceTimerRef.current = setTimeout(() => saveValue(localValue), 500);
|
|
276
299
|
|
|
277
300
|
return () => {
|
|
278
301
|
if (debounceTimerRef.current) {
|
|
@@ -281,6 +304,26 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
|
|
|
281
304
|
};
|
|
282
305
|
}, [localValue]);
|
|
283
306
|
|
|
307
|
+
// Desmontar com edição pendente (trocar de aba, sair da tela) grava o valor,
|
|
308
|
+
// em vez de descartá-lo junto com o timer.
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
return () => {
|
|
311
|
+
if (isTextInputRef.current) saveValue(localValueRef.current);
|
|
312
|
+
};
|
|
313
|
+
}, []);
|
|
314
|
+
|
|
315
|
+
const handleFieldBlur = (event: React.FocusEvent<HTMLDivElement>) => {
|
|
316
|
+
const next = event.relatedTarget as Node | null;
|
|
317
|
+
if (next && event.currentTarget.contains(next)) return;
|
|
318
|
+
saveValue(localValueRef.current);
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
const handleFieldKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
322
|
+
if (event.key === 'Enter' && event.target instanceof HTMLInputElement) {
|
|
323
|
+
saveValue(localValueRef.current);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
|
|
284
327
|
// Booleano em vez de `setting` na lista de dependências: o objeto da setting
|
|
285
328
|
// troca de identidade a cada render do pai e dispararia a resolução do rótulo
|
|
286
329
|
// de novo a cada vez.
|
|
@@ -332,7 +375,9 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
|
|
|
332
375
|
|
|
333
376
|
const handleChangeValue = (newValue: string, immediate = false) => {
|
|
334
377
|
setLocalValue(newValue);
|
|
378
|
+
localValueRef.current = newValue;
|
|
335
379
|
if (immediate) {
|
|
380
|
+
savedValueRef.current = newValue;
|
|
336
381
|
setSettingValue(setting.slug, newValue)
|
|
337
382
|
.then(async () => {
|
|
338
383
|
showToastHandler('success', t('settingUpdated'));
|
|
@@ -384,518 +429,558 @@ export const SettingField = ({ setting, className }: SettingFieldProps) => {
|
|
|
384
429
|
};
|
|
385
430
|
|
|
386
431
|
const parsedValue = getParsedValue();
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
className={cn(className, 'w-full min-w-0 bg-background md:min-w-50')}
|
|
409
|
-
/>
|
|
410
|
-
);
|
|
411
|
-
|
|
412
|
-
case SettingComponentEnum.INPUT_DURATION:
|
|
413
|
-
isTextInputRef.current = true; // reaproveita o debounce de 500ms existente
|
|
414
|
-
return (
|
|
415
|
-
<DurationInput
|
|
416
|
-
value={
|
|
417
|
-
typeof parsedValue === 'number'
|
|
418
|
-
? parsedValue
|
|
419
|
-
: parseFloat(parsedValue) || 0
|
|
420
|
-
}
|
|
421
|
-
onChange={(seconds) => handleChangeValue(formatValue(seconds))}
|
|
422
|
-
persistKey={setting.slug}
|
|
423
|
-
className={cn(className, 'w-full min-w-0 bg-background md:min-w-50')}
|
|
424
|
-
/>
|
|
425
|
-
);
|
|
426
|
-
|
|
427
|
-
case SettingComponentEnum.INPUT_BYTES:
|
|
428
|
-
isTextInputRef.current = true; // reaproveita o debounce de 500ms existente
|
|
429
|
-
return (
|
|
430
|
-
<ByteSizeInput
|
|
431
|
-
value={
|
|
432
|
-
typeof parsedValue === 'number'
|
|
433
|
-
? parsedValue
|
|
434
|
-
: parseFloat(parsedValue) || 0
|
|
435
|
-
}
|
|
436
|
-
onChange={(bytes) => handleChangeValue(formatValue(bytes))}
|
|
437
|
-
persistKey={setting.slug}
|
|
438
|
-
className={cn(className, 'w-full min-w-0 bg-background md:min-w-50')}
|
|
439
|
-
/>
|
|
440
|
-
);
|
|
441
|
-
|
|
442
|
-
case SettingComponentEnum.INPUT_SECRET:
|
|
443
|
-
isTextInputRef.current = true;
|
|
444
|
-
return (
|
|
445
|
-
<div className="relative w-full min-w-0 md:min-w-50">
|
|
432
|
+
isTextInputRef.current = false;
|
|
433
|
+
const control = renderControl();
|
|
434
|
+
|
|
435
|
+
if (!isTextInputRef.current) return control;
|
|
436
|
+
|
|
437
|
+
return (
|
|
438
|
+
<div
|
|
439
|
+
ref={fieldRef}
|
|
440
|
+
className="contents"
|
|
441
|
+
onBlur={handleFieldBlur}
|
|
442
|
+
onKeyDown={handleFieldKeyDown}
|
|
443
|
+
>
|
|
444
|
+
{control}
|
|
445
|
+
</div>
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
function renderControl() {
|
|
449
|
+
switch (component) {
|
|
450
|
+
case SettingComponentEnum.INPUT_TEXT:
|
|
451
|
+
isTextInputRef.current = true;
|
|
452
|
+
return (
|
|
446
453
|
<Input
|
|
447
|
-
type=
|
|
454
|
+
type="text"
|
|
448
455
|
value={parsedValue || ''}
|
|
449
456
|
onChange={(e) => handleChangeValue(formatValue(e.target.value))}
|
|
450
457
|
className={cn(
|
|
451
458
|
className,
|
|
452
|
-
'w-full min-w-0 bg-background
|
|
459
|
+
'w-full min-w-0 bg-background md:min-w-50'
|
|
453
460
|
)}
|
|
454
461
|
/>
|
|
455
|
-
|
|
456
|
-
type="button"
|
|
457
|
-
variant="ghost"
|
|
458
|
-
size="icon"
|
|
459
|
-
className="absolute right-1 top-1/2 h-8 w-8 -translate-y-1/2"
|
|
460
|
-
onClick={() => setShowSecretValue((prev) => !prev)}
|
|
461
|
-
tooltip={showSecretValue ? t('hidePassword') : t('showPassword')}
|
|
462
|
-
>
|
|
463
|
-
{showSecretValue ? (
|
|
464
|
-
<EyeOff className="h-4 w-4" />
|
|
465
|
-
) : (
|
|
466
|
-
<Eye className="h-4 w-4" />
|
|
467
|
-
)}
|
|
468
|
-
</Button>
|
|
469
|
-
</div>
|
|
470
|
-
);
|
|
471
|
-
|
|
472
|
-
case SettingComponentEnum.INPUT_FILE:
|
|
473
|
-
isTextInputRef.current = true;
|
|
474
|
-
|
|
475
|
-
const handleFileUpload = async (
|
|
476
|
-
e: React.ChangeEvent<HTMLInputElement>
|
|
477
|
-
) => {
|
|
478
|
-
const file = e.target.files?.[0];
|
|
479
|
-
if (!file) return;
|
|
462
|
+
);
|
|
480
463
|
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
if (!fileId) {
|
|
498
|
-
throw new Error(t('fileIdNotReturned'));
|
|
499
|
-
}
|
|
464
|
+
case SettingComponentEnum.INPUT_NUMBER:
|
|
465
|
+
isTextInputRef.current = true;
|
|
466
|
+
return (
|
|
467
|
+
<Input
|
|
468
|
+
type="number"
|
|
469
|
+
value={parsedValue || 0}
|
|
470
|
+
onChange={(e) =>
|
|
471
|
+
handleChangeValue(formatValue(parseFloat(e.target.value) || 0))
|
|
472
|
+
}
|
|
473
|
+
className={cn(
|
|
474
|
+
className,
|
|
475
|
+
'w-full min-w-0 bg-background md:min-w-50'
|
|
476
|
+
)}
|
|
477
|
+
/>
|
|
478
|
+
);
|
|
500
479
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
480
|
+
case SettingComponentEnum.INPUT_DURATION:
|
|
481
|
+
isTextInputRef.current = true; // reaproveita o debounce de 500ms existente
|
|
482
|
+
return (
|
|
483
|
+
<DurationInput
|
|
484
|
+
value={
|
|
485
|
+
typeof parsedValue === 'number'
|
|
486
|
+
? parsedValue
|
|
487
|
+
: parseFloat(parsedValue) || 0
|
|
488
|
+
}
|
|
489
|
+
onChange={(seconds) => handleChangeValue(formatValue(seconds))}
|
|
490
|
+
persistKey={setting.slug}
|
|
491
|
+
className={cn(
|
|
492
|
+
className,
|
|
493
|
+
'w-full min-w-0 bg-background md:min-w-50'
|
|
494
|
+
)}
|
|
495
|
+
/>
|
|
496
|
+
);
|
|
508
497
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
498
|
+
case SettingComponentEnum.INPUT_BYTES:
|
|
499
|
+
isTextInputRef.current = true; // reaproveita o debounce de 500ms existente
|
|
500
|
+
return (
|
|
501
|
+
<ByteSizeInput
|
|
502
|
+
value={
|
|
503
|
+
typeof parsedValue === 'number'
|
|
504
|
+
? parsedValue
|
|
505
|
+
: parseFloat(parsedValue) || 0
|
|
506
|
+
}
|
|
507
|
+
onChange={(bytes) => handleChangeValue(formatValue(bytes))}
|
|
508
|
+
persistKey={setting.slug}
|
|
509
|
+
className={cn(
|
|
510
|
+
className,
|
|
511
|
+
'w-full min-w-0 bg-background md:min-w-50'
|
|
512
|
+
)}
|
|
513
|
+
/>
|
|
514
|
+
);
|
|
512
515
|
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
return (
|
|
526
|
-
<div className="flex w-full min-w-0 flex-col gap-2 md:min-w-50">
|
|
527
|
-
{parsedValue && (
|
|
528
|
-
<div>
|
|
529
|
-
<img
|
|
530
|
-
src={parsedValue || ''}
|
|
531
|
-
alt={t('uploadedFileAlt')}
|
|
532
|
-
className="max-w-full md:w-50"
|
|
533
|
-
/>
|
|
534
|
-
</div>
|
|
535
|
-
)}
|
|
536
|
-
<div className="flex w-full min-w-0 items-center gap-2">
|
|
537
|
-
<input
|
|
538
|
-
ref={fileInputRef}
|
|
539
|
-
type="file"
|
|
540
|
-
onChange={handleFileUpload}
|
|
541
|
-
className="hidden"
|
|
542
|
-
id={`file-${setting.id}`}
|
|
516
|
+
case SettingComponentEnum.INPUT_SECRET:
|
|
517
|
+
isTextInputRef.current = true;
|
|
518
|
+
return (
|
|
519
|
+
<div className="relative w-full min-w-0 md:min-w-50">
|
|
520
|
+
<Input
|
|
521
|
+
type={showSecretValue ? 'text' : 'password'}
|
|
522
|
+
value={parsedValue || ''}
|
|
523
|
+
onChange={(e) => handleChangeValue(formatValue(e.target.value))}
|
|
524
|
+
className={cn(
|
|
525
|
+
className,
|
|
526
|
+
'w-full min-w-0 bg-background pr-10 md:min-w-50'
|
|
527
|
+
)}
|
|
543
528
|
/>
|
|
544
529
|
<Button
|
|
545
530
|
type="button"
|
|
546
|
-
variant="
|
|
531
|
+
variant="ghost"
|
|
547
532
|
size="icon"
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
tooltip={t('uploadFile')}
|
|
533
|
+
className="absolute right-1 top-1/2 h-8 w-8 -translate-y-1/2"
|
|
534
|
+
onClick={() => setShowSecretValue((prev) => !prev)}
|
|
535
|
+
tooltip={showSecretValue ? t('hidePassword') : t('showPassword')}
|
|
552
536
|
>
|
|
553
|
-
|
|
537
|
+
{showSecretValue ? (
|
|
538
|
+
<EyeOff className="h-4 w-4" />
|
|
539
|
+
) : (
|
|
540
|
+
<Eye className="h-4 w-4" />
|
|
541
|
+
)}
|
|
554
542
|
</Button>
|
|
555
|
-
<Input
|
|
556
|
-
type="text"
|
|
557
|
-
value={parsedValue || ''}
|
|
558
|
-
onChange={(e) => handleChangeValue(formatValue(e.target.value))}
|
|
559
|
-
className="min-w-0 flex-1 bg-background"
|
|
560
|
-
placeholder={t('fileUrlPlaceholder')}
|
|
561
|
-
disabled={isUploading}
|
|
562
|
-
/>
|
|
563
543
|
</div>
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
544
|
+
);
|
|
545
|
+
|
|
546
|
+
case SettingComponentEnum.INPUT_FILE:
|
|
547
|
+
isTextInputRef.current = true;
|
|
548
|
+
|
|
549
|
+
const handleFileUpload = async (
|
|
550
|
+
e: React.ChangeEvent<HTMLInputElement>
|
|
551
|
+
) => {
|
|
552
|
+
const file = e.target.files?.[0];
|
|
553
|
+
if (!file) return;
|
|
554
|
+
|
|
555
|
+
setIsUploading(true);
|
|
556
|
+
try {
|
|
557
|
+
const formData = new FormData();
|
|
558
|
+
formData.append('file', file);
|
|
559
|
+
|
|
560
|
+
const response = await request<{ id?: number | string }>({
|
|
561
|
+
url: '/file',
|
|
562
|
+
method: 'POST',
|
|
563
|
+
data: formData,
|
|
564
|
+
headers: {
|
|
565
|
+
'Content-Type': 'multipart/form-data',
|
|
566
|
+
},
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
const fileId = Number(response.data?.id);
|
|
570
|
+
|
|
571
|
+
if (!fileId) {
|
|
572
|
+
throw new Error(t('fileIdNotReturned'));
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// `/file/image/:id` é público, imutável (Cache-Control + ETag) e não
|
|
576
|
+
// expira. A URL de `PUT /file/open/:id`, gravada aqui antes, carrega
|
|
577
|
+
// um JWT de 3600s: a setting virava link morto uma hora depois do
|
|
578
|
+
// upload. Absoluta de propósito — o mesmo valor é lido pelo <img> da
|
|
579
|
+
// sidebar, pelo favicon dinâmico, pelo metadata do layout e pelo
|
|
580
|
+
// e-mail transacional, que roda fora do browser.
|
|
581
|
+
const fileUrl = buildAbsoluteImageUrl(fileId);
|
|
577
582
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
583
|
+
if (!fileUrl) {
|
|
584
|
+
throw new Error(t('apiBaseUrlNotConfigured'));
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
handleChangeValue(formatValue(fileUrl));
|
|
588
|
+
showToastHandler('success', t('fileUploaded'));
|
|
589
|
+
} catch (error) {
|
|
590
|
+
showToastHandler('error', t('fileUploadFailed'));
|
|
591
|
+
} finally {
|
|
592
|
+
setIsUploading(false);
|
|
593
|
+
if (fileInputRef.current) {
|
|
594
|
+
fileInputRef.current.value = '';
|
|
595
|
+
}
|
|
584
596
|
}
|
|
585
|
-
|
|
586
|
-
/>
|
|
587
|
-
);
|
|
597
|
+
};
|
|
588
598
|
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
599
|
+
return (
|
|
600
|
+
<div className="flex w-full min-w-0 flex-col gap-2 md:min-w-50">
|
|
601
|
+
{parsedValue && (
|
|
602
|
+
<div>
|
|
603
|
+
<img
|
|
604
|
+
src={parsedValue || ''}
|
|
605
|
+
alt={t('uploadedFileAlt')}
|
|
606
|
+
className="max-w-full md:w-50"
|
|
607
|
+
/>
|
|
608
|
+
</div>
|
|
609
|
+
)}
|
|
610
|
+
<div className="flex w-full min-w-0 items-center gap-2">
|
|
611
|
+
<input
|
|
612
|
+
ref={fileInputRef}
|
|
613
|
+
type="file"
|
|
614
|
+
onChange={handleFileUpload}
|
|
615
|
+
className="hidden"
|
|
616
|
+
id={`file-${setting.id}`}
|
|
617
|
+
/>
|
|
618
|
+
<Button
|
|
619
|
+
type="button"
|
|
620
|
+
variant="outline"
|
|
621
|
+
size="icon"
|
|
622
|
+
onClick={() => fileInputRef.current?.click()}
|
|
623
|
+
disabled={isUploading}
|
|
624
|
+
className="shrink-0"
|
|
625
|
+
tooltip={t('uploadFile')}
|
|
626
|
+
>
|
|
627
|
+
<Upload className="h-4 w-4" />
|
|
628
|
+
</Button>
|
|
629
|
+
<Input
|
|
630
|
+
type="text"
|
|
631
|
+
value={parsedValue || ''}
|
|
632
|
+
onChange={(e) => handleChangeValue(formatValue(e.target.value))}
|
|
633
|
+
className="min-w-0 flex-1 bg-background"
|
|
634
|
+
placeholder={t('fileUrlPlaceholder')}
|
|
635
|
+
disabled={isUploading}
|
|
636
|
+
/>
|
|
637
|
+
</div>
|
|
638
|
+
</div>
|
|
639
|
+
);
|
|
640
|
+
|
|
641
|
+
case SettingComponentEnum.INPUT_TAGS: {
|
|
642
|
+
const tagsValue: string[] = Array.isArray(parsedValue)
|
|
593
643
|
? parsedValue
|
|
594
644
|
: [];
|
|
595
|
-
|
|
596
645
|
return (
|
|
597
|
-
<
|
|
646
|
+
<TagsInput
|
|
647
|
+
value={tagsValue}
|
|
648
|
+
onChange={(val) => handleChangeValue(formatValue(val), true)}
|
|
598
649
|
className={cn(
|
|
599
|
-
|
|
600
|
-
|
|
650
|
+
className,
|
|
651
|
+
'w-full min-w-0 bg-background md:min-w-50'
|
|
601
652
|
)}
|
|
602
|
-
|
|
603
|
-
{options.map((option) => {
|
|
604
|
-
return (
|
|
605
|
-
<div key={option.id} className="flex items-center space-x-2">
|
|
606
|
-
<Checkbox
|
|
607
|
-
className="bg-background"
|
|
608
|
-
checked={selectedValues.includes(option.value)}
|
|
609
|
-
onCheckedChange={(checked) => {
|
|
610
|
-
let newValues = [...selectedValues];
|
|
611
|
-
if (checked) {
|
|
612
|
-
newValues.push(option.value);
|
|
613
|
-
} else {
|
|
614
|
-
newValues = newValues.filter(
|
|
615
|
-
(val) => val !== option.value
|
|
616
|
-
);
|
|
617
|
-
}
|
|
618
|
-
handleChangeValue(formatValue(newValues), true);
|
|
619
|
-
}}
|
|
620
|
-
/>
|
|
621
|
-
<Label>{option.value}</Label>
|
|
622
|
-
</div>
|
|
623
|
-
);
|
|
624
|
-
})}
|
|
625
|
-
</div>
|
|
653
|
+
/>
|
|
626
654
|
);
|
|
627
655
|
}
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
case SettingComponentEnum.COMBOBOX:
|
|
639
|
-
const options = setting.setting_list || [];
|
|
640
|
-
return (
|
|
641
|
-
<Select
|
|
642
|
-
value={parsedValue?.toString() || ''}
|
|
643
|
-
onValueChange={(val) => handleChangeValue(formatValue(val), true)}
|
|
644
|
-
>
|
|
645
|
-
<SelectTrigger
|
|
646
|
-
className={cn(
|
|
647
|
-
'w-full min-w-0 bg-background md:min-w-50',
|
|
648
|
-
className
|
|
649
|
-
)}
|
|
650
|
-
>
|
|
651
|
-
<SelectValue placeholder={t('selectOption')} />
|
|
652
|
-
</SelectTrigger>
|
|
653
|
-
<SelectContent>
|
|
654
|
-
{options.map((option) => (
|
|
655
|
-
<SelectItem key={option.id} value={option.value}>
|
|
656
|
-
{optionLabel(option.value)}
|
|
657
|
-
</SelectItem>
|
|
658
|
-
))}
|
|
659
|
-
</SelectContent>
|
|
660
|
-
</Select>
|
|
661
|
-
);
|
|
662
|
-
|
|
663
|
-
case SettingComponentEnum.RADIO:
|
|
664
|
-
const radioOptions = setting.setting_list || [];
|
|
665
|
-
return (
|
|
666
|
-
<RadioGroup
|
|
667
|
-
value={parsedValue?.toString() || ''}
|
|
668
|
-
onValueChange={(val) => handleChangeValue(formatValue(val), true)}
|
|
669
|
-
className={cn(className, 'w-full min-w-0 md:min-w-50')}
|
|
670
|
-
>
|
|
671
|
-
{radioOptions.map((option) => (
|
|
672
|
-
<div key={option.id} className="flex items-center space-x-2">
|
|
673
|
-
<RadioGroupItem
|
|
674
|
-
value={option.value}
|
|
675
|
-
id={`radio-${option.id}`}
|
|
676
|
-
className="bg-background"
|
|
677
|
-
/>
|
|
678
|
-
<Label htmlFor={`radio-${option.id}`}>
|
|
679
|
-
{optionLabel(option.value)}
|
|
680
|
-
</Label>
|
|
681
|
-
</div>
|
|
682
|
-
))}
|
|
683
|
-
</RadioGroup>
|
|
684
|
-
);
|
|
685
|
-
|
|
686
|
-
case SettingComponentEnum.COLOR_PICKER:
|
|
687
|
-
isTextInputRef.current = true;
|
|
688
|
-
return (
|
|
689
|
-
<div className="flex w-full min-w-0 items-center gap-2 md:max-w-50">
|
|
690
|
-
<Input
|
|
691
|
-
type="color"
|
|
692
|
-
value={parsedValue || '#000000'}
|
|
693
|
-
onChange={(e) => handleChangeValue(formatValue(e.target.value))}
|
|
694
|
-
className={cn(
|
|
695
|
-
'h-10 w-10 border-none p-0 cursor-pointer',
|
|
696
|
-
className
|
|
697
|
-
)}
|
|
656
|
+
|
|
657
|
+
case SettingComponentEnum.SWITCH:
|
|
658
|
+
return (
|
|
659
|
+
<Switch
|
|
660
|
+
checked={parsedValue || false}
|
|
661
|
+
onCheckedChange={(checked) =>
|
|
662
|
+
handleChangeValue(formatValue(checked), true)
|
|
663
|
+
}
|
|
664
|
+
className={cn('w-8! min-w-8 max-w-8', className)}
|
|
698
665
|
/>
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
666
|
+
);
|
|
667
|
+
|
|
668
|
+
case SettingComponentEnum.CHECKBOX:
|
|
669
|
+
if (setting.type === SettingTypeEnum.ARRAY) {
|
|
670
|
+
const options = setting.setting_list || [];
|
|
671
|
+
const selectedValues: string[] = Array.isArray(parsedValue)
|
|
672
|
+
? parsedValue
|
|
673
|
+
: [];
|
|
674
|
+
|
|
675
|
+
return (
|
|
676
|
+
<div
|
|
677
|
+
className={cn(
|
|
678
|
+
'flex w-full min-w-0 flex-col space-y-2 md:min-w-50',
|
|
679
|
+
className
|
|
680
|
+
)}
|
|
681
|
+
>
|
|
682
|
+
{options.map((option) => {
|
|
683
|
+
return (
|
|
684
|
+
<div key={option.id} className="flex items-center space-x-2">
|
|
685
|
+
<Checkbox
|
|
686
|
+
className="bg-background"
|
|
687
|
+
checked={selectedValues.includes(option.value)}
|
|
688
|
+
onCheckedChange={(checked) => {
|
|
689
|
+
let newValues = [...selectedValues];
|
|
690
|
+
if (checked) {
|
|
691
|
+
newValues.push(option.value);
|
|
692
|
+
} else {
|
|
693
|
+
newValues = newValues.filter(
|
|
694
|
+
(val) => val !== option.value
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
handleChangeValue(formatValue(newValues), true);
|
|
698
|
+
}}
|
|
699
|
+
/>
|
|
700
|
+
<Label>{option.value}</Label>
|
|
701
|
+
</div>
|
|
702
|
+
);
|
|
703
|
+
})}
|
|
704
|
+
</div>
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
return (
|
|
708
|
+
<Checkbox
|
|
709
|
+
checked={parsedValue || false}
|
|
710
|
+
onCheckedChange={(checked) =>
|
|
711
|
+
handleChangeValue(formatValue(checked), true)
|
|
712
|
+
}
|
|
713
|
+
className={cn(className)}
|
|
707
714
|
/>
|
|
708
|
-
|
|
709
|
-
);
|
|
710
|
-
|
|
711
|
-
// Read-only navigation shortcut: takes the user from a module's settings tab
|
|
712
|
-
// to a related screen. Deliberately does NOT set isTextInputRef nor call
|
|
713
|
-
// handleChangeValue — rendering it must never write anything.
|
|
714
|
-
case SettingComponentEnum.LINK: {
|
|
715
|
-
const href = String(parsedValue || '');
|
|
716
|
-
// Internal paths only. A setting value must not become an open redirect.
|
|
717
|
-
const safeHref =
|
|
718
|
-
href.startsWith('/') && !href.startsWith('//') ? href : '#';
|
|
719
|
-
|
|
720
|
-
return (
|
|
721
|
-
<Button asChild variant="outline" size="sm" disabled={safeHref === '#'}>
|
|
722
|
-
<Link href={safeHref}>
|
|
723
|
-
<ExternalLink className="h-4 w-4" />
|
|
724
|
-
{t('openLink')}
|
|
725
|
-
</Link>
|
|
726
|
-
</Button>
|
|
727
|
-
);
|
|
728
|
-
}
|
|
715
|
+
);
|
|
729
716
|
|
|
730
|
-
|
|
731
|
-
|
|
717
|
+
case SettingComponentEnum.COMBOBOX:
|
|
718
|
+
const options = setting.setting_list || [];
|
|
719
|
+
return (
|
|
720
|
+
<Select
|
|
721
|
+
value={parsedValue?.toString() || ''}
|
|
722
|
+
onValueChange={(val) => handleChangeValue(formatValue(val), true)}
|
|
723
|
+
>
|
|
724
|
+
<SelectTrigger
|
|
725
|
+
className={cn(
|
|
726
|
+
'w-full min-w-0 bg-background md:min-w-50',
|
|
727
|
+
className
|
|
728
|
+
)}
|
|
729
|
+
>
|
|
730
|
+
<SelectValue placeholder={t('selectOption')} />
|
|
731
|
+
</SelectTrigger>
|
|
732
|
+
<SelectContent>
|
|
733
|
+
{options.map((option) => (
|
|
734
|
+
<SelectItem key={option.id} value={option.value}>
|
|
735
|
+
{optionLabel(option.value)}
|
|
736
|
+
</SelectItem>
|
|
737
|
+
))}
|
|
738
|
+
</SelectContent>
|
|
739
|
+
</Select>
|
|
740
|
+
);
|
|
741
|
+
|
|
742
|
+
case SettingComponentEnum.RADIO:
|
|
743
|
+
const radioOptions = setting.setting_list || [];
|
|
744
|
+
return (
|
|
745
|
+
<RadioGroup
|
|
746
|
+
value={parsedValue?.toString() || ''}
|
|
747
|
+
onValueChange={(val) => handleChangeValue(formatValue(val), true)}
|
|
748
|
+
className={cn(className, 'w-full min-w-0 md:min-w-50')}
|
|
749
|
+
>
|
|
750
|
+
{radioOptions.map((option) => (
|
|
751
|
+
<div key={option.id} className="flex items-center space-x-2">
|
|
752
|
+
<RadioGroupItem
|
|
753
|
+
value={option.value}
|
|
754
|
+
id={`radio-${option.id}`}
|
|
755
|
+
className="bg-background"
|
|
756
|
+
/>
|
|
757
|
+
<Label htmlFor={`radio-${option.id}`}>
|
|
758
|
+
{optionLabel(option.value)}
|
|
759
|
+
</Label>
|
|
760
|
+
</div>
|
|
761
|
+
))}
|
|
762
|
+
</RadioGroup>
|
|
763
|
+
);
|
|
732
764
|
|
|
733
|
-
|
|
765
|
+
case SettingComponentEnum.COLOR_PICKER:
|
|
734
766
|
isTextInputRef.current = true;
|
|
735
767
|
return (
|
|
736
|
-
<
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
768
|
+
<div className="flex w-full min-w-0 items-center gap-2 md:max-w-50">
|
|
769
|
+
<Input
|
|
770
|
+
type="color"
|
|
771
|
+
value={parsedValue || '#000000'}
|
|
772
|
+
onChange={(e) => handleChangeValue(formatValue(e.target.value))}
|
|
773
|
+
className={cn(
|
|
774
|
+
'h-10 w-10 border-none p-0 cursor-pointer',
|
|
775
|
+
className
|
|
776
|
+
)}
|
|
777
|
+
/>
|
|
778
|
+
<Input
|
|
779
|
+
type="text"
|
|
780
|
+
value={parsedValue || '#000000'}
|
|
781
|
+
onChange={(e) => handleChangeValue(formatValue(e.target.value))}
|
|
782
|
+
className="flex-1 bg-background"
|
|
783
|
+
placeholder="#000000"
|
|
784
|
+
maxLength={7}
|
|
785
|
+
minLength={7}
|
|
786
|
+
/>
|
|
787
|
+
</div>
|
|
788
|
+
);
|
|
789
|
+
|
|
790
|
+
// Read-only navigation shortcut: takes the user from a module's settings tab
|
|
791
|
+
// to a related screen. Deliberately does NOT set isTextInputRef nor call
|
|
792
|
+
// handleChangeValue — rendering it must never write anything.
|
|
793
|
+
case SettingComponentEnum.LINK: {
|
|
794
|
+
const href = String(parsedValue || '');
|
|
795
|
+
// Internal paths only. A setting value must not become an open redirect.
|
|
796
|
+
const safeHref =
|
|
797
|
+
href.startsWith('/') && !href.startsWith('//') ? href : '#';
|
|
798
|
+
|
|
799
|
+
return (
|
|
800
|
+
<Button
|
|
801
|
+
asChild
|
|
802
|
+
variant="outline"
|
|
803
|
+
size="sm"
|
|
804
|
+
disabled={safeHref === '#'}
|
|
805
|
+
>
|
|
806
|
+
<Link href={safeHref}>
|
|
807
|
+
<ExternalLink className="h-4 w-4" />
|
|
808
|
+
{t('openLink')}
|
|
809
|
+
</Link>
|
|
810
|
+
</Button>
|
|
742
811
|
);
|
|
743
812
|
}
|
|
744
813
|
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
: t(`${profileTypeKey}.name`);
|
|
757
|
-
|
|
758
|
-
// O picker exibe o nome do perfil, mas a setting guarda o slug.
|
|
759
|
-
const applyProfile = (
|
|
760
|
-
profile: {
|
|
761
|
-
id?: number | null;
|
|
762
|
-
slug?: string | null;
|
|
763
|
-
name?: string | null;
|
|
764
|
-
} | null
|
|
765
|
-
) => {
|
|
766
|
-
setStorageProfileSelectedLabel(profile?.name ?? '');
|
|
767
|
-
setResolvedProfileId(profile?.id ?? null);
|
|
768
|
-
handleChangeValue(profile?.slug ? String(profile.slug) : '', true);
|
|
769
|
-
};
|
|
770
|
-
|
|
771
|
-
return (
|
|
772
|
-
<>
|
|
773
|
-
<div className="flex w-full min-w-0 items-center gap-2 md:max-w-72">
|
|
774
|
-
<EntityPicker<IntegrationProfileOption & Record<string, unknown>>
|
|
775
|
-
placeholder={t(`${profileTypeKey}.placeholder`)}
|
|
776
|
-
emptyStateDescription={t(`${profileTypeKey}.emptyState`)}
|
|
777
|
-
value={parsedValue || null}
|
|
778
|
-
onChange={(slug, option) =>
|
|
779
|
-
applyProfile(
|
|
780
|
-
option
|
|
781
|
-
? {
|
|
782
|
-
id: Number((option as { id?: number }).id) || null,
|
|
783
|
-
slug: slug != null ? String(slug) : '',
|
|
784
|
-
name: option.name ?? '',
|
|
785
|
-
}
|
|
786
|
-
: null
|
|
787
|
-
)
|
|
788
|
-
}
|
|
789
|
-
valueType="string"
|
|
790
|
-
initialSelectedLabel={storageProfileSelectedLabel}
|
|
791
|
-
showCreateButton={false}
|
|
792
|
-
clearable={false}
|
|
793
|
-
loadOptions={async ({ page, pageSize, search }) => {
|
|
794
|
-
const qs = new URLSearchParams({
|
|
795
|
-
page: String(page),
|
|
796
|
-
pageSize: String(pageSize),
|
|
797
|
-
});
|
|
798
|
-
if (typeSlug) qs.set('typeSlug', typeSlug);
|
|
799
|
-
if (providerSlug) qs.set('providerSlug', providerSlug);
|
|
800
|
-
if (search) qs.set('search', search);
|
|
801
|
-
|
|
802
|
-
const response = await request<
|
|
803
|
-
ApiPaginated<
|
|
804
|
-
IntegrationProfileOption & Record<string, unknown>
|
|
805
|
-
>
|
|
806
|
-
>({
|
|
807
|
-
url: `/integration-profile?${qs}`,
|
|
808
|
-
method: 'GET',
|
|
809
|
-
});
|
|
810
|
-
const paged = response.data;
|
|
811
|
-
|
|
812
|
-
return {
|
|
813
|
-
// O endpoint filtra por tipo e por provider; o que sobra
|
|
814
|
-
// (Google Play como gateway do checkout web, por exemplo) sai
|
|
815
|
-
// aqui para não oferecer uma escolha que o backend recusaria.
|
|
816
|
-
items: (paged?.data ?? []).filter(
|
|
817
|
-
(profile) =>
|
|
818
|
-
!excludeProviders?.includes(
|
|
819
|
-
String(profile.integration_provider?.slug ?? '')
|
|
820
|
-
)
|
|
821
|
-
),
|
|
822
|
-
hasMore: (paged?.page ?? 0) < (paged?.lastPage ?? 0),
|
|
823
|
-
};
|
|
824
|
-
}}
|
|
825
|
-
getOptionValue={(opt) => opt.slug}
|
|
826
|
-
getOptionLabel={(opt) => String(opt.name ?? '')}
|
|
827
|
-
getOptionDescription={(opt) => {
|
|
828
|
-
const optionProviderSlug = String(
|
|
829
|
-
opt.integration_provider?.slug ?? ''
|
|
830
|
-
);
|
|
831
|
-
const providerName = getLocalizedIntegrationName(
|
|
832
|
-
opt.integration_provider?.integration_provider_locale,
|
|
833
|
-
currentLocaleCode
|
|
834
|
-
);
|
|
835
|
-
return [providerName || optionProviderSlug, opt.slug]
|
|
836
|
-
.filter(Boolean)
|
|
837
|
-
.join(' • ');
|
|
838
|
-
}}
|
|
839
|
-
className={cn('flex-1 min-w-0', className)}
|
|
814
|
+
case SettingComponentEnum.ENTITY_PICKER: {
|
|
815
|
+
const profileConfig = getIntegrationProfileSettingConfig(setting);
|
|
816
|
+
|
|
817
|
+
if (!profileConfig) {
|
|
818
|
+
isTextInputRef.current = true;
|
|
819
|
+
return (
|
|
820
|
+
<Input
|
|
821
|
+
type="text"
|
|
822
|
+
value={parsedValue || ''}
|
|
823
|
+
onChange={(e) => handleChangeValue(formatValue(e.target.value))}
|
|
824
|
+
className={cn(className, 'bg-background')}
|
|
840
825
|
/>
|
|
826
|
+
);
|
|
827
|
+
}
|
|
841
828
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
829
|
+
const { typeSlug, providerSlug, excludeProviders } = profileConfig;
|
|
830
|
+
// Um tipo sem tradução própria (ou sem filtro nenhum) cai no genérico.
|
|
831
|
+
const profileTypeKey =
|
|
832
|
+
typeSlug && t.has(`profileTypes.${typeSlug}.placeholder`)
|
|
833
|
+
? `profileTypes.${typeSlug}`
|
|
834
|
+
: GENERIC_PROFILE_TYPE_KEY;
|
|
835
|
+
// Provider com nome próprio (Kubernetes) manda no rótulo dos botões;
|
|
836
|
+
// sem ele, o substantivo é o do tipo.
|
|
837
|
+
const profileName =
|
|
838
|
+
providerSlug && t.has(`profileProviders.${providerSlug}`)
|
|
839
|
+
? t(`profileProviders.${providerSlug}`)
|
|
840
|
+
: t(`${profileTypeKey}.name`);
|
|
841
|
+
|
|
842
|
+
// O picker exibe o nome do perfil, mas a setting guarda o slug.
|
|
843
|
+
const applyProfile = (
|
|
844
|
+
profile: {
|
|
845
|
+
id?: number | null;
|
|
846
|
+
slug?: string | null;
|
|
847
|
+
name?: string | null;
|
|
848
|
+
} | null
|
|
849
|
+
) => {
|
|
850
|
+
setStorageProfileSelectedLabel(profile?.name ?? '');
|
|
851
|
+
setResolvedProfileId(profile?.id ?? null);
|
|
852
|
+
handleChangeValue(profile?.slug ? String(profile.slug) : '', true);
|
|
853
|
+
};
|
|
856
854
|
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
855
|
+
return (
|
|
856
|
+
<>
|
|
857
|
+
<div className="flex w-full min-w-0 items-center gap-2 md:max-w-72">
|
|
858
|
+
<EntityPicker<IntegrationProfileOption & Record<string, unknown>>
|
|
859
|
+
placeholder={t(`${profileTypeKey}.placeholder`)}
|
|
860
|
+
emptyStateDescription={t(`${profileTypeKey}.emptyState`)}
|
|
861
|
+
value={parsedValue || null}
|
|
862
|
+
onChange={(slug, option) =>
|
|
863
|
+
applyProfile(
|
|
864
|
+
option
|
|
865
|
+
? {
|
|
866
|
+
id: Number((option as { id?: number }).id) || null,
|
|
867
|
+
slug: slug != null ? String(slug) : '',
|
|
868
|
+
name: option.name ?? '',
|
|
869
|
+
}
|
|
870
|
+
: null
|
|
871
|
+
)
|
|
872
|
+
}
|
|
873
|
+
valueType="string"
|
|
874
|
+
initialSelectedLabel={storageProfileSelectedLabel}
|
|
875
|
+
showCreateButton={false}
|
|
876
|
+
clearable={false}
|
|
877
|
+
loadOptions={async ({ page, pageSize, search }) => {
|
|
878
|
+
const qs = new URLSearchParams({
|
|
879
|
+
page: String(page),
|
|
880
|
+
pageSize: String(pageSize),
|
|
881
|
+
});
|
|
882
|
+
if (typeSlug) qs.set('typeSlug', typeSlug);
|
|
883
|
+
if (providerSlug) qs.set('providerSlug', providerSlug);
|
|
884
|
+
if (search) qs.set('search', search);
|
|
885
|
+
|
|
886
|
+
const response = await request<
|
|
887
|
+
ApiPaginated<
|
|
888
|
+
IntegrationProfileOption & Record<string, unknown>
|
|
889
|
+
>
|
|
890
|
+
>({
|
|
891
|
+
url: `/integration-profile?${qs}`,
|
|
892
|
+
method: 'GET',
|
|
893
|
+
});
|
|
894
|
+
const paged = response.data;
|
|
895
|
+
|
|
896
|
+
return {
|
|
897
|
+
// O endpoint filtra por tipo e por provider; o que sobra
|
|
898
|
+
// (Google Play como gateway do checkout web, por exemplo) sai
|
|
899
|
+
// aqui para não oferecer uma escolha que o backend recusaria.
|
|
900
|
+
items: (paged?.data ?? []).filter(
|
|
901
|
+
(profile) =>
|
|
902
|
+
!excludeProviders?.includes(
|
|
903
|
+
String(profile.integration_provider?.slug ?? '')
|
|
904
|
+
)
|
|
905
|
+
),
|
|
906
|
+
hasMore: (paged?.page ?? 0) < (paged?.lastPage ?? 0),
|
|
907
|
+
};
|
|
908
|
+
}}
|
|
909
|
+
getOptionValue={(opt) => opt.slug}
|
|
910
|
+
getOptionLabel={(opt) => String(opt.name ?? '')}
|
|
911
|
+
getOptionDescription={(opt) => {
|
|
912
|
+
const optionProviderSlug = String(
|
|
913
|
+
opt.integration_provider?.slug ?? ''
|
|
914
|
+
);
|
|
915
|
+
const providerName = getLocalizedIntegrationName(
|
|
916
|
+
opt.integration_provider?.integration_provider_locale,
|
|
917
|
+
currentLocaleCode
|
|
918
|
+
);
|
|
919
|
+
return [providerName || optionProviderSlug, opt.slug]
|
|
920
|
+
.filter(Boolean)
|
|
921
|
+
.join(' • ');
|
|
922
|
+
}}
|
|
923
|
+
className={cn('flex-1 min-w-0', className)}
|
|
924
|
+
/>
|
|
925
|
+
|
|
926
|
+
<Button
|
|
927
|
+
type="button"
|
|
928
|
+
variant="outline"
|
|
929
|
+
size="icon"
|
|
930
|
+
className="h-10 w-10 shrink-0"
|
|
931
|
+
onClick={() => {
|
|
932
|
+
setStorageProfileEditingId(resolvedProfileId);
|
|
933
|
+
setIsStorageProfileSheetOpen(true);
|
|
934
|
+
}}
|
|
935
|
+
disabled={!resolvedProfileId}
|
|
936
|
+
tooltip={t('editProfileAria', { profile: profileName })}
|
|
937
|
+
>
|
|
938
|
+
<Pencil className="h-4 w-4" />
|
|
939
|
+
</Button>
|
|
940
|
+
|
|
941
|
+
<Button
|
|
942
|
+
type="button"
|
|
943
|
+
variant="outline"
|
|
944
|
+
size="icon"
|
|
945
|
+
className="h-10 w-10 shrink-0"
|
|
946
|
+
onClick={() => {
|
|
947
|
+
setStorageProfileEditingId(null);
|
|
948
|
+
setIsStorageProfileSheetOpen(true);
|
|
949
|
+
}}
|
|
950
|
+
tooltip={t('createProfileAria', { profile: profileName })}
|
|
951
|
+
>
|
|
952
|
+
<Plus className="h-4 w-4" />
|
|
953
|
+
</Button>
|
|
954
|
+
</div>
|
|
955
|
+
|
|
956
|
+
<IntegrationProfileSheet
|
|
957
|
+
open={isStorageProfileSheetOpen}
|
|
958
|
+
onOpenChange={(nextOpen) => {
|
|
959
|
+
setIsStorageProfileSheetOpen(nextOpen);
|
|
960
|
+
if (!nextOpen) setStorageProfileEditingId(null);
|
|
865
961
|
}}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
962
|
+
profileId={storageProfileEditingId}
|
|
963
|
+
initialTypeSlug={typeSlug}
|
|
964
|
+
lockedTypeSlug={typeSlug}
|
|
965
|
+
lockedProviderSlug={providerSlug}
|
|
966
|
+
excludeProviderSlugs={excludeProviders}
|
|
967
|
+
onSaved={(profile: IntegrationProfileSheetSavedProfile) =>
|
|
968
|
+
applyProfile(profile)
|
|
969
|
+
}
|
|
970
|
+
/>
|
|
971
|
+
</>
|
|
972
|
+
);
|
|
973
|
+
}
|
|
871
974
|
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
}
|
|
878
|
-
|
|
879
|
-
initialTypeSlug={typeSlug}
|
|
880
|
-
lockedTypeSlug={typeSlug}
|
|
881
|
-
lockedProviderSlug={providerSlug}
|
|
882
|
-
excludeProviderSlugs={excludeProviders}
|
|
883
|
-
onSaved={(profile: IntegrationProfileSheetSavedProfile) =>
|
|
884
|
-
applyProfile(profile)
|
|
885
|
-
}
|
|
975
|
+
default:
|
|
976
|
+
return (
|
|
977
|
+
<Input
|
|
978
|
+
type="text"
|
|
979
|
+
value={parsedValue || ''}
|
|
980
|
+
onChange={(e) => handleChangeValue(formatValue(e.target.value))}
|
|
981
|
+
className={cn(className, 'bg-background')}
|
|
886
982
|
/>
|
|
887
|
-
|
|
888
|
-
);
|
|
983
|
+
);
|
|
889
984
|
}
|
|
890
|
-
|
|
891
|
-
default:
|
|
892
|
-
return (
|
|
893
|
-
<Input
|
|
894
|
-
type="text"
|
|
895
|
-
value={parsedValue || ''}
|
|
896
|
-
onChange={(e) => handleChangeValue(formatValue(e.target.value))}
|
|
897
|
-
className={cn(className, 'bg-background')}
|
|
898
|
-
/>
|
|
899
|
-
);
|
|
900
985
|
}
|
|
901
986
|
};
|