@praxisui/files-upload 3.0.0-beta.7 → 3.0.0-beta.8

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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, inject, signal, Inject, Component, InjectionToken, Optional, EventEmitter, ViewChild, Output, Input, ChangeDetectionStrategy, ENVIRONMENT_INITIALIZER } from '@angular/core';
2
+ import { Injectable, inject, signal, InjectionToken, Inject, Component, Optional, EventEmitter, ViewChild, Output, Input, ChangeDetectionStrategy, ENVIRONMENT_INITIALIZER } from '@angular/core';
3
3
  import { ActivatedRoute } from '@angular/router';
4
4
  import * as i9 from '@angular/common';
5
5
  import { CommonModule } from '@angular/common';
@@ -16,7 +16,7 @@ import { MatMenuModule } from '@angular/material/menu';
16
16
  import * as i1 from '@angular/common/http';
17
17
  import { HttpHeaders, HttpEventType, HttpResponse, HttpErrorResponse } from '@angular/common/http';
18
18
  import * as i1$2 from '@praxisui/core';
19
- import { providePraxisI18n, ComponentKeyService, ASYNC_CONFIG_STORAGE, PraxisIconDirective, PraxisI18nService, ComponentMetadataRegistry } from '@praxisui/core';
19
+ import { providePraxisI18n, PraxisI18nService, ComponentKeyService, ASYNC_CONFIG_STORAGE, PraxisIconDirective, ComponentMetadataRegistry } from '@praxisui/core';
20
20
  import * as i4 from '@angular/material/tabs';
21
21
  import { MatTabsModule } from '@angular/material/tabs';
22
22
  import * as i5 from '@angular/material/form-field';
@@ -393,1212 +393,1197 @@ function getErrorMeta(code) {
393
393
  return DEFAULT_CATALOG[code] ?? { title: code };
394
394
  }
395
395
 
396
- class PraxisFilesUploadConfigEditor {
397
- fb;
398
- panelData;
399
- snackBar;
400
- destroyRef;
401
- form;
402
- // Integração com backend: baseUrl e sinais de configuração efetiva
403
- baseUrl;
404
- state;
405
- serverLoading = () => (this.state ? this.state.loading() : false);
406
- serverData = () => (this.state ? this.state.data() : undefined);
407
- serverError = () => (this.state ? this.state.error() : undefined);
408
- get uiGroup() {
409
- return this.form.get('ui');
410
- }
411
- get dropzoneGroup() {
412
- return this.form.get('ui').get('dropzone');
413
- }
414
- get listGroup() {
415
- return this.form.get('ui').get('list');
416
- }
417
- get limitsGroup() {
418
- return this.form.get('limits');
419
- }
420
- get optionsGroup() {
421
- return this.form.get('options');
422
- }
423
- get quotasGroup() {
424
- return this.form.get('quotas');
425
- }
426
- get rateLimitGroup() {
427
- return this.form.get('rateLimit');
428
- }
429
- get bulkGroup() {
430
- return this.form.get('bulk');
431
- }
432
- get messagesGroup() {
433
- return this.form.get('messages');
434
- }
435
- get headersGroup() {
436
- return this.form.get('headers');
437
- }
438
- get errorsGroup() {
439
- return this.messagesGroup.get('errors');
440
- }
441
- errorCodes = Object.values(ErrorCode);
442
- errorEntries = [];
443
- isDirty$ = new BehaviorSubject(false);
444
- isValid$;
445
- isBusy$ = new BehaviorSubject(false);
446
- jsonError = null;
447
- constructor(fb, panelData, snackBar, destroyRef) {
448
- this.fb = fb;
449
- this.panelData = panelData;
450
- this.snackBar = snackBar;
451
- this.destroyRef = destroyRef;
452
- this.form = this.fb.group({
453
- strategy: ['direct'],
454
- ui: this.fb.group({
455
- showDropzone: [true],
456
- showProgress: [true],
457
- showConflictPolicySelector: [true],
458
- manualUpload: [false],
459
- dense: [false],
460
- accept: [''],
461
- showMetadataForm: [false],
462
- // Grupos específicos de UI
463
- dropzone: this.fb.group({
464
- expandOnDragProximity: [true],
465
- proximityPx: [64],
466
- expandMode: ['overlay'],
467
- expandHeight: [200],
468
- expandDebounceMs: [120],
469
- }),
470
- list: this.fb.group({
471
- collapseAfter: [5],
472
- detailsMode: ['auto'],
473
- detailsMaxWidth: [480],
474
- detailsShowTechnical: [false],
475
- detailsFields: [''], // CSV na UI
476
- detailsAnchor: ['item'],
477
- }),
478
- }),
479
- limits: this.fb.group({
480
- maxFileSizeBytes: [null],
481
- maxFilesPerBulk: [null],
482
- maxBulkSizeBytes: [null],
483
- }),
484
- options: this.fb.group({
485
- defaultConflictPolicy: ['RENAME'],
486
- strictValidation: [true],
487
- maxUploadSizeMb: [50],
488
- allowedExtensions: [''],
489
- acceptMimeTypes: [''],
490
- targetDirectory: [''],
491
- enableVirusScanning: [false],
492
- }),
493
- bulk: this.fb.group({
494
- failFast: [false],
495
- parallelUploads: [1],
496
- retryCount: [0],
497
- retryBackoffMs: [0],
498
- }),
499
- quotas: this.fb.group({
500
- showQuotaWarnings: [false],
501
- blockOnExceed: [false],
502
- }),
503
- rateLimit: this.fb.group({
504
- autoRetryOn429: [false],
505
- showBannerOn429: [true],
506
- maxAutoRetry: [0],
507
- baseBackoffMs: [0],
508
- }),
509
- headers: this.fb.group({
510
- tenantHeader: ['X-Tenant-Id'],
511
- userHeader: ['X-User-Id'],
512
- // Valores usados nas consultas de configuração do servidor
513
- tenantValue: [''],
514
- userValue: [''],
515
- }),
516
- messages: this.fb.group({
517
- successSingle: [''],
518
- successBulk: [''],
519
- errors: this.fb.group({}),
520
- }),
521
- });
522
- // Inicializa labels mais didáticos para os códigos de erro
523
- const FRIENDLY = {
524
- INVALID_FILE_TYPE: 'Tipo de arquivo inválido',
525
- FILE_TOO_LARGE: 'Arquivo muito grande',
526
- NOT_FOUND: 'Arquivo não encontrado',
527
- UNAUTHORIZED: 'Acesso não autorizado',
528
- RATE_LIMIT_EXCEEDED: 'Limite de requisições excedido',
529
- INTERNAL_ERROR: 'Erro interno do servidor',
530
- QUOTA_EXCEEDED: 'Cota de upload excedida',
531
- SEC_VIRUS_DETECTED: 'Vírus detectado no arquivo',
532
- SEC_MALICIOUS_CONTENT: 'Conteúdo malicioso detectado',
533
- SEC_DANGEROUS_TYPE: 'Tipo de arquivo perigoso',
534
- FMT_MAGIC_MISMATCH: 'Conteúdo do arquivo incompatível',
535
- FMT_CORRUPTED: 'Arquivo corrompido',
536
- FMT_UNSUPPORTED: 'Formato não suportado',
537
- SYS_STORAGE_ERROR: 'Erro no armazenamento',
538
- SYS_SERVICE_DOWN: 'Serviço indisponível',
539
- SYS_RATE_LIMIT: 'Limite de requisições (sistema) excedido',
540
- };
541
- this.errorEntries = this.errorCodes.map((code) => ({
542
- code,
543
- label: FRIENDLY[code] ?? String(code),
544
- }));
545
- const errorsGroup = this.errorsGroup;
546
- this.errorCodes.forEach((code) => {
547
- errorsGroup.addControl(code, this.fb.control(''));
548
- });
549
- this.isValid$ = this.form.statusChanges.pipe(map$1((s) => s === 'VALID'), startWith(this.form.valid));
550
- // Definir baseUrl e inicializar hook em contexto de injeção
551
- this.baseUrl = this.panelData?.baseUrl ?? this.panelData?.__baseUrl;
552
- this.state = useEffectiveUploadConfig(this.baseUrl ?? '/api/files', () => this.getHeadersForFetch());
553
- // Observa mudanças na configuração efetiva do servidor (em contexto de injeção)
554
- toObservable(this.state.data)
555
- .pipe(takeUntilDestroyed(this.destroyRef))
556
- .subscribe((cfg) => this.applyServerConfig(cfg));
557
- }
558
- ngOnInit() {
559
- // baseUrl opcional vinda do componente pai (Settings Panel inputs)
560
- // (já definida no construtor)
561
- if (this.panelData) {
562
- const patch = { ...this.panelData };
563
- if (patch.ui?.accept) {
564
- patch.ui = { ...patch.ui, accept: patch.ui.accept.join(',') };
565
- }
566
- // Normalizar lista de detalhes para CSV
567
- if (patch.ui?.list?.detailsFields) {
568
- patch.ui = {
569
- ...patch.ui,
570
- list: {
571
- ...patch.ui.list,
572
- detailsFields: patch.ui.list.detailsFields.join(','),
573
- },
574
- };
575
- }
576
- if (patch.options?.allowedExtensions) {
577
- patch.options = {
578
- ...patch.options,
579
- allowedExtensions: patch.options.allowedExtensions.join(','),
580
- };
581
- }
582
- if (patch.options?.acceptMimeTypes) {
583
- patch.options = {
584
- ...patch.options,
585
- acceptMimeTypes: patch.options.acceptMimeTypes.join(','),
586
- };
587
- }
588
- this.form.patchValue(patch);
589
- }
590
- // Sempre que cabeçalhos mudarem, atualiza contexto da consulta (e refaz fetch)
591
- this.headersGroup.valueChanges.subscribe(() => {
592
- this.state.refetch();
593
- });
594
- this.form.valueChanges.subscribe(() => this.isDirty$.next(true));
595
- }
596
- applyServerConfig(cfg) {
597
- if (!cfg)
598
- return;
599
- // Configuração efetiva do backend separada em validações locais, opções
600
- // de backend e execução de lote.
601
- const options = cfg.options ?? {};
602
- const bulk = cfg.bulk ?? {};
603
- // Validações locais alimentadas pelo backend
604
- this.limitsGroup.patchValue({
605
- // Preencher máx. arquivos por lote a partir do backend
606
- maxFilesPerBulk: typeof bulk.maxFilesPerBatch === 'number'
607
- ? bulk.maxFilesPerBatch
608
- : null,
609
- }, { emitEvent: false });
610
- // Opções de backend (normalizar arrays em string CSV)
611
- const allowed = Array.isArray(options.allowedExtensions)
612
- ? options.allowedExtensions.join(',')
613
- : '';
614
- const mimes = Array.isArray(options.acceptMimeTypes)
615
- ? options.acceptMimeTypes.join(',')
616
- : '';
617
- this.optionsGroup.patchValue({
618
- defaultConflictPolicy: options.nameConflictPolicy ?? null,
619
- strictValidation: options.strictValidation ?? null,
620
- maxUploadSizeMb: options.maxUploadSizeMb ?? null,
621
- allowedExtensions: allowed,
622
- acceptMimeTypes: mimes,
623
- targetDirectory: options.targetDirectory ?? '',
624
- enableVirusScanning: !!options.enableVirusScanning,
625
- }, { emitEvent: false });
626
- // Execução de lote
627
- this.bulkGroup.patchValue({
628
- failFast: bulk.failFastModeDefault ?? false,
629
- parallelUploads: typeof bulk.maxConcurrentUploads === 'number'
630
- ? bulk.maxConcurrentUploads
631
- : 1,
632
- }, { emitEvent: false });
633
- // rate limit (somente leitura na UI)
634
- this.rateLimitGroup.patchValue({
635
- // Mantemos flags de UI; os números vêm do servidor e são mostrados no resumo
636
- }, { emitEvent: false });
637
- // quotas (somente preferências de UI)
638
- this.quotasGroup.patchValue({}, { emitEvent: false });
639
- // Mensagens de erro do servidor
640
- const serverMessages = cfg.messages ?? {};
641
- const errorsGroup = this.errorsGroup;
642
- const alias = {
643
- INVALID_TYPE: ErrorCode.INVALID_FILE_TYPE,
644
- UNSUPPORTED_FILE_TYPE: ErrorCode.FMT_UNSUPPORTED,
645
- MAGIC_NUMBER_MISMATCH: ErrorCode.FMT_MAGIC_MISMATCH,
646
- CORRUPTED_FILE: ErrorCode.FMT_CORRUPTED,
647
- FILE_TOO_LARGE: ErrorCode.FILE_TOO_LARGE,
648
- RATE_LIMIT_EXCEEDED: ErrorCode.RATE_LIMIT_EXCEEDED,
649
- QUOTA_EXCEEDED: ErrorCode.QUOTA_EXCEEDED,
650
- INTERNAL_ERROR: ErrorCode.INTERNAL_ERROR,
651
- UNKNOWN_ERROR: ErrorCode.INTERNAL_ERROR,
652
- MALWARE_DETECTED: ErrorCode.SEC_MALICIOUS_CONTENT,
653
- VIRUS_DETECTED: ErrorCode.SEC_VIRUS_DETECTED,
654
- DANGEROUS_FILE_TYPE: ErrorCode.SEC_DANGEROUS_TYPE,
655
- DANGEROUS_EXECUTABLE: ErrorCode.SEC_DANGEROUS_TYPE,
656
- FILE_STORE_ERROR: ErrorCode.SYS_STORAGE_ERROR,
657
- SYS_STORAGE_ERROR: ErrorCode.SYS_STORAGE_ERROR,
658
- };
659
- Object.keys(serverMessages).forEach((rawCode) => {
660
- const mapped = alias[rawCode] ?? ErrorCode[rawCode];
661
- const ctrlName = mapped ? String(mapped) : rawCode;
662
- if (!errorsGroup.get(ctrlName)) {
663
- errorsGroup.addControl(ctrlName, this.fb.control(''));
664
- }
665
- if (!this.errorEntries.some((e) => e.code === ctrlName)) {
666
- const meta = getErrorMeta(rawCode);
667
- const label = meta.title || ctrlName;
668
- this.errorEntries.push({ code: ctrlName, label });
669
- }
670
- errorsGroup
671
- .get(ctrlName)
672
- ?.patchValue(serverMessages[rawCode], { emitEvent: false });
673
- });
674
- }
675
- getSettingsValue() {
676
- const value = this.form.value;
677
- // accept (UI)
678
- const accept = this.uiGroup.get('accept')?.value;
679
- if (accept !== undefined) {
680
- value.ui = value.ui ?? {};
681
- value.ui.accept = accept
682
- .split(',')
683
- .map((s) => s.trim())
684
- .filter((s) => !!s);
685
- }
686
- // NOVO: detailsFields (UI)
687
- const df = this.uiGroup.get('list')?.get('detailsFields')
688
- ?.value;
689
- if (df !== undefined) {
690
- value.ui = value.ui ?? {};
691
- value.ui.list = value.ui.list ?? {};
692
- value.ui.list.detailsFields = df
693
- .split(',')
694
- .map((s) => s.trim())
695
- .filter((s) => !!s);
696
- }
697
- // options.allowedExtensions
698
- const allowedExt = this.optionsGroup.get('allowedExtensions')
699
- ?.value;
700
- if (allowedExt !== undefined) {
701
- value.options = value.options ?? {};
702
- value.options.allowedExtensions = allowedExt
703
- .split(',')
704
- .map((s) => s.trim())
705
- .filter((s) => !!s);
706
- }
707
- // options.acceptMimeTypes
708
- const mime = this.optionsGroup.get('acceptMimeTypes')?.value;
709
- if (mime !== undefined) {
710
- value.options = value.options ?? {};
711
- value.options.acceptMimeTypes = mime
712
- .split(',')
713
- .map((s) => s.trim())
714
- .filter((s) => !!s);
715
- }
716
- return value;
717
- }
718
- copyServerConfig() {
719
- const data = this.serverData();
720
- if (!data) {
721
- return;
722
- }
723
- const json = JSON.stringify(data, null, 2);
724
- if (navigator?.clipboard?.writeText) {
725
- navigator.clipboard.writeText(json);
726
- this.snackBar.open('Configuração copiada', undefined, {
727
- duration: 2000,
728
- });
729
- }
730
- }
731
- onJsonChange(json) {
732
- try {
733
- const parsed = JSON.parse(json);
734
- this.form.patchValue(parsed);
735
- this.jsonError = null;
736
- }
737
- catch {
738
- this.jsonError = 'JSON inválido: verifique a sintaxe.';
739
- this.snackBar.open('JSON inválido', undefined, { duration: 2000 });
740
- }
741
- }
742
- getHeadersForFetch() {
743
- const h = this.headersGroup.value;
744
- const headers = {};
745
- if (h?.tenantHeader && h?.tenantValue)
746
- headers[h.tenantHeader] = h.tenantValue;
747
- if (h?.userHeader && h?.userValue)
748
- headers[h.userHeader] = h.userValue;
749
- return headers;
750
- }
751
- refetchServerConfig() {
752
- this.state.refetch();
753
- }
754
- get summaryStrategyTitle() {
755
- const strategy = this.form.get('strategy')?.value;
756
- if (strategy === 'presign')
757
- return 'Upload com URL pré-assinada';
758
- if (strategy === 'auto')
759
- return 'Seleção automática da estratégia';
760
- return 'Upload direto ao backend';
761
- }
762
- get summaryStrategyDetail() {
763
- const strategy = this.form.get('strategy')?.value;
764
- if (strategy === 'presign') {
765
- return 'Depende de endpoint de presign e costuma reduzir carga direta no servidor principal.';
766
- }
767
- if (strategy === 'auto') {
768
- return 'Tenta presign primeiro e volta ao fluxo direto quando necessário.';
769
- }
770
- return 'Fluxo mais simples de integrar e diagnosticar em ambientes controlados.';
771
- }
772
- get summaryExperienceTitle() {
773
- const ui = this.uiGroup.value;
774
- if (ui?.manualUpload)
775
- return 'Revisão manual antes do envio';
776
- if (ui?.showDropzone === false)
777
- return 'Seleção orientada por campo compacto';
778
- return 'Envio imediato com dropzone visível';
779
- }
780
- get summaryExperienceDetail() {
781
- const ui = this.uiGroup.value;
782
- const tokens = [
783
- ui?.showProgress ? 'progresso visível' : 'sem barra de progresso',
784
- ui?.showConflictPolicySelector
785
- ? 'escolha de conflito disponível'
786
- : 'política de conflito escondida',
787
- ui?.showMetadataForm ? 'metadados editáveis na UI' : 'sem formulário extra',
788
- ];
789
- return tokens.join(' ');
790
- }
791
- get summaryLimitsTitle() {
792
- const maxFile = this.optionsGroup.get('maxUploadSizeMb')?.value;
793
- const maxBulk = this.limitsGroup.get('maxFilesPerBulk')?.value;
794
- return `${maxFile || ''} MB por arquivo • ${maxBulk || '—'} itens por lote`;
795
- }
796
- get summaryLimitsDetail() {
797
- const parallel = this.bulkGroup.get('parallelUploads')?.value;
798
- const failFast = this.bulkGroup.get('failFast')?.value;
799
- return `${parallel || 1} upload(s) paralelos • ${failFast ? 'fail-fast ativo' : 'falhas parciais permitidas'}`;
800
- }
801
- get summaryServerTitle() {
802
- const conflict = this.optionsGroup.get('defaultConflictPolicy')?.value || 'RENAME';
803
- return `Conflito padrão: ${conflict}`;
804
- }
805
- get summaryServerDetail() {
806
- const strict = this.optionsGroup.get('strictValidation')?.value;
807
- const virus = this.optionsGroup.get('enableVirusScanning')?.value;
808
- return `${strict ? 'validação rigorosa ligada' : 'validação rígida desativada'} • ${virus ? 'antivírus forçado' : 'antivírus opcional/inativo'}`;
809
- }
810
- get activeRisks() {
811
- const risks = [];
812
- if (this.optionsGroup.get('defaultConflictPolicy')?.value === 'OVERWRITE') {
813
- risks.push({
814
- title: 'Sobrescrita habilitada',
815
- detail: 'Arquivos existentes podem ser substituídos silenciosamente se o backend aceitar a operação.',
816
- });
817
- }
818
- if (this.optionsGroup.get('enableVirusScanning')?.value === true) {
819
- risks.push({
820
- title: 'Antivírus pode elevar latência',
821
- detail: 'Ativar varredura forçada melhora segurança, mas pode impactar throughput e tempo de resposta.',
822
- });
823
- }
824
- if (this.bulkGroup.get('failFast')?.value === true) {
825
- risks.push({
826
- title: 'Lote para no primeiro erro',
827
- detail: 'Usuários podem precisar reenviar itens válidos quando o primeiro erro interromper o restante do lote.',
828
- });
829
- }
830
- if ((this.rateLimitGroup.get('maxAutoRetry')?.value ?? 0) >= 5) {
831
- risks.push({
832
- title: 'Retry automático agressivo',
833
- detail: 'Muitas tentativas automáticas podem aumentar tempo de espera percebido e ruído operacional.',
834
- });
835
- }
836
- if (this.uiGroup.get('manualUpload')?.value === true) {
837
- risks.push({
838
- title: 'Fluxo exige ação manual',
839
- detail: 'Adequado para revisão, mas aumenta passos no envio e pode reduzir taxa de conclusão em cenários simples.',
840
- });
841
- }
842
- return risks;
843
- }
844
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisFilesUploadConfigEditor, deps: [{ token: i1$1.FormBuilder }, { token: SETTINGS_PANEL_DATA }, { token: i2.MatSnackBar }, { token: i0.DestroyRef }], target: i0.ɵɵFactoryTarget.Component });
845
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisFilesUploadConfigEditor, isStandalone: true, selector: "praxis-files-upload-config-editor", ngImport: i0, template: `
846
- <div class="editor-layout">
847
- <aside class="summary-panel" aria-label="Resumo executivo da configuração">
848
- <h3>Resumo executivo</h3>
849
- <p class="summary-intro">
850
- Revise aqui o impacto operacional antes de percorrer todos os grupos
851
- de configuração.
852
- </p>
853
-
854
- <div class="summary-grid">
855
- <section class="summary-card">
856
- <span class="eyebrow">Estratégia</span>
857
- <strong>{{ summaryStrategyTitle }}</strong>
858
- <p>{{ summaryStrategyDetail }}</p>
859
- </section>
860
-
861
- <section class="summary-card">
862
- <span class="eyebrow">Experiência</span>
863
- <strong>{{ summaryExperienceTitle }}</strong>
864
- <p>{{ summaryExperienceDetail }}</p>
865
- </section>
866
-
867
- <section class="summary-card">
868
- <span class="eyebrow">Limites</span>
869
- <strong>{{ summaryLimitsTitle }}</strong>
870
- <p>{{ summaryLimitsDetail }}</p>
871
- </section>
872
-
873
- <section class="summary-card">
874
- <span class="eyebrow">Servidor</span>
875
- <strong>{{ summaryServerTitle }}</strong>
876
- <p>{{ summaryServerDetail }}</p>
877
- </section>
878
- </div>
879
-
880
- <section class="risk-panel" [class.safe]="activeRisks.length === 0">
881
- <h4>
882
- <mat-icon aria-hidden="true">{{
883
- activeRisks.length === 0 ? 'verified' : 'warning'
884
- }}</mat-icon>
885
- Riscos e atenção
886
- </h4>
887
- <div *ngIf="activeRisks.length > 0; else noRiskState" class="risk-list">
888
- <article class="risk-item" *ngFor="let risk of activeRisks">
889
- <strong>{{ risk.title }}</strong>
890
- <p>{{ risk.detail }}</p>
891
- </article>
892
- </div>
893
- <ng-template #noRiskState>
894
- <p class="safe-copy">
895
- Nenhum risco operacional evidente na configuração atual.
896
- </p>
897
- </ng-template>
898
- </section>
899
- </aside>
900
-
901
- <div class="editor-main">
902
- <mat-tab-group>
903
- <mat-tab label="Comportamento">
904
- <div class="tab-panel">
905
- <section class="tab-intro">
906
- <h3>Estratégia e cadência do envio</h3>
907
- <p>
908
- Defina aqui como o usuário inicia o upload e como o lote se
909
- comporta em termos de paralelismo e retentativa.
910
- </p>
911
- </section>
912
- <section class="config-section">
913
- <form [formGroup]="form">
914
- <mat-form-field appearance="fill">
915
- <mat-label>Estratégia de envio <span class="opt-tag">opcional</span></mat-label>
916
- <mat-select formControlName="strategy">
917
- <mat-option value="direct">Direto (HTTP padrão)</mat-option>
918
- <mat-option value="presign">URL pré-assinada (S3/GCS)</mat-option>
919
- <mat-option value="auto"
920
- >Automático (tenta pré-assinada e volta ao direto)</mat-option
921
- >
922
- </mat-select>
923
- <button
924
- mat-icon-button
925
- matSuffix
926
- class="help-icon-button"
927
- type="button"
928
- [matTooltip]="'Como os arquivos serão enviados ao servidor.'"
929
- matTooltipPosition="above"
930
- >
931
- <mat-icon>help_outline</mat-icon>
932
- </button>
933
- </mat-form-field>
934
- </form>
935
- </section>
936
- <section class="config-section">
937
- <form [formGroup]="bulkGroup">
938
- <h4 class="section-subtitle">
939
- <mat-icon aria-hidden="true">build</mat-icon>
940
- Opções configuráveis — Lote
941
- </h4>
942
- <p class="section-note">
943
- Use paralelismo e retries com moderação. Em ambientes corporativos,
944
- esses controles afetam throughput, experiência percebida e carga no
945
- backend.
946
- </p>
947
- <mat-form-field appearance="fill">
948
- <mat-label>Uploads paralelos <span class="opt-tag">opcional</span></mat-label>
949
- <input matInput type="number" formControlName="parallelUploads" />
950
- <button
951
- mat-icon-button
952
- matSuffix
953
- class="help-icon-button"
954
- type="button"
955
- [matTooltip]="'Quantos arquivos enviar ao mesmo tempo.'"
956
- matTooltipPosition="above"
957
- >
958
- <mat-icon>help_outline</mat-icon>
959
- </button>
960
- </mat-form-field>
961
- <mat-form-field appearance="fill">
962
- <mat-label>Número de tentativas <span class="opt-tag">opcional</span></mat-label>
963
- <input matInput type="number" formControlName="retryCount" />
964
- <button
965
- mat-icon-button
966
- matSuffix
967
- class="help-icon-button"
968
- type="button"
969
- [matTooltip]="'Tentativas automáticas em caso de falha.'"
970
- matTooltipPosition="above"
971
- >
972
- <mat-icon>help_outline</mat-icon>
973
- </button>
974
- </mat-form-field>
975
- <mat-form-field appearance="fill">
976
- <mat-label>Intervalo entre tentativas (ms) <span class="opt-tag">opcional</span></mat-label>
977
- <input matInput type="number" formControlName="retryBackoffMs" />
978
- <button
979
- mat-icon-button
980
- matSuffix
981
- class="help-icon-button"
982
- type="button"
983
- [matTooltip]="'Tempo de espera entre tentativas.'"
984
- matTooltipPosition="above"
985
- >
986
- <mat-icon>help_outline</mat-icon>
987
- </button>
988
- </mat-form-field>
989
- </form>
990
- </section>
991
- </div>
992
- </mat-tab>
993
- <mat-tab label="Interface">
994
- <div class="tab-panel">
995
- <section class="tab-intro">
996
- <h3>Experiência visível para o usuário</h3>
997
- <p>
998
- Configure densidade, dropzone, metadados e detalhes da lista.
999
- Priorize clareza operacional antes de habilitar recursos avançados.
1000
- </p>
1001
- </section>
1002
- <section class="config-section">
1003
- <form [formGroup]="uiGroup">
1004
- <h4 class="section-subtitle">
1005
- <mat-icon aria-hidden="true">edit</mat-icon>
1006
- Opções configuráveis — Interface
1007
- </h4>
1008
- <p class="section-note">
1009
- Esta seção controla a ergonomia do componente. Mudanças aqui afetam
1010
- descoberta, esforço de uso e taxa de conclusão do envio.
1011
- </p>
1012
- <mat-checkbox formControlName="showDropzone"
1013
- >Exibir área de soltar</mat-checkbox
1014
- >
1015
- <mat-checkbox formControlName="showProgress"
1016
- >Exibir barra de progresso</mat-checkbox
1017
- >
1018
- <mat-checkbox formControlName="showConflictPolicySelector"
1019
- >Permitir escolher a política de conflito</mat-checkbox
1020
- >
1021
- <mat-checkbox formControlName="manualUpload"
1022
- >Exigir clique em “Enviar” (modo manual)</mat-checkbox
1023
- >
1024
- <mat-checkbox formControlName="dense">Layout compacto</mat-checkbox>
1025
- <mat-form-field appearance="fill">
1026
- <mat-label>Tipos permitidos (accept) <span class="opt-tag">opcional</span></mat-label>
1027
- <input
1028
- matInput
1029
- formControlName="accept"
1030
- placeholder="ex.: pdf,jpg,png"
1031
- />
1032
- <button
1033
- mat-icon-button
1034
- matSuffix
1035
- class="help-icon-button"
1036
- type="button"
1037
- [matTooltip]="'Lista separada por vírgula (opcional).'"
1038
- matTooltipPosition="above"
1039
- >
1040
- <mat-icon>help_outline</mat-icon>
1041
- </button>
1042
- </mat-form-field>
1043
- <mat-checkbox formControlName="showMetadataForm"
1044
- >Exibir formulário de metadados (JSON)</mat-checkbox
1045
- >
1046
-
1047
- <!-- NOVO: Grupo Dropzone -->
1048
- <fieldset [formGroup]="dropzoneGroup" class="subgroup">
1049
- <legend>
1050
- <mat-icon aria-hidden="true">download</mat-icon>
1051
- Dropzone (expansão por proximidade)
1052
- </legend>
1053
- <mat-checkbox formControlName="expandOnDragProximity">
1054
- Expandir ao aproximar arquivo durante arraste
1055
- </mat-checkbox>
1056
- <mat-form-field appearance="fill">
1057
- <mat-label>Raio de proximidade (px) <span class="opt-tag">opcional</span></mat-label>
1058
- <input matInput type="number" formControlName="proximityPx" />
1059
- </mat-form-field>
1060
- <mat-form-field appearance="fill">
1061
- <mat-label>Modo de expansão</mat-label>
1062
- <mat-select formControlName="expandMode">
1063
- <mat-option value="overlay">Overlay (recomendado)</mat-option>
1064
- <mat-option value="inline">Inline</mat-option>
1065
- </mat-select>
1066
- </mat-form-field>
1067
- <mat-form-field appearance="fill">
1068
- <mat-label>Altura do overlay (px) <span class="opt-tag">opcional</span></mat-label>
1069
- <input matInput type="number" formControlName="expandHeight" />
1070
- </mat-form-field>
1071
- <mat-form-field appearance="fill">
1072
- <mat-label>Debounce de arraste (ms) <span class="opt-tag">opcional</span></mat-label>
1073
- <input matInput type="number" formControlName="expandDebounceMs" />
1074
- </mat-form-field>
1075
- </fieldset>
1076
-
1077
- <!-- NOVO: Grupo Lista/Detalhes -->
1078
- <fieldset [formGroup]="listGroup" class="subgroup">
1079
- <legend>
1080
- <mat-icon aria-hidden="true">view_list</mat-icon>
1081
- Lista e detalhes
1082
- </legend>
1083
- <mat-form-field appearance="fill">
1084
- <mat-label>Colapsar após (itens) <span class="opt-tag">opcional</span></mat-label>
1085
- <input matInput type="number" formControlName="collapseAfter" />
1086
- </mat-form-field>
1087
- <mat-form-field appearance="fill">
1088
- <mat-label>Modo de detalhes</mat-label>
1089
- <mat-select formControlName="detailsMode">
1090
- <mat-option value="auto">Automático</mat-option>
1091
- <mat-option value="card">Card (overlay)</mat-option>
1092
- <mat-option value="sidesheet">Side-sheet</mat-option>
1093
- </mat-select>
1094
- </mat-form-field>
1095
- <mat-form-field appearance="fill">
1096
- <mat-label>Largura máxima do card (px) <span class="opt-tag">opcional</span></mat-label>
1097
- <input matInput type="number" formControlName="detailsMaxWidth" />
1098
- </mat-form-field>
1099
- <mat-checkbox formControlName="detailsShowTechnical">
1100
- Mostrar detalhes técnicos por padrão
1101
- </mat-checkbox>
1102
- <mat-form-field appearance="fill">
1103
- <mat-label>Campos de metadados (whitelist) <span class="opt-tag">opcional</span></mat-label>
1104
- <input matInput formControlName="detailsFields" placeholder="ex.: id,fileName,contentType" />
1105
- <button
1106
- mat-icon-button
1107
- matSuffix
1108
- class="help-icon-button"
1109
- type="button"
1110
- [matTooltip]="'Lista separada por vírgula; vazio = todos.'"
1111
- matTooltipPosition="above"
1112
- >
1113
- <mat-icon>help_outline</mat-icon>
1114
- </button>
1115
- </mat-form-field>
1116
- <mat-form-field appearance="fill">
1117
- <mat-label>Âncora do overlay</mat-label>
1118
- <mat-select formControlName="detailsAnchor">
1119
- <mat-option value="item">Item</mat-option>
1120
- <mat-option value="field">Campo</mat-option>
1121
- </mat-select>
1122
- </mat-form-field>
1123
- </fieldset>
1124
- </form>
1125
- </section>
1126
- </div>
1127
- </mat-tab>
1128
- <mat-tab label="Validações">
1129
- <div class="tab-panel">
1130
- <section class="tab-intro">
1131
- <h3>Regras de aceite e comportamento operacional</h3>
1132
- <p>
1133
- Separe mentalmente o que é validação de UX local do que é regra
1134
- efetiva de backend. Revise com atenção as opções destrutivas.
1135
- </p>
1136
- </section>
1137
- <section class="config-section">
1138
- <form [formGroup]="limitsGroup">
1139
- <h4 class="section-subtitle">
1140
- <mat-icon aria-hidden="true">build</mat-icon>
1141
- Opções configuráveis — Validações
1142
- </h4>
1143
- <p class="section-note">
1144
- Limites locais melhoram feedback imediato, mas não substituem a
1145
- política efetiva do servidor.
1146
- </p>
1147
- <mat-form-field appearance="fill">
1148
- <mat-label>Tamanho máximo do arquivo (bytes) <span class="opt-tag">opcional</span></mat-label>
1149
- <input matInput type="number" formControlName="maxFileSizeBytes" />
1150
- <button
1151
- mat-icon-button
1152
- matSuffix
1153
- class="help-icon-button"
1154
- type="button"
1155
- [matTooltip]="'Limite de validação no cliente (opcional).'"
1156
- matTooltipPosition="above"
1157
- >
1158
- <mat-icon>help_outline</mat-icon>
1159
- </button>
1160
- </mat-form-field>
1161
- <mat-form-field appearance="fill">
1162
- <mat-label>Máx. arquivos por lote <span class="opt-tag">opcional</span></mat-label>
1163
- <input matInput type="number" formControlName="maxFilesPerBulk" />
1164
- </mat-form-field>
1165
- <mat-form-field appearance="fill">
1166
- <mat-label>Tamanho máximo do lote (bytes) <span class="opt-tag">opcional</span></mat-label>
1167
- <input matInput type="number" formControlName="maxBulkSizeBytes" />
1168
- </mat-form-field>
1169
- </form>
1170
- </section>
1171
- <section class="config-section">
1172
- <form [formGroup]="optionsGroup">
1173
- <h4 class="section-subtitle">
1174
- <mat-icon aria-hidden="true">tune</mat-icon>
1175
- Opções configuráveis — Backend
1176
- </h4>
1177
- <p class="section-note">
1178
- Estas opções têm impacto direto no contrato com o servidor e em
1179
- governança operacional. Trate alterações aqui como decisão de
1180
- política, não apenas de interface.
1181
- </p>
1182
- <mat-form-field appearance="fill">
1183
- <mat-label>Política de conflito (padrão) <span class="opt-tag">opcional</span></mat-label>
1184
- <mat-select formControlName="defaultConflictPolicy">
1185
- <mat-option value="RENAME">Renomear automaticamente</mat-option>
1186
- <mat-option value="MAKE_UNIQUE">Gerar nome único</mat-option>
1187
- <mat-option value="OVERWRITE">Sobrescrever arquivo existente</mat-option>
1188
- <mat-option value="SKIP">Pular se já existir</mat-option>
1189
- <mat-option value="ERROR">Falhar (erro)</mat-option>
1190
- </mat-select>
1191
- <button
1192
- mat-icon-button
1193
- matSuffix
1194
- class="help-icon-button"
1195
- type="button"
1196
- [matTooltip]="'O que fazer quando o nome do arquivo já existe.'"
1197
- matTooltipPosition="above"
1198
- >
1199
- <mat-icon>help_outline</mat-icon>
1200
- </button>
1201
- <div class="warn" *ngIf="optionsGroup.get('defaultConflictPolicy')?.value === 'OVERWRITE'">
1202
- <mat-icon color="warn" aria-hidden="true">warning</mat-icon>
1203
- Atenção: OVERWRITE pode sobrescrever arquivos existentes.
1204
- </div>
1205
- </mat-form-field>
1206
- <mat-checkbox formControlName="strictValidation"
1207
- >Validação rigorosa (backend)</mat-checkbox
1208
- >
1209
- <mat-form-field appearance="fill">
1210
- <mat-label>Tamanho máx. por arquivo (MB) <span class="req-tag">mandatório</span></mat-label>
1211
- <input matInput type="number" formControlName="maxUploadSizeMb" required />
1212
- <button
1213
- mat-icon-button
1214
- matSuffix
1215
- class="help-icon-button"
1216
- type="button"
1217
- [matTooltip]="'Validado pelo backend (1–500 MB).'"
1218
- matTooltipPosition="above"
1219
- >
1220
- <mat-icon>help_outline</mat-icon>
1221
- </button>
1222
- </mat-form-field>
1223
- <mat-form-field appearance="fill">
1224
- <mat-label>Extensões permitidas <span class="opt-tag">opcional</span></mat-label>
1225
- <input
1226
- matInput
1227
- formControlName="allowedExtensions"
1228
- placeholder="ex.: pdf,docx,xlsx"
1229
- />
1230
- <button
1231
- mat-icon-button
1232
- matSuffix
1233
- class="help-icon-button"
1234
- type="button"
1235
- [matTooltip]="'Lista separada por vírgula (opcional).'"
1236
- matTooltipPosition="above"
1237
- >
1238
- <mat-icon>help_outline</mat-icon>
1239
- </button>
1240
- </mat-form-field>
1241
- <mat-form-field appearance="fill">
1242
- <mat-label>MIME types aceitos <span class="opt-tag">opcional</span></mat-label>
1243
- <input
1244
- matInput
1245
- formControlName="acceptMimeTypes"
1246
- placeholder="ex.: application/pdf,image/png"
1247
- />
1248
- <button
1249
- mat-icon-button
1250
- matSuffix
1251
- class="help-icon-button"
1252
- type="button"
1253
- [matTooltip]="'Lista separada por vírgula (opcional).'"
1254
- matTooltipPosition="above"
1255
- >
1256
- <mat-icon>help_outline</mat-icon>
1257
- </button>
1258
- </mat-form-field>
1259
- <mat-form-field appearance="fill">
1260
- <mat-label>Diretório destino <span class="opt-tag">opcional</span></mat-label>
1261
- <input
1262
- matInput
1263
- formControlName="targetDirectory"
1264
- placeholder="ex.: documentos/notas"
1265
- />
1266
- </mat-form-field>
1267
- <mat-checkbox formControlName="enableVirusScanning">
1268
- Forçar antivírus (quando disponível)
1269
- </mat-checkbox>
1270
- <div class="warn" *ngIf="optionsGroup.get('enableVirusScanning')?.value === true">
1271
- <mat-icon color="warn" aria-hidden="true">warning</mat-icon>
1272
- Pode impactar desempenho e latência de upload.
1273
- </div>
1274
- </form>
1275
- </section>
1276
- <section class="config-section">
1277
- <form [formGroup]="quotasGroup">
1278
- <h4 class="section-subtitle">
1279
- <mat-icon aria-hidden="true">edit</mat-icon>
1280
- Opções configuráveis — Quotas (UI)
1281
- </h4>
1282
- <mat-checkbox formControlName="showQuotaWarnings"
1283
- >Exibir avisos de cota</mat-checkbox
1284
- >
1285
- <mat-checkbox formControlName="blockOnExceed"
1286
- >Bloquear ao exceder cota</mat-checkbox
1287
- >
1288
- </form>
1289
- </section>
1290
- <section class="config-section">
1291
- <form [formGroup]="rateLimitGroup">
1292
- <h4 class="section-subtitle">
1293
- <mat-icon aria-hidden="true">edit</mat-icon>
1294
- Opções configuráveis — Rate Limit (UI)
1295
- </h4>
1296
- <p class="section-note">
1297
- Prefira poucos retries automáticos. Mais tentativas aliviam atrito
1298
- imediato, mas podem mascarar gargalos reais do ambiente.
1299
- </p>
1300
- <mat-checkbox formControlName="showBannerOn429"
1301
- >Exibir banner quando atingir o limite</mat-checkbox
1302
- >
1303
- <mat-checkbox formControlName="autoRetryOn429"
1304
- >Tentar novamente automaticamente</mat-checkbox
1305
- >
1306
- <mat-form-field appearance="fill">
1307
- <mat-label>Máximo de tentativas automáticas <span class="opt-tag">opcional</span></mat-label>
1308
- <input matInput type="number" formControlName="maxAutoRetry" />
1309
- </mat-form-field>
1310
- <mat-form-field appearance="fill">
1311
- <mat-label>Intervalo base entre tentativas (ms) <span class="opt-tag">opcional</span></mat-label>
1312
- <input matInput type="number" formControlName="baseBackoffMs" />
1313
- </mat-form-field>
1314
- </form>
1315
- </section>
1316
- <section class="config-section">
1317
- <form [formGroup]="bulkGroup">
1318
- <h4 class="section-subtitle">
1319
- <mat-icon aria-hidden="true">bolt</mat-icon>
1320
- Opções configuráveis — Execução do lote
1321
- </h4>
1322
- <mat-checkbox formControlName="failFast"
1323
- >Parar no primeiro erro (fail-fast)</mat-checkbox
1324
- >
1325
- </form>
1326
- </section>
1327
- </div>
1328
- </mat-tab>
1329
- <mat-tab label="Mensagens">
1330
- <div class="tab-panel">
1331
- <section class="tab-intro">
1332
- <h3>Mensagens orientadas à operação</h3>
1333
- <p>
1334
- Ajuste o texto exibido ao usuário final. Priorize clareza,
1335
- consistência e linguagem acionável.
1336
- </p>
1337
- </section>
1338
- <section class="config-section">
1339
- <form [formGroup]="messagesGroup">
1340
- <h4 class="section-subtitle">
1341
- <mat-icon aria-hidden="true">edit</mat-icon>
1342
- Opções configuráveis — Mensagens (UI)
1343
- </h4>
1344
- <p class="section-note">
1345
- Mensagens curtas e objetivas funcionam melhor em contextos de alta
1346
- frequência. Use este grupo para adequar a voz da interface ao seu
1347
- ambiente.
1348
- </p>
1349
- <mat-form-field appearance="fill">
1350
- <mat-label>Sucesso (individual) <span class="opt-tag">opcional</span></mat-label>
1351
- <input
1352
- matInput
1353
- formControlName="successSingle"
1354
- placeholder="ex.: Arquivo enviado com sucesso"
1355
- />
1356
- </mat-form-field>
1357
- <mat-form-field appearance="fill">
1358
- <mat-label>Sucesso (em lote) <span class="opt-tag">opcional</span></mat-label>
1359
- <input
1360
- matInput
1361
- formControlName="successBulk"
1362
- placeholder="ex.: Upload concluído"
1363
- />
1364
- </mat-form-field>
1365
- <div [formGroup]="errorsGroup">
1366
- <ng-container *ngFor="let e of errorEntries">
1367
- <mat-form-field appearance="fill">
1368
- <mat-label>{{ e.label }} <span class="opt-tag">opcional</span></mat-label>
1369
- <input matInput [formControlName]="e.code" />
1370
- <mat-hint class="code-hint">{{ e.code }}</mat-hint>
1371
- </mat-form-field>
1372
- </ng-container>
1373
- </div>
1374
- </form>
1375
- </section>
1376
- </div>
1377
- </mat-tab>
1378
- <mat-tab label="Cabeçalhos">
1379
- <div class="tab-panel">
1380
- <section class="tab-intro">
1381
- <h3>Contexto de tenant e usuário</h3>
1382
- <p>
1383
- Use cabeçalhos para consultar configuração efetiva do servidor no
1384
- mesmo contexto que o componente usará em produção.
1385
- </p>
1386
- </section>
1387
- <section class="config-section">
1388
- <form [formGroup]="headersGroup">
1389
- <h4 class="section-subtitle">
1390
- <mat-icon aria-hidden="true">edit</mat-icon>
1391
- Opções configuráveis — Cabeçalhos (consulta)
1392
- </h4>
1393
- <mat-form-field appearance="fill">
1394
- <mat-label>Cabeçalho de tenant <span class="opt-tag">opcional</span></mat-label>
1395
- <input
1396
- matInput
1397
- formControlName="tenantHeader"
1398
- placeholder="X-Tenant-Id"
1399
- />
1400
- </mat-form-field>
1401
- <mat-form-field appearance="fill">
1402
- <mat-label>Valor do tenant <span class="opt-tag">opcional</span></mat-label>
1403
- <input
1404
- matInput
1405
- formControlName="tenantValue"
1406
- placeholder="ex.: demo-tenant"
1407
- />
1408
- </mat-form-field>
1409
- <mat-form-field appearance="fill">
1410
- <mat-label>Cabeçalho de usuário <span class="opt-tag">opcional</span></mat-label>
1411
- <input
1412
- matInput
1413
- formControlName="userHeader"
1414
- placeholder="X-User-Id"
1415
- />
1416
- </mat-form-field>
1417
- <mat-form-field appearance="fill">
1418
- <mat-label>Valor do usuário <span class="opt-tag">opcional</span></mat-label>
1419
- <input matInput formControlName="userValue" placeholder="ex.: 42" />
1420
- </mat-form-field>
1421
- </form>
1422
- </section>
1423
- </div>
1424
- </mat-tab>
1425
- <mat-tab label="Servidor">
1426
- <div class="tab-panel">
1427
- <section class="tab-intro">
1428
- <h3>Contrato efetivo retornado pelo backend</h3>
1429
- <p>
1430
- Esta aba é a fonte de verdade do ambiente ativo. Compare o resumo
1431
- do servidor com o que foi configurado nos formulários anteriores.
1432
- </p>
1433
- </section>
1434
- <section class="config-section">
1435
- <div class="server-tab">
1436
- <div class="toolbar">
1437
- <h4 class="section-subtitle ro">
1438
- <mat-icon aria-hidden="true">info</mat-icon>
1439
- Servidor (somente leitura)
1440
- <span class="badge">read-only</span>
1441
- </h4>
1442
- <button type="button" (click)="refetchServerConfig()">
1443
- Recarregar do servidor
1444
- </button>
1445
- <span class="hint" *ngIf="!baseUrl"
1446
- >Defina a baseUrl no componente pai para consultar
1447
- /api/files/config.</span
1448
- >
1449
- </div>
1450
- <div *ngIf="serverLoading(); else serverLoaded">
1451
- Carregando configuração do servidor…
1452
- </div>
1453
- <ng-template #serverLoaded>
1454
- <div *ngIf="serverError(); else serverOk" class="error">
1455
- Falha ao carregar: {{ serverError() | json }}
1456
- </div>
1457
- <ng-template #serverOk>
1458
- <section *ngIf="serverData() as _">
1459
- <h3>Resumo da configuração ativa</h3>
1460
- <ul class="summary">
1461
- <li>
1462
- <strong>Max por arquivo (MB):</strong>
1463
- {{ serverData()?.options?.maxUploadSizeMb }}
1464
- </li>
1465
- <li>
1466
- <strong>Validação rigorosa:</strong>
1467
- {{ serverData()?.options?.strictValidation }}
1468
- </li>
1469
- <li>
1470
- <strong>Antivírus:</strong>
1471
- {{ serverData()?.options?.enableVirusScanning }}
1472
- </li>
1473
- <li>
1474
- <strong>Conflito de nome (padrão):</strong>
1475
- {{ serverData()?.options?.nameConflictPolicy }}
1476
- </li>
1477
- <li>
1478
- <strong>MIME aceitos:</strong>
1479
- {{
1480
- (serverData()?.options?.acceptMimeTypes || []).join(', ')
1481
- }}
1482
- </li>
1483
- <li>
1484
- <strong>Bulk - fail-fast padrão:</strong>
1485
- {{ serverData()?.bulk?.failFastModeDefault }}
1486
- </li>
1487
- <li>
1488
- <strong>Rate limit:</strong>
1489
- {{ serverData()?.rateLimit?.enabled }} ({{
1490
- serverData()?.rateLimit?.perMinute
1491
- }}/min, {{ serverData()?.rateLimit?.perHour }}/h)
1492
- </li>
1493
- <li>
1494
- <strong>Quotas:</strong> {{ serverData()?.quotas?.enabled }}
1495
- </li>
1496
- <li>
1497
- <strong>Servidor:</strong> v{{
1498
- serverData()?.metadata?.version
1499
- }}
1500
- • {{ serverData()?.metadata?.locale }}
1501
- </li>
1502
- </ul>
1503
- <details>
1504
- <summary>Ver JSON</summary>
1505
- <button
1506
- mat-icon-button
1507
- aria-label="Copiar JSON"
1508
- (click)="copyServerConfig()"
1509
- type="button"
1510
- title="Copiar JSON"
1511
- >
1512
- <mat-icon>content_copy</mat-icon>
1513
- </button>
1514
- <pre>{{ serverData() | json }}</pre>
1515
- </details>
1516
- <p class="note">
1517
- As opções acima que podem ser alteradas via payload são:
1518
- conflito de nome, validação rigorosa, tamanho máximo (MB),
1519
- extensões/MIME aceitos, diretório destino, antivírus,
1520
- metadados personalizados e fail-fast (no bulk).
1521
- </p>
1522
- </section>
1523
- </ng-template>
1524
- </ng-template>
1525
- </div>
1526
- </section>
1527
- </div>
1528
- </mat-tab>
1529
- <mat-tab label="JSON">
1530
- <div class="tab-panel">
1531
- <section class="tab-intro">
1532
- <h3>Modo avançado</h3>
1533
- <p>
1534
- Edite o payload bruto apenas quando precisar de controle fino.
1535
- Alterações aqui exigem mais rigor de revisão e validação.
1536
- </p>
1537
- </section>
1538
- <div class="advanced-note">
1539
- O JSON é uma visão de baixo nível da configuração. Prefira as abas
1540
- guiadas sempre que possível para reduzir erro humano e manter a
1541
- configuração auditável.
1542
- </div>
1543
- <textarea
1544
- class="json-textarea"
1545
- rows="10"
1546
- [ngModel]="form.value | json"
1547
- (ngModelChange)="onJsonChange($event)"
1548
- ></textarea>
1549
- <div class="error" *ngIf="jsonError">{{ jsonError }}</div>
1550
- </div>
1551
- </mat-tab>
1552
- </mat-tab-group>
1553
- </div>
1554
- </div>
1555
- `, isInline: true, styles: [".editor-layout{display:grid;grid-template-columns:minmax(260px,320px) minmax(0,1fr);gap:16px;align-items:start}.summary-panel{position:sticky;top:0;display:flex;flex-direction:column;gap:12px;padding:16px;border:1px solid var(--pfx-surface-border, #d8d8d8);border-radius:16px;background:linear-gradient(180deg,#fafbfc,#fff)}.summary-panel h3{margin:0;font-size:1rem;font-weight:600}.summary-panel .summary-intro{margin:0;color:#000000a6;line-height:1.4;font-size:.88rem}.summary-grid{display:grid;gap:10px}.summary-card{padding:12px;border-radius:12px;background:#fff;border:1px solid #e7e9ee}.summary-card .eyebrow{display:block;margin-bottom:4px;font-size:.72rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:#5f6b7a}.summary-card strong{display:block;margin-bottom:4px;font-size:.95rem}.summary-card p{margin:0;font-size:.85rem;line-height:1.45;color:#000000b8}.risk-panel{padding:12px;border-radius:12px;background:#fff8e8;border:1px solid #f0d29a}.risk-panel.safe{background:#f4fbf6;border-color:#b8ddc1}.risk-panel h4{display:flex;align-items:center;gap:6px;margin:0 0 8px;font-size:.92rem}.risk-list{display:grid;gap:8px}.risk-item{padding:10px;border-radius:10px;background:#ffffffb8}.risk-item strong{display:block;margin-bottom:2px;font-size:.84rem}.risk-item p,.risk-panel .safe-copy{margin:0;font-size:.82rem;line-height:1.4;color:#000000b8}.editor-main{min-width:0}.tab-panel{display:grid;gap:16px;padding-top:8px}.tab-intro{padding:16px 18px;border:1px solid #e5e8ef;border-radius:16px;background:linear-gradient(180deg,#fff,#f7f9fc)}.tab-intro h3{margin:0 0 6px;font-size:1rem;font-weight:600}.tab-intro p{margin:0;font-size:.9rem;line-height:1.5;color:#000000b8}.config-section{padding:18px;border:1px solid #e5e8ef;border-radius:16px;background:#fff}.config-section+.config-section{margin-top:0}.section-note{margin:0 0 16px;color:#000000b3;font-size:.88rem;line-height:1.45}.advanced-note{padding:14px 16px;border-radius:14px;border:1px solid #f0d29a;background:#fff8e8;color:#000000c7;font-size:.88rem;line-height:1.45}.json-textarea{width:100%;min-height:280px;padding:14px 16px;border-radius:14px;border:1px solid #d7dce5;background:#0f172a;color:#e5eefc;font:500 .84rem/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;resize:vertical}.server-tab{display:grid;gap:16px}.toolbar{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.summary{display:grid;gap:8px;padding-left:18px}.summary li{line-height:1.45}details{margin-top:12px;padding:14px 16px;border:1px solid #e5e8ef;border-radius:14px;background:#fbfcfe}details pre{white-space:pre-wrap;word-break:break-word}@media(max-width:1100px){.editor-layout{grid-template-columns:1fr}.summary-panel{position:static}}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i9.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i9.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i4.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i4.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i5.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i7.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i8.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i11.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i10.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i11$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatSnackBarModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: i9.JsonPipe, name: "json" }] });
396
+ const FILES_UPLOAD_EN_US = {
397
+ 'praxis.filesUpload.editor.summary.ariaLabel': 'Executive summary for the current configuration',
398
+ 'praxis.filesUpload.editor.summary.title': 'Executive summary',
399
+ 'praxis.filesUpload.editor.summary.intro': 'Review the operational impact here before going through all configuration groups.',
400
+ 'praxis.filesUpload.editor.summary.strategy': 'Strategy',
401
+ 'praxis.filesUpload.editor.summary.experience': 'Experience',
402
+ 'praxis.filesUpload.editor.summary.limits': 'Limits',
403
+ 'praxis.filesUpload.editor.summary.server': 'Server',
404
+ 'praxis.filesUpload.editor.risks.title': 'Risks and attention points',
405
+ 'praxis.filesUpload.editor.risks.none': 'No evident operational risk in the current configuration.',
406
+ 'praxis.filesUpload.editor.tabs.behavior': 'Behavior',
407
+ 'praxis.filesUpload.editor.tabs.interface': 'Interface',
408
+ 'praxis.filesUpload.editor.tabs.validation': 'Validation',
409
+ 'praxis.filesUpload.editor.tabs.messages': 'Messages',
410
+ 'praxis.filesUpload.editor.tabs.server': 'Server',
411
+ 'praxis.filesUpload.editor.tabs.json': 'JSON',
412
+ 'praxis.filesUpload.editor.behavior.intro.title': 'Upload strategy and cadence',
413
+ 'praxis.filesUpload.editor.behavior.intro.body': 'Define how the user starts the upload and how the batch behaves in terms of parallelism and retries.',
414
+ 'praxis.filesUpload.editor.behavior.strategy.label': 'Upload strategy',
415
+ 'praxis.filesUpload.editor.behavior.strategy.direct': 'Direct (standard HTTP)',
416
+ 'praxis.filesUpload.editor.behavior.strategy.presign': 'Presigned URL (S3/GCS)',
417
+ 'praxis.filesUpload.editor.behavior.strategy.auto': 'Automatic (tries presigned first and falls back to direct)',
418
+ 'praxis.filesUpload.editor.behavior.strategy.hint': 'How files will be sent to the server.',
419
+ 'praxis.filesUpload.editor.behavior.bulk.title': 'Configurable options — Batch',
420
+ 'praxis.filesUpload.editor.behavior.bulk.note': 'Use parallelism and retries in moderation. In enterprise environments these controls affect throughput, perceived experience and backend load.',
421
+ 'praxis.filesUpload.editor.behavior.bulk.parallel': 'Parallel uploads',
422
+ 'praxis.filesUpload.editor.behavior.bulk.parallelHint': 'How many files to send at the same time.',
423
+ 'praxis.filesUpload.editor.behavior.bulk.retryCount': 'Retry count',
424
+ 'praxis.filesUpload.editor.behavior.bulk.retryCountHint': 'Automatic attempts in case of failure.',
425
+ 'praxis.filesUpload.editor.behavior.bulk.retryBackoff': 'Retry interval (ms)',
426
+ 'praxis.filesUpload.editor.behavior.bulk.retryBackoffHint': 'Wait time between attempts.',
427
+ 'praxis.filesUpload.editor.interface.intro.title': 'Visible experience for the user',
428
+ 'praxis.filesUpload.editor.interface.intro.body': 'Configure density, dropzone, metadata and list details. Prioritize operational clarity before enabling advanced capabilities.',
429
+ 'praxis.filesUpload.editor.interface.section.title': 'Configurable options — Interface',
430
+ 'praxis.filesUpload.editor.interface.section.note': 'This section controls the component ergonomics. Changes here affect discoverability, usage effort and upload completion rate.',
431
+ 'praxis.filesUpload.editor.interface.showDropzone': 'Show drop area',
432
+ 'praxis.filesUpload.editor.interface.showProgress': 'Show progress bar',
433
+ 'praxis.filesUpload.editor.interface.showConflictPolicySelector': 'Allow choosing the conflict policy',
434
+ 'praxis.filesUpload.editor.interface.manualUpload': 'Require clicking "Upload" (manual mode)',
435
+ 'praxis.filesUpload.editor.interface.dense': 'Compact layout',
436
+ 'praxis.filesUpload.editor.interface.accept': 'Allowed types (accept)',
437
+ 'praxis.filesUpload.editor.shared.optionalTag': 'optional',
438
+ 'praxis.filesUpload.editor.shared.csvOptional': 'Comma-separated list (optional).',
439
+ 'praxis.filesUpload.editor.placeholders.accept': 'e.g.: pdf,jpg,png',
440
+ 'praxis.filesUpload.editor.placeholders.detailsFields': 'e.g.: id,fileName,contentType',
441
+ 'praxis.filesUpload.editor.placeholders.allowedExtensions': 'e.g.: pdf,docx,xlsx',
442
+ 'praxis.filesUpload.editor.placeholders.acceptMimeTypes': 'e.g.: application/pdf,image/png',
443
+ 'praxis.filesUpload.editor.placeholders.targetDirectory': 'e.g.: documents/invoices',
444
+ 'praxis.filesUpload.editor.placeholders.successSingle': 'e.g.: File uploaded successfully',
445
+ 'praxis.filesUpload.editor.placeholders.successBulk': 'e.g.: Upload completed',
446
+ 'praxis.filesUpload.editor.placeholders.tenantValue': 'e.g.: demo-tenant',
447
+ 'praxis.filesUpload.editor.placeholders.userValue': 'e.g.: 42',
448
+ 'praxis.filesUpload.editor.interface.metadataForm': 'Show metadata form (JSON)',
449
+ 'praxis.filesUpload.editor.interface.dropzone.title': 'Dropzone (proximity expansion)',
450
+ 'praxis.filesUpload.editor.interface.dropzone.expandOnProximity': 'Expand when a file gets close during drag',
451
+ 'praxis.filesUpload.editor.interface.dropzone.proximity': 'Proximity radius (px)',
452
+ 'praxis.filesUpload.editor.interface.dropzone.mode': 'Expansion mode',
453
+ 'praxis.filesUpload.editor.interface.dropzone.mode.overlay': 'Overlay (recommended)',
454
+ 'praxis.filesUpload.editor.interface.dropzone.mode.inline': 'Inline',
455
+ 'praxis.filesUpload.editor.interface.dropzone.height': 'Overlay height (px)',
456
+ 'praxis.filesUpload.editor.interface.dropzone.debounce': 'Drag debounce (ms)',
457
+ 'praxis.filesUpload.editor.interface.list.title': 'List and details',
458
+ 'praxis.filesUpload.editor.interface.list.collapseAfter': 'Collapse after (items)',
459
+ 'praxis.filesUpload.editor.interface.list.detailsMode': 'Details mode',
460
+ 'praxis.filesUpload.editor.interface.list.detailsMode.auto': 'Automatic',
461
+ 'praxis.filesUpload.editor.interface.list.detailsMode.card': 'Card (overlay)',
462
+ 'praxis.filesUpload.editor.interface.list.detailsMode.sidesheet': 'Side-sheet',
463
+ 'praxis.filesUpload.editor.interface.list.detailsWidth': 'Maximum card width (px)',
464
+ 'praxis.filesUpload.editor.interface.list.showTechnical': 'Show technical details by default',
465
+ 'praxis.filesUpload.editor.interface.list.detailsFields': 'Metadata fields (whitelist)',
466
+ 'praxis.filesUpload.editor.interface.list.detailsFieldsHint': 'Comma-separated list; empty = all.',
467
+ 'praxis.filesUpload.editor.interface.list.anchor': 'Overlay anchor',
468
+ 'praxis.filesUpload.editor.interface.list.anchor.item': 'Item',
469
+ 'praxis.filesUpload.editor.interface.list.anchor.field': 'Field',
470
+ 'praxis.filesUpload.editor.validation.intro.title': 'Acceptance rules and operational behavior',
471
+ 'praxis.filesUpload.editor.validation.intro.body': 'Separate local UX validation from effective backend rules. Review destructive options carefully.',
472
+ 'praxis.filesUpload.editor.validation.local.title': 'Configurable options — Validation',
473
+ 'praxis.filesUpload.editor.validation.local.note': 'Local limits improve immediate feedback, but do not replace the effective server policy.',
474
+ 'praxis.filesUpload.editor.validation.local.maxFileSize': 'Maximum file size (bytes)',
475
+ 'praxis.filesUpload.editor.validation.local.maxFileSizeHint': 'Client-side validation limit (optional).',
476
+ 'praxis.filesUpload.editor.validation.local.maxFilesPerBulk': 'Max files per batch',
477
+ 'praxis.filesUpload.editor.validation.local.maxBulkSize': 'Maximum batch size (bytes)',
478
+ 'praxis.filesUpload.editor.validation.backend.title': 'Configurable options — Backend',
479
+ 'praxis.filesUpload.editor.validation.backend.note': 'These options directly impact the server contract and operational governance. Treat changes here as policy decisions, not only UI changes.',
480
+ 'praxis.filesUpload.editor.validation.backend.conflictPolicy': 'Conflict policy (default)',
481
+ 'praxis.filesUpload.editor.validation.backend.conflictPolicy.rename': 'Rename automatically',
482
+ 'praxis.filesUpload.editor.validation.backend.conflictPolicy.unique': 'Generate unique name',
483
+ 'praxis.filesUpload.editor.validation.backend.conflictPolicy.overwrite': 'Overwrite existing file',
484
+ 'praxis.filesUpload.editor.validation.backend.conflictPolicy.skip': 'Skip if already exists',
485
+ 'praxis.filesUpload.editor.validation.backend.conflictPolicy.error': 'Fail (error)',
486
+ 'praxis.filesUpload.editor.validation.backend.conflictPolicyHint': 'What to do when the file name already exists.',
487
+ 'praxis.filesUpload.editor.validation.backend.overwriteWarn': 'Warning: OVERWRITE may replace existing files.',
488
+ 'praxis.filesUpload.editor.validation.backend.strictValidation': 'Strict validation (backend)',
489
+ 'praxis.filesUpload.editor.validation.backend.maxUploadSizeMb': 'Maximum size per file (MB)',
490
+ 'praxis.filesUpload.editor.validation.backend.maxUploadSizeMbHint': 'Validated by the backend (1–500 MB).',
491
+ 'praxis.filesUpload.editor.validation.backend.allowedExtensions': 'Allowed extensions',
492
+ 'praxis.filesUpload.editor.validation.backend.acceptMimeTypes': 'Accepted MIME types',
493
+ 'praxis.filesUpload.editor.validation.backend.targetDirectory': 'Target directory',
494
+ 'praxis.filesUpload.editor.validation.backend.enableVirusScanning': 'Force antivirus (when available)',
495
+ 'praxis.filesUpload.editor.validation.backend.virusWarn': 'May impact upload performance and latency.',
496
+ 'praxis.filesUpload.editor.validation.quotas.title': 'Configurable options — Quotas (UI)',
497
+ 'praxis.filesUpload.editor.validation.quotas.showWarnings': 'Show quota warnings',
498
+ 'praxis.filesUpload.editor.validation.quotas.blockOnExceed': 'Block on quota exceed',
499
+ 'praxis.filesUpload.editor.validation.rateLimit.title': 'Configurable options — Rate Limit (UI)',
500
+ 'praxis.filesUpload.editor.validation.rateLimit.note': 'Prefer a small number of automatic retries. More attempts reduce immediate friction, but may hide real environment bottlenecks.',
501
+ 'praxis.filesUpload.editor.validation.rateLimit.showBanner': 'Show banner when the limit is reached',
502
+ 'praxis.filesUpload.editor.validation.rateLimit.autoRetry': 'Retry automatically',
503
+ 'praxis.filesUpload.editor.validation.rateLimit.maxAutoRetry': 'Maximum automatic retries',
504
+ 'praxis.filesUpload.editor.validation.rateLimit.baseBackoff': 'Base retry interval (ms)',
505
+ 'praxis.filesUpload.editor.validation.batch.title': 'Configurable options — Batch execution',
506
+ 'praxis.filesUpload.editor.validation.batch.failFast': 'Stop on the first error (fail-fast)',
507
+ 'praxis.filesUpload.editor.messages.intro.title': 'Messages oriented to operations',
508
+ 'praxis.filesUpload.editor.messages.intro.body': 'Adjust the text shown to the end user. Prioritize clarity, consistency and actionable language.',
509
+ 'praxis.filesUpload.editor.messages.section.title': 'Configurable options — Messages (UI)',
510
+ 'praxis.filesUpload.editor.messages.section.note': 'Short and objective messages work better in high-frequency contexts. Use this group to adapt the interface voice to your environment.',
511
+ 'praxis.filesUpload.editor.messages.successSingle': 'Success (single)',
512
+ 'praxis.filesUpload.editor.messages.successBulk': 'Success (batch)',
513
+ 'praxis.filesUpload.editor.tabs.headers': 'Headers',
514
+ 'praxis.filesUpload.editor.headers.intro.title': 'Tenant and user context',
515
+ 'praxis.filesUpload.editor.headers.intro.body': 'Use headers to query the effective server configuration in the same context the component will use in production.',
516
+ 'praxis.filesUpload.editor.headers.section.title': 'Configurable options — Headers (query)',
517
+ 'praxis.filesUpload.editor.headers.tenantHeader': 'Tenant header',
518
+ 'praxis.filesUpload.editor.headers.tenantValue': 'Tenant value',
519
+ 'praxis.filesUpload.editor.headers.userHeader': 'User header',
520
+ 'praxis.filesUpload.editor.headers.userValue': 'User value',
521
+ 'praxis.filesUpload.editor.errors.INVALID_FILE_TYPE': 'Invalid file type',
522
+ 'praxis.filesUpload.editor.errors.FILE_TOO_LARGE': 'File too large',
523
+ 'praxis.filesUpload.editor.errors.NOT_FOUND': 'File not found',
524
+ 'praxis.filesUpload.editor.errors.UNAUTHORIZED': 'Unauthorized access',
525
+ 'praxis.filesUpload.editor.errors.RATE_LIMIT_EXCEEDED': 'Request limit exceeded',
526
+ 'praxis.filesUpload.editor.errors.INTERNAL_ERROR': 'Internal server error',
527
+ 'praxis.filesUpload.editor.errors.QUOTA_EXCEEDED': 'Upload quota exceeded',
528
+ 'praxis.filesUpload.editor.errors.SEC_VIRUS_DETECTED': 'Virus detected in file',
529
+ 'praxis.filesUpload.editor.errors.SEC_MALICIOUS_CONTENT': 'Malicious content detected',
530
+ 'praxis.filesUpload.editor.errors.SEC_DANGEROUS_TYPE': 'Dangerous file type',
531
+ 'praxis.filesUpload.editor.errors.FMT_MAGIC_MISMATCH': 'File content does not match type',
532
+ 'praxis.filesUpload.editor.errors.FMT_CORRUPTED': 'Corrupted file',
533
+ 'praxis.filesUpload.editor.errors.FMT_UNSUPPORTED': 'Unsupported format',
534
+ 'praxis.filesUpload.editor.errors.SYS_STORAGE_ERROR': 'Storage error',
535
+ 'praxis.filesUpload.editor.errors.SYS_SERVICE_DOWN': 'Service unavailable',
536
+ 'praxis.filesUpload.editor.errors.SYS_RATE_LIMIT': 'System request limit exceeded',
537
+ 'praxis.filesUpload.editor.server.summary.maxPerFile': 'Max per file (MB):',
538
+ 'praxis.filesUpload.editor.server.summary.strictValidation': 'Strict validation:',
539
+ 'praxis.filesUpload.editor.server.summary.antivirus': 'Antivirus:',
540
+ 'praxis.filesUpload.editor.server.summary.conflictPolicy': 'Default name conflict:',
541
+ 'praxis.filesUpload.editor.server.summary.mimeTypes': 'Accepted MIME types:',
542
+ 'praxis.filesUpload.editor.server.summary.bulkFailFast': 'Default bulk fail-fast:',
543
+ 'praxis.filesUpload.editor.server.summary.rateLimit': 'Rate limit:',
544
+ 'praxis.filesUpload.editor.server.summary.quotas': 'Quotas:',
545
+ 'praxis.filesUpload.editor.server.summary.server': 'Server:',
546
+ 'praxis.filesUpload.editor.server.mutableOptionsText': 'The options above that can be changed via payload are: name conflict, strict validation, maximum size (MB), accepted extensions/MIME, target directory, antivirus, custom metadata and fail-fast (in bulk).',
547
+ 'praxis.filesUpload.editor.server.intro.title': 'Effective contract returned by the backend',
548
+ 'praxis.filesUpload.editor.server.intro.body': 'This tab is the source of truth for the active environment. Compare the server summary with what was configured in the previous forms.',
549
+ 'praxis.filesUpload.editor.server.readonly': 'Server (read-only)',
550
+ 'praxis.filesUpload.editor.server.reload': 'Reload from server',
551
+ 'praxis.filesUpload.editor.server.baseUrlHint': 'Set baseUrl on the parent component to query /api/files/config.',
552
+ 'praxis.filesUpload.editor.server.loading': 'Loading server configuration...',
553
+ 'praxis.filesUpload.editor.server.loadError': 'Failed to load:',
554
+ 'praxis.filesUpload.editor.server.summaryTitle': 'Active configuration summary',
555
+ 'praxis.filesUpload.editor.server.viewJson': 'View JSON',
556
+ 'praxis.filesUpload.editor.server.copyJson': 'Copy JSON',
557
+ 'praxis.filesUpload.editor.server.mutableOptionsPrefix': 'The options above that can be changed via payload are:',
558
+ 'praxis.filesUpload.editor.json.intro.title': 'Advanced mode',
559
+ 'praxis.filesUpload.editor.json.intro.body': 'Edit the raw payload only when you need fine-grained control. Changes here require stricter review and validation.',
560
+ 'praxis.filesUpload.editor.json.note': 'JSON is a low-level view of the configuration. Prefer the guided tabs whenever possible to reduce human error and keep the configuration auditable.',
561
+ 'praxis.filesUpload.editor.snackbar.configCopied': 'Configuration copied',
562
+ 'praxis.filesUpload.editor.snackbar.invalidJson': 'Invalid JSON',
563
+ 'praxis.filesUpload.editor.json.invalid': 'Invalid JSON: check the syntax.',
564
+ 'praxis.filesUpload.editor.summary.strategy.presign': 'Upload with presigned URL',
565
+ 'praxis.filesUpload.editor.summary.strategy.auto': 'Automatic strategy selection',
566
+ 'praxis.filesUpload.editor.summary.strategy.direct': 'Direct upload to backend',
567
+ 'praxis.filesUpload.editor.summary.strategy.presignDetail': 'Depends on a presign endpoint and usually reduces direct load on the main server.',
568
+ 'praxis.filesUpload.editor.summary.strategy.autoDetail': 'Tries presign first and falls back to the direct flow when needed.',
569
+ 'praxis.filesUpload.editor.summary.strategy.directDetail': 'Simpler flow to integrate and diagnose in controlled environments.',
570
+ 'praxis.filesUpload.editor.summary.experience.manual': 'Manual review before upload',
571
+ 'praxis.filesUpload.editor.summary.experience.compact': 'Selection guided by compact field',
572
+ 'praxis.filesUpload.editor.summary.experience.dropzone': 'Immediate upload with visible dropzone',
573
+ 'praxis.filesUpload.editor.summary.experience.progressVisible': 'visible progress',
574
+ 'praxis.filesUpload.editor.summary.experience.progressHidden': 'no progress bar',
575
+ 'praxis.filesUpload.editor.summary.experience.conflictVisible': 'conflict selection available',
576
+ 'praxis.filesUpload.editor.summary.experience.conflictHidden': 'conflict policy hidden',
577
+ 'praxis.filesUpload.editor.summary.experience.metadataVisible': 'editable metadata in the UI',
578
+ 'praxis.filesUpload.editor.summary.experience.metadataHidden': 'no extra form',
579
+ 'praxis.filesUpload.editor.summary.limits.titleValue': '{maxFile} MB per file • {maxBulk} items per batch',
580
+ 'praxis.filesUpload.editor.summary.limits.detailValue': '{parallel} parallel upload(s) • {mode}',
581
+ 'praxis.filesUpload.editor.summary.limits.failFast': 'fail-fast enabled',
582
+ 'praxis.filesUpload.editor.summary.limits.partialFailures': 'partial failures allowed',
583
+ 'praxis.filesUpload.editor.summary.server.titleValue': 'Default conflict: {conflict}',
584
+ 'praxis.filesUpload.editor.summary.server.detailValue': '{strict} • {virus}',
585
+ 'praxis.filesUpload.editor.summary.server.strictOn': 'strict validation enabled',
586
+ 'praxis.filesUpload.editor.summary.server.strictOff': 'strict validation disabled',
587
+ 'praxis.filesUpload.editor.summary.server.virusOn': 'forced antivirus',
588
+ 'praxis.filesUpload.editor.summary.server.virusOff': 'optional/inactive antivirus',
589
+ 'praxis.filesUpload.editor.risks.overwrite.title': 'Overwrite enabled',
590
+ 'praxis.filesUpload.editor.risks.overwrite.detail': 'Existing files may be silently replaced if the backend accepts the operation.',
591
+ 'praxis.filesUpload.editor.risks.virus.title': 'Antivirus may increase latency',
592
+ 'praxis.filesUpload.editor.risks.virus.detail': 'Forced scanning improves security, but may impact throughput and response time.',
593
+ 'praxis.filesUpload.editor.risks.failFast.title': 'Batch stops on the first error',
594
+ 'praxis.filesUpload.editor.risks.failFast.detail': 'Users may need to resend valid items when the first error interrupts the rest of the batch.',
595
+ 'praxis.filesUpload.editor.risks.retry.title': 'Aggressive automatic retry',
596
+ 'praxis.filesUpload.editor.risks.retry.detail': 'Too many automatic retries may increase perceived wait time and operational noise.',
597
+ 'praxis.filesUpload.editor.risks.manual.title': 'Flow requires manual action',
598
+ 'praxis.filesUpload.editor.risks.manual.detail': 'Suitable for review, but adds upload steps and may reduce completion rate in simple scenarios.',
599
+ 'praxis.filesUpload.settingsAriaLabel': 'Open settings',
600
+ 'praxis.filesUpload.dropzoneLabel': 'Drag files or',
601
+ 'praxis.filesUpload.dropzoneButton': 'select',
602
+ 'praxis.filesUpload.conflictPolicyLabel': 'Conflict policy',
603
+ 'praxis.filesUpload.metadataLabel': 'Metadata (JSON)',
604
+ 'praxis.filesUpload.progressAriaLabel': 'Upload progress',
605
+ 'praxis.filesUpload.rateLimitBanner': 'Rate limit exceeded. Try again at',
606
+ 'praxis.filesUpload.settingsTitle': 'Files Upload Configuration',
607
+ 'praxis.filesUpload.statusSuccess': 'Uploaded',
608
+ 'praxis.filesUpload.statusError': 'Error',
609
+ 'praxis.filesUpload.invalidMetadata': 'Invalid metadata.',
610
+ 'praxis.filesUpload.genericUploadError': 'File upload error.',
611
+ 'praxis.filesUpload.acceptError': 'File type not allowed.',
612
+ 'praxis.filesUpload.maxFileSizeError': 'File exceeds the maximum size.',
613
+ 'praxis.filesUpload.maxFilesPerBulkError': 'Too many files selected.',
614
+ 'praxis.filesUpload.maxBulkSizeError': 'Total files size exceeded.',
615
+ 'praxis.filesUpload.serviceUnavailable': 'Upload service unavailable.',
616
+ 'praxis.filesUpload.baseUrlMissing': 'Base URL not configured. Provide [baseUrl] to enable uploads.',
617
+ 'praxis.filesUpload.selectFiles': 'Select file(s)',
618
+ 'praxis.filesUpload.remove': 'Remove',
619
+ 'praxis.filesUpload.moreActions': 'More actions',
620
+ 'praxis.filesUpload.policySummaryAction': 'Information about upload policies',
621
+ 'praxis.filesUpload.retry': 'Retry',
622
+ 'praxis.filesUpload.download': 'Download',
623
+ 'praxis.filesUpload.copyLink': 'Copy link',
624
+ 'praxis.filesUpload.details': 'Details',
625
+ 'praxis.filesUpload.detailsMetadata': 'Metadata',
626
+ 'praxis.filesUpload.dropzoneHint': 'Drag and drop files here or click to select',
627
+ 'praxis.filesUpload.dropzoneProximityHint': 'Drop here to upload',
628
+ 'praxis.filesUpload.placeholder': 'Select or drop files…',
629
+ 'praxis.filesUpload.partialUploadError': 'Partial upload completed with failures in part of the batch.',
630
+ 'praxis.filesUpload.presignMetadataMissing': 'Presigned upload completed without file metadata.',
631
+ 'praxis.filesUpload.fieldChipAria': 'Additional files selected',
632
+ 'praxis.filesUpload.fieldPendingCountSuffix': 'file(s) selected',
633
+ 'praxis.filesUpload.removePendingFile': 'Remove selected file',
634
+ 'praxis.filesUpload.fieldPolicyTypes': 'Types',
635
+ 'praxis.filesUpload.fieldPolicyMaxPerFile': 'Max/file',
636
+ 'praxis.filesUpload.fieldPolicyMaxItems': 'Max/items',
637
+ 'praxis.filesUpload.fieldPolicyNotConfigured': 'Not configured',
638
+ 'praxis.filesUpload.sizeUnitBytes': 'Bytes',
639
+ 'praxis.filesUpload.sizeUnitKB': 'KB',
640
+ 'praxis.filesUpload.sizeUnitMB': 'MB',
641
+ 'praxis.filesUpload.sizeUnitGB': 'GB',
642
+ 'praxis.filesUpload.sizeUnitTB': 'TB',
643
+ 'praxis.filesUpload.errors.INVALID_FILE_TYPE': 'Invalid file type.',
644
+ 'praxis.filesUpload.errors.FILE_TOO_LARGE': 'File is too large.',
645
+ 'praxis.filesUpload.errors.NOT_FOUND': 'File not found.',
646
+ 'praxis.filesUpload.errors.UNAUTHORIZED': 'Unauthorized request.',
647
+ 'praxis.filesUpload.errors.RATE_LIMIT_EXCEEDED': 'Request limit exceeded.',
648
+ 'praxis.filesUpload.errors.INTERNAL_ERROR': 'Internal server error.',
649
+ 'praxis.filesUpload.errors.QUOTA_EXCEEDED': 'Quota exceeded.',
650
+ 'praxis.filesUpload.errors.SEC_VIRUS_DETECTED': 'Virus detected in file.',
651
+ 'praxis.filesUpload.errors.SEC_MALICIOUS_CONTENT': 'Malicious content detected.',
652
+ 'praxis.filesUpload.errors.SEC_DANGEROUS_TYPE': 'Dangerous file type.',
653
+ 'praxis.filesUpload.errors.FMT_MAGIC_MISMATCH': 'File content does not match the declared format.',
654
+ 'praxis.filesUpload.errors.FMT_CORRUPTED': 'Corrupted file.',
655
+ 'praxis.filesUpload.errors.FMT_UNSUPPORTED': 'Unsupported file format.',
656
+ 'praxis.filesUpload.errors.SYS_STORAGE_ERROR': 'Storage error.',
657
+ 'praxis.filesUpload.errors.SYS_SERVICE_DOWN': 'Service unavailable.',
658
+ 'praxis.filesUpload.errors.SYS_RATE_LIMIT': 'Request limit exceeded.',
659
+ 'praxis.filesUpload.errors.ARQUIVO_MUITO_GRANDE': 'File is too large.',
660
+ 'praxis.filesUpload.errors.TIPO_ARQUIVO_INVALIDO': 'Invalid file type.',
661
+ 'praxis.filesUpload.errors.TIPO_MIDIA_NAO_SUPORTADO': 'Unsupported media type.',
662
+ 'praxis.filesUpload.errors.CAMPO_OBRIGATORIO_AUSENTE': 'Required field is missing.',
663
+ 'praxis.filesUpload.errors.OPCOES_JSON_INVALIDAS': 'Invalid JSON options.',
664
+ 'praxis.filesUpload.errors.ARGUMENTO_INVALIDO': 'Invalid argument.',
665
+ 'praxis.filesUpload.errors.NAO_AUTORIZADO': 'Authentication required.',
666
+ 'praxis.filesUpload.errors.ERRO_INTERNO': 'Internal server error.',
667
+ 'praxis.filesUpload.errors.LIMITE_TAXA_EXCEDIDO': 'Rate limit exceeded.',
668
+ 'praxis.filesUpload.errors.COTA_EXCEDIDA': 'Quota exceeded.',
669
+ 'praxis.filesUpload.errors.ARQUIVO_JA_EXISTE': 'File already exists.',
670
+ 'praxis.filesUpload.errors.INVALID_JSON_OPTIONS': 'Invalid JSON options.',
671
+ 'praxis.filesUpload.errors.EMPTY_FILENAME': 'File name is required.',
672
+ 'praxis.filesUpload.errors.FILE_EXISTS': 'File already exists.',
673
+ 'praxis.filesUpload.errors.PATH_TRAVERSAL': 'Path traversal detected.',
674
+ 'praxis.filesUpload.errors.INSUFFICIENT_STORAGE': 'Insufficient storage.',
675
+ 'praxis.filesUpload.errors.UPLOAD_TIMEOUT': 'Upload timed out.',
676
+ 'praxis.filesUpload.errors.BULK_UPLOAD_TIMEOUT': 'Bulk upload timed out.',
677
+ 'praxis.filesUpload.errors.BULK_UPLOAD_CANCELLED': 'Bulk upload cancelled.',
678
+ 'praxis.filesUpload.errors.USER_CANCELLED': 'Upload cancelled by user.',
679
+ 'praxis.filesUpload.errors.UNKNOWN_ERROR': 'Unknown error.',
680
+ 'praxis.filesUpload.fieldPendingSummary': 'Files ready to upload',
681
+ 'praxis.filesUpload.fieldSelectedSummary': 'Selected file',
682
+ 'praxis.filesUpload.fieldSelectedPlural': 'files attached',
683
+ 'praxis.filesUpload.fieldEmptySummary': 'No file selected',
684
+ 'praxis.filesUpload.fieldErrorSummary': 'Fix the error to continue',
685
+ 'praxis.filesUpload.fieldActionUpload': 'Upload selected files',
686
+ 'praxis.filesUpload.fieldActionUploadShort': 'Upload',
687
+ 'praxis.filesUpload.fieldActionCancel': 'Cancel current selection',
688
+ 'praxis.filesUpload.fieldActionCancelShort': 'Cancel',
689
+ 'praxis.filesUpload.fieldActionSelect': 'Select file(s)',
690
+ 'praxis.filesUpload.fieldActionClear': 'Clear',
691
+ 'praxis.filesUpload.fieldInfoAction': 'Information',
692
+ 'praxis.filesUpload.fieldMoreActions': 'More actions',
693
+ 'praxis.filesUpload.fieldPendingAriaSuffix': 'files ready to upload',
694
+ 'praxis.filesUpload.selectedOverlayAriaLabel': 'Selected files',
695
+ 'praxis.filesUpload.selectedOverlayTitle': 'Selected',
696
+ 'praxis.filesUpload.bulkSummaryTotal': 'Total',
697
+ 'praxis.filesUpload.bulkSummarySuccess': 'Success',
698
+ 'praxis.filesUpload.bulkSummaryFailed': 'Failed',
699
+ 'praxis.filesUpload.resultMetaIdLabel': 'ID',
700
+ 'praxis.filesUpload.resultMetaTypeLabel': 'Type',
701
+ 'praxis.filesUpload.resultMetaSizeLabel': 'Size',
702
+ 'praxis.filesUpload.resultMetaUploadedAtLabel': 'Uploaded at',
703
+ 'praxis.filesUpload.policySummaryAny': 'any',
704
+ 'praxis.filesUpload.policySummaryNotConfigured': '—',
705
+ 'praxis.filesUpload.policySummaryTypesLabel': 'Types',
706
+ 'praxis.filesUpload.policySummaryMaxPerFileLabel': 'Max. per file',
707
+ 'praxis.filesUpload.policySummaryQuantityLabel': 'Qty',
708
+ 'praxis.filesUpload.showAll': 'Show all',
709
+ 'praxis.filesUpload.showLess': 'Show less',
710
+ };
711
+
712
+ const FILES_UPLOAD_PT_BR = {
713
+ 'praxis.filesUpload.editor.summary.ariaLabel': 'Resumo executivo da configuracao atual',
714
+ 'praxis.filesUpload.editor.summary.title': 'Resumo executivo',
715
+ 'praxis.filesUpload.editor.summary.intro': 'Revise aqui o impacto operacional antes de percorrer todos os grupos de configuracao.',
716
+ 'praxis.filesUpload.editor.summary.strategy': 'Estrategia',
717
+ 'praxis.filesUpload.editor.summary.experience': 'Experiencia',
718
+ 'praxis.filesUpload.editor.summary.limits': 'Limites',
719
+ 'praxis.filesUpload.editor.summary.server': 'Servidor',
720
+ 'praxis.filesUpload.editor.risks.title': 'Riscos e atencao',
721
+ 'praxis.filesUpload.editor.risks.none': 'Nenhum risco operacional evidente na configuracao atual.',
722
+ 'praxis.filesUpload.editor.tabs.behavior': 'Comportamento',
723
+ 'praxis.filesUpload.editor.tabs.interface': 'Interface',
724
+ 'praxis.filesUpload.editor.tabs.validation': 'Validacoes',
725
+ 'praxis.filesUpload.editor.tabs.messages': 'Mensagens',
726
+ 'praxis.filesUpload.editor.tabs.server': 'Servidor',
727
+ 'praxis.filesUpload.editor.tabs.json': 'JSON',
728
+ 'praxis.filesUpload.editor.behavior.intro.title': 'Estrategia e cadencia do envio',
729
+ 'praxis.filesUpload.editor.behavior.intro.body': 'Defina aqui como o usuario inicia o upload e como o lote se comporta em termos de paralelismo e retentativa.',
730
+ 'praxis.filesUpload.editor.behavior.strategy.label': 'Estrategia de envio',
731
+ 'praxis.filesUpload.editor.behavior.strategy.direct': 'Direto (HTTP padrao)',
732
+ 'praxis.filesUpload.editor.behavior.strategy.presign': 'URL pre-assinada (S3/GCS)',
733
+ 'praxis.filesUpload.editor.behavior.strategy.auto': 'Automatico (tenta pre-assinada e volta ao direto)',
734
+ 'praxis.filesUpload.editor.behavior.strategy.hint': 'Como os arquivos serao enviados ao servidor.',
735
+ 'praxis.filesUpload.editor.behavior.bulk.title': 'Opcoes configuraveis — Lote',
736
+ 'praxis.filesUpload.editor.behavior.bulk.note': 'Use paralelismo e retries com moderacao. Em ambientes corporativos, esses controles afetam throughput, experiencia percebida e carga no backend.',
737
+ 'praxis.filesUpload.editor.behavior.bulk.parallel': 'Uploads paralelos',
738
+ 'praxis.filesUpload.editor.behavior.bulk.parallelHint': 'Quantos arquivos enviar ao mesmo tempo.',
739
+ 'praxis.filesUpload.editor.behavior.bulk.retryCount': 'Numero de tentativas',
740
+ 'praxis.filesUpload.editor.behavior.bulk.retryCountHint': 'Tentativas automaticas em caso de falha.',
741
+ 'praxis.filesUpload.editor.behavior.bulk.retryBackoff': 'Intervalo entre tentativas (ms)',
742
+ 'praxis.filesUpload.editor.behavior.bulk.retryBackoffHint': 'Tempo de espera entre tentativas.',
743
+ 'praxis.filesUpload.editor.interface.intro.title': 'Experiencia visivel para o usuario',
744
+ 'praxis.filesUpload.editor.interface.intro.body': 'Configure densidade, dropzone, metadados e detalhes da lista. Priorize clareza operacional antes de habilitar recursos avancados.',
745
+ 'praxis.filesUpload.editor.interface.section.title': 'Opcoes configuraveis — Interface',
746
+ 'praxis.filesUpload.editor.interface.section.note': 'Esta secao controla a ergonomia do componente. Mudancas aqui afetam descoberta, esforco de uso e taxa de conclusao do envio.',
747
+ 'praxis.filesUpload.editor.interface.showDropzone': 'Exibir area de soltar',
748
+ 'praxis.filesUpload.editor.interface.showProgress': 'Exibir barra de progresso',
749
+ 'praxis.filesUpload.editor.interface.showConflictPolicySelector': 'Permitir escolher a politica de conflito',
750
+ 'praxis.filesUpload.editor.interface.manualUpload': 'Exigir clique em "Enviar" (modo manual)',
751
+ 'praxis.filesUpload.editor.interface.dense': 'Layout compacto',
752
+ 'praxis.filesUpload.editor.interface.accept': 'Tipos permitidos (accept)',
753
+ 'praxis.filesUpload.editor.shared.optionalTag': 'opcional',
754
+ 'praxis.filesUpload.editor.shared.csvOptional': 'Lista separada por virgula (opcional).',
755
+ 'praxis.filesUpload.editor.placeholders.accept': 'ex.: pdf,jpg,png',
756
+ 'praxis.filesUpload.editor.placeholders.detailsFields': 'ex.: id,fileName,contentType',
757
+ 'praxis.filesUpload.editor.placeholders.allowedExtensions': 'ex.: pdf,docx,xlsx',
758
+ 'praxis.filesUpload.editor.placeholders.acceptMimeTypes': 'ex.: application/pdf,image/png',
759
+ 'praxis.filesUpload.editor.placeholders.targetDirectory': 'ex.: documentos/notas',
760
+ 'praxis.filesUpload.editor.placeholders.successSingle': 'ex.: Arquivo enviado com sucesso',
761
+ 'praxis.filesUpload.editor.placeholders.successBulk': 'ex.: Upload concluido',
762
+ 'praxis.filesUpload.editor.placeholders.tenantValue': 'ex.: demo-tenant',
763
+ 'praxis.filesUpload.editor.placeholders.userValue': 'ex.: 42',
764
+ 'praxis.filesUpload.editor.interface.metadataForm': 'Exibir formulario de metadados (JSON)',
765
+ 'praxis.filesUpload.editor.interface.dropzone.title': 'Dropzone (expansao por proximidade)',
766
+ 'praxis.filesUpload.editor.interface.dropzone.expandOnProximity': 'Expandir ao aproximar arquivo durante arraste',
767
+ 'praxis.filesUpload.editor.interface.dropzone.proximity': 'Raio de proximidade (px)',
768
+ 'praxis.filesUpload.editor.interface.dropzone.mode': 'Modo de expansao',
769
+ 'praxis.filesUpload.editor.interface.dropzone.mode.overlay': 'Overlay (recomendado)',
770
+ 'praxis.filesUpload.editor.interface.dropzone.mode.inline': 'Inline',
771
+ 'praxis.filesUpload.editor.interface.dropzone.height': 'Altura do overlay (px)',
772
+ 'praxis.filesUpload.editor.interface.dropzone.debounce': 'Debounce de arraste (ms)',
773
+ 'praxis.filesUpload.editor.interface.list.title': 'Lista e detalhes',
774
+ 'praxis.filesUpload.editor.interface.list.collapseAfter': 'Colapsar apos (itens)',
775
+ 'praxis.filesUpload.editor.interface.list.detailsMode': 'Modo de detalhes',
776
+ 'praxis.filesUpload.editor.interface.list.detailsMode.auto': 'Automatico',
777
+ 'praxis.filesUpload.editor.interface.list.detailsMode.card': 'Card (overlay)',
778
+ 'praxis.filesUpload.editor.interface.list.detailsMode.sidesheet': 'Side-sheet',
779
+ 'praxis.filesUpload.editor.interface.list.detailsWidth': 'Largura maxima do card (px)',
780
+ 'praxis.filesUpload.editor.interface.list.showTechnical': 'Mostrar detalhes tecnicos por padrao',
781
+ 'praxis.filesUpload.editor.interface.list.detailsFields': 'Campos de metadados (whitelist)',
782
+ 'praxis.filesUpload.editor.interface.list.detailsFieldsHint': 'Lista separada por virgula; vazio = todos.',
783
+ 'praxis.filesUpload.editor.interface.list.anchor': 'Ancora do overlay',
784
+ 'praxis.filesUpload.editor.interface.list.anchor.item': 'Item',
785
+ 'praxis.filesUpload.editor.interface.list.anchor.field': 'Campo',
786
+ 'praxis.filesUpload.editor.validation.intro.title': 'Regras de aceite e comportamento operacional',
787
+ 'praxis.filesUpload.editor.validation.intro.body': 'Separe mentalmente o que e validacao de UX local do que e regra efetiva de backend. Revise com atencao as opcoes destrutivas.',
788
+ 'praxis.filesUpload.editor.validation.local.title': 'Opcoes configuraveis — Validacoes',
789
+ 'praxis.filesUpload.editor.validation.local.note': 'Limites locais melhoram feedback imediato, mas nao substituem a politica efetiva do servidor.',
790
+ 'praxis.filesUpload.editor.validation.local.maxFileSize': 'Tamanho maximo do arquivo (bytes)',
791
+ 'praxis.filesUpload.editor.validation.local.maxFileSizeHint': 'Limite de validacao no cliente (opcional).',
792
+ 'praxis.filesUpload.editor.validation.local.maxFilesPerBulk': 'Max. arquivos por lote',
793
+ 'praxis.filesUpload.editor.validation.local.maxBulkSize': 'Tamanho maximo do lote (bytes)',
794
+ 'praxis.filesUpload.editor.validation.backend.title': 'Opcoes configuraveis Backend',
795
+ 'praxis.filesUpload.editor.validation.backend.note': 'Estas opcoes tem impacto direto no contrato com o servidor e em governanca operacional. Trate alteracoes aqui como decisao de politica, nao apenas de interface.',
796
+ 'praxis.filesUpload.editor.validation.backend.conflictPolicy': 'Politica de conflito (padrao)',
797
+ 'praxis.filesUpload.editor.validation.backend.conflictPolicy.rename': 'Renomear automaticamente',
798
+ 'praxis.filesUpload.editor.validation.backend.conflictPolicy.unique': 'Gerar nome unico',
799
+ 'praxis.filesUpload.editor.validation.backend.conflictPolicy.overwrite': 'Sobrescrever arquivo existente',
800
+ 'praxis.filesUpload.editor.validation.backend.conflictPolicy.skip': 'Pular se ja existir',
801
+ 'praxis.filesUpload.editor.validation.backend.conflictPolicy.error': 'Falhar (erro)',
802
+ 'praxis.filesUpload.editor.validation.backend.conflictPolicyHint': 'O que fazer quando o nome do arquivo ja existe.',
803
+ 'praxis.filesUpload.editor.validation.backend.overwriteWarn': 'Atencao: OVERWRITE pode sobrescrever arquivos existentes.',
804
+ 'praxis.filesUpload.editor.validation.backend.strictValidation': 'Validacao rigorosa (backend)',
805
+ 'praxis.filesUpload.editor.validation.backend.maxUploadSizeMb': 'Tamanho max. por arquivo (MB)',
806
+ 'praxis.filesUpload.editor.validation.backend.maxUploadSizeMbHint': 'Validado pelo backend (1–500 MB).',
807
+ 'praxis.filesUpload.editor.validation.backend.allowedExtensions': 'Extensoes permitidas',
808
+ 'praxis.filesUpload.editor.validation.backend.acceptMimeTypes': 'MIME types aceitos',
809
+ 'praxis.filesUpload.editor.validation.backend.targetDirectory': 'Diretorio destino',
810
+ 'praxis.filesUpload.editor.validation.backend.enableVirusScanning': 'Forcar antivirus (quando disponivel)',
811
+ 'praxis.filesUpload.editor.validation.backend.virusWarn': 'Pode impactar desempenho e latencia de upload.',
812
+ 'praxis.filesUpload.editor.validation.quotas.title': 'Opcoes configuraveis — Quotas (UI)',
813
+ 'praxis.filesUpload.editor.validation.quotas.showWarnings': 'Exibir avisos de cota',
814
+ 'praxis.filesUpload.editor.validation.quotas.blockOnExceed': 'Bloquear ao exceder cota',
815
+ 'praxis.filesUpload.editor.validation.rateLimit.title': 'Opcoes configuraveis Rate Limit (UI)',
816
+ 'praxis.filesUpload.editor.validation.rateLimit.note': 'Prefira poucos retries automaticos. Mais tentativas aliviam atrito imediato, mas podem mascarar gargalos reais do ambiente.',
817
+ 'praxis.filesUpload.editor.validation.rateLimit.showBanner': 'Exibir banner quando atingir o limite',
818
+ 'praxis.filesUpload.editor.validation.rateLimit.autoRetry': 'Tentar novamente automaticamente',
819
+ 'praxis.filesUpload.editor.validation.rateLimit.maxAutoRetry': 'Maximo de tentativas automaticas',
820
+ 'praxis.filesUpload.editor.validation.rateLimit.baseBackoff': 'Intervalo base entre tentativas (ms)',
821
+ 'praxis.filesUpload.editor.validation.batch.title': 'Opcoes configuraveis Execucao do lote',
822
+ 'praxis.filesUpload.editor.validation.batch.failFast': 'Parar no primeiro erro (fail-fast)',
823
+ 'praxis.filesUpload.editor.messages.intro.title': 'Mensagens orientadas a operacao',
824
+ 'praxis.filesUpload.editor.messages.intro.body': 'Ajuste o texto exibido ao usuario final. Priorize clareza, consistencia e linguagem acionavel.',
825
+ 'praxis.filesUpload.editor.messages.section.title': 'Opcoes configuraveis — Mensagens (UI)',
826
+ 'praxis.filesUpload.editor.messages.section.note': 'Mensagens curtas e objetivas funcionam melhor em contextos de alta frequencia. Use este grupo para adequar a voz da interface ao seu ambiente.',
827
+ 'praxis.filesUpload.editor.messages.successSingle': 'Sucesso (individual)',
828
+ 'praxis.filesUpload.editor.messages.successBulk': 'Sucesso (em lote)',
829
+ 'praxis.filesUpload.editor.tabs.headers': 'Cabecalhos',
830
+ 'praxis.filesUpload.editor.headers.intro.title': 'Contexto de tenant e usuario',
831
+ 'praxis.filesUpload.editor.headers.intro.body': 'Use cabecalhos para consultar configuracao efetiva do servidor no mesmo contexto que o componente usara em producao.',
832
+ 'praxis.filesUpload.editor.headers.section.title': 'Opcoes configuraveis — Cabecalhos (consulta)',
833
+ 'praxis.filesUpload.editor.headers.tenantHeader': 'Cabecalho de tenant',
834
+ 'praxis.filesUpload.editor.headers.tenantValue': 'Valor do tenant',
835
+ 'praxis.filesUpload.editor.headers.userHeader': 'Cabecalho de usuario',
836
+ 'praxis.filesUpload.editor.headers.userValue': 'Valor do usuario',
837
+ 'praxis.filesUpload.editor.errors.INVALID_FILE_TYPE': 'Tipo de arquivo invalido',
838
+ 'praxis.filesUpload.editor.errors.FILE_TOO_LARGE': 'Arquivo muito grande',
839
+ 'praxis.filesUpload.editor.errors.NOT_FOUND': 'Arquivo nao encontrado',
840
+ 'praxis.filesUpload.editor.errors.UNAUTHORIZED': 'Acesso nao autorizado',
841
+ 'praxis.filesUpload.editor.errors.RATE_LIMIT_EXCEEDED': 'Limite de requisicoes excedido',
842
+ 'praxis.filesUpload.editor.errors.INTERNAL_ERROR': 'Erro interno do servidor',
843
+ 'praxis.filesUpload.editor.errors.QUOTA_EXCEEDED': 'Cota de upload excedida',
844
+ 'praxis.filesUpload.editor.errors.SEC_VIRUS_DETECTED': 'Virus detectado no arquivo',
845
+ 'praxis.filesUpload.editor.errors.SEC_MALICIOUS_CONTENT': 'Conteudo malicioso detectado',
846
+ 'praxis.filesUpload.editor.errors.SEC_DANGEROUS_TYPE': 'Tipo de arquivo perigoso',
847
+ 'praxis.filesUpload.editor.errors.FMT_MAGIC_MISMATCH': 'Conteudo do arquivo incompativel',
848
+ 'praxis.filesUpload.editor.errors.FMT_CORRUPTED': 'Arquivo corrompido',
849
+ 'praxis.filesUpload.editor.errors.FMT_UNSUPPORTED': 'Formato nao suportado',
850
+ 'praxis.filesUpload.editor.errors.SYS_STORAGE_ERROR': 'Erro no armazenamento',
851
+ 'praxis.filesUpload.editor.errors.SYS_SERVICE_DOWN': 'Servico indisponivel',
852
+ 'praxis.filesUpload.editor.errors.SYS_RATE_LIMIT': 'Limite de requisicoes (sistema) excedido',
853
+ 'praxis.filesUpload.editor.server.summary.maxPerFile': 'Max por arquivo (MB):',
854
+ 'praxis.filesUpload.editor.server.summary.strictValidation': 'Validacao rigorosa:',
855
+ 'praxis.filesUpload.editor.server.summary.antivirus': 'Antivirus:',
856
+ 'praxis.filesUpload.editor.server.summary.conflictPolicy': 'Conflito de nome (padrao):',
857
+ 'praxis.filesUpload.editor.server.summary.mimeTypes': 'MIME aceitos:',
858
+ 'praxis.filesUpload.editor.server.summary.bulkFailFast': 'Bulk - fail-fast padrao:',
859
+ 'praxis.filesUpload.editor.server.summary.rateLimit': 'Rate limit:',
860
+ 'praxis.filesUpload.editor.server.summary.quotas': 'Quotas:',
861
+ 'praxis.filesUpload.editor.server.summary.server': 'Servidor:',
862
+ 'praxis.filesUpload.editor.server.mutableOptionsText': 'As opcoes acima que podem ser alteradas via payload sao: conflito de nome, validacao rigorosa, tamanho maximo (MB), extensoes/MIME aceitos, diretorio destino, antivirus, metadados personalizados e fail-fast (no bulk).',
863
+ 'praxis.filesUpload.editor.server.intro.title': 'Contrato efetivo retornado pelo backend',
864
+ 'praxis.filesUpload.editor.server.intro.body': 'Esta aba e a fonte de verdade do ambiente ativo. Compare o resumo do servidor com o que foi configurado nos formularios anteriores.',
865
+ 'praxis.filesUpload.editor.server.readonly': 'Servidor (somente leitura)',
866
+ 'praxis.filesUpload.editor.server.reload': 'Recarregar do servidor',
867
+ 'praxis.filesUpload.editor.server.baseUrlHint': 'Defina a baseUrl no componente pai para consultar /api/files/config.',
868
+ 'praxis.filesUpload.editor.server.loading': 'Carregando configuracao do servidor...',
869
+ 'praxis.filesUpload.editor.server.loadError': 'Falha ao carregar:',
870
+ 'praxis.filesUpload.editor.server.summaryTitle': 'Resumo da configuracao ativa',
871
+ 'praxis.filesUpload.editor.server.viewJson': 'Ver JSON',
872
+ 'praxis.filesUpload.editor.server.copyJson': 'Copiar JSON',
873
+ 'praxis.filesUpload.editor.server.mutableOptionsPrefix': 'As opcoes acima que podem ser alteradas via payload sao:',
874
+ 'praxis.filesUpload.editor.json.intro.title': 'Modo avancado',
875
+ 'praxis.filesUpload.editor.json.intro.body': 'Edite o payload bruto apenas quando precisar de controle fino. Alteracoes aqui exigem mais rigor de revisao e validacao.',
876
+ 'praxis.filesUpload.editor.json.note': 'O JSON e uma visao de baixo nivel da configuracao. Prefira as abas guiadas sempre que possivel para reduzir erro humano e manter a configuracao auditavel.',
877
+ 'praxis.filesUpload.editor.snackbar.configCopied': 'Configuracao copiada',
878
+ 'praxis.filesUpload.editor.snackbar.invalidJson': 'JSON invalido',
879
+ 'praxis.filesUpload.editor.json.invalid': 'JSON invalido: verifique a sintaxe.',
880
+ 'praxis.filesUpload.editor.summary.strategy.presign': 'Upload com URL pre-assinada',
881
+ 'praxis.filesUpload.editor.summary.strategy.auto': 'Selecao automatica da estrategia',
882
+ 'praxis.filesUpload.editor.summary.strategy.direct': 'Upload direto ao backend',
883
+ 'praxis.filesUpload.editor.summary.strategy.presignDetail': 'Depende de endpoint de presign e costuma reduzir carga direta no servidor principal.',
884
+ 'praxis.filesUpload.editor.summary.strategy.autoDetail': 'Tenta presign primeiro e volta ao fluxo direto quando necessario.',
885
+ 'praxis.filesUpload.editor.summary.strategy.directDetail': 'Fluxo mais simples de integrar e diagnosticar em ambientes controlados.',
886
+ 'praxis.filesUpload.editor.summary.experience.manual': 'Revisao manual antes do envio',
887
+ 'praxis.filesUpload.editor.summary.experience.compact': 'Selecao orientada por campo compacto',
888
+ 'praxis.filesUpload.editor.summary.experience.dropzone': 'Envio imediato com dropzone visivel',
889
+ 'praxis.filesUpload.editor.summary.experience.progressVisible': 'progresso visivel',
890
+ 'praxis.filesUpload.editor.summary.experience.progressHidden': 'sem barra de progresso',
891
+ 'praxis.filesUpload.editor.summary.experience.conflictVisible': 'escolha de conflito disponivel',
892
+ 'praxis.filesUpload.editor.summary.experience.conflictHidden': 'politica de conflito escondida',
893
+ 'praxis.filesUpload.editor.summary.experience.metadataVisible': 'metadados editaveis na UI',
894
+ 'praxis.filesUpload.editor.summary.experience.metadataHidden': 'sem formulario extra',
895
+ 'praxis.filesUpload.editor.summary.limits.titleValue': '{maxFile} MB por arquivo {maxBulk} itens por lote',
896
+ 'praxis.filesUpload.editor.summary.limits.detailValue': '{parallel} upload(s) paralelos • {mode}',
897
+ 'praxis.filesUpload.editor.summary.limits.failFast': 'fail-fast ativo',
898
+ 'praxis.filesUpload.editor.summary.limits.partialFailures': 'falhas parciais permitidas',
899
+ 'praxis.filesUpload.editor.summary.server.titleValue': 'Conflito padrao: {conflict}',
900
+ 'praxis.filesUpload.editor.summary.server.detailValue': '{strict} • {virus}',
901
+ 'praxis.filesUpload.editor.summary.server.strictOn': 'validacao rigorosa ligada',
902
+ 'praxis.filesUpload.editor.summary.server.strictOff': 'validacao rigida desativada',
903
+ 'praxis.filesUpload.editor.summary.server.virusOn': 'antivirus forcado',
904
+ 'praxis.filesUpload.editor.summary.server.virusOff': 'antivirus opcional/inativo',
905
+ 'praxis.filesUpload.editor.risks.overwrite.title': 'Sobrescrita habilitada',
906
+ 'praxis.filesUpload.editor.risks.overwrite.detail': 'Arquivos existentes podem ser substituidos silenciosamente se o backend aceitar a operacao.',
907
+ 'praxis.filesUpload.editor.risks.virus.title': 'Antivirus pode elevar latencia',
908
+ 'praxis.filesUpload.editor.risks.virus.detail': 'Ativar varredura forcada melhora seguranca, mas pode impactar throughput e tempo de resposta.',
909
+ 'praxis.filesUpload.editor.risks.failFast.title': 'Lote para no primeiro erro',
910
+ 'praxis.filesUpload.editor.risks.failFast.detail': 'Usuarios podem precisar reenviar itens validos quando o primeiro erro interromper o restante do lote.',
911
+ 'praxis.filesUpload.editor.risks.retry.title': 'Retry automatico agressivo',
912
+ 'praxis.filesUpload.editor.risks.retry.detail': 'Muitas tentativas automaticas podem aumentar tempo de espera percebido e ruido operacional.',
913
+ 'praxis.filesUpload.editor.risks.manual.title': 'Fluxo exige acao manual',
914
+ 'praxis.filesUpload.editor.risks.manual.detail': 'Adequado para revisao, mas aumenta passos no envio e pode reduzir taxa de conclusao em cenarios simples.',
915
+ 'praxis.filesUpload.settingsAriaLabel': 'Abrir configurações',
916
+ 'praxis.filesUpload.dropzoneLabel': 'Arraste arquivos ou',
917
+ 'praxis.filesUpload.dropzoneButton': 'selecionar',
918
+ 'praxis.filesUpload.conflictPolicyLabel': 'Política de conflito',
919
+ 'praxis.filesUpload.metadataLabel': 'Metadados (JSON)',
920
+ 'praxis.filesUpload.progressAriaLabel': 'Progresso do upload',
921
+ 'praxis.filesUpload.rateLimitBanner': 'Limite de requisições excedido. Tente novamente às',
922
+ 'praxis.filesUpload.settingsTitle': 'Configuração de Upload de Arquivos',
923
+ 'praxis.filesUpload.statusSuccess': 'Enviado',
924
+ 'praxis.filesUpload.statusError': 'Erro',
925
+ 'praxis.filesUpload.invalidMetadata': 'Metadados inválidos.',
926
+ 'praxis.filesUpload.genericUploadError': 'Erro no envio de arquivo.',
927
+ 'praxis.filesUpload.acceptError': 'Tipo de arquivo não permitido.',
928
+ 'praxis.filesUpload.maxFileSizeError': 'Arquivo excede o tamanho máximo.',
929
+ 'praxis.filesUpload.maxFilesPerBulkError': 'Quantidade de arquivos excedida.',
930
+ 'praxis.filesUpload.maxBulkSizeError': 'Tamanho total dos arquivos excedido.',
931
+ 'praxis.filesUpload.serviceUnavailable': 'Serviço de upload indisponível.',
932
+ 'praxis.filesUpload.baseUrlMissing': 'Base URL não configurada. Informe [baseUrl] para habilitar o envio.',
933
+ 'praxis.filesUpload.selectFiles': 'Selecionar arquivo(s)',
934
+ 'praxis.filesUpload.remove': 'Remover',
935
+ 'praxis.filesUpload.moreActions': 'Mais ações',
936
+ 'praxis.filesUpload.policySummaryAction': 'Informações sobre políticas de upload',
937
+ 'praxis.filesUpload.retry': 'Reenviar',
938
+ 'praxis.filesUpload.download': 'Baixar',
939
+ 'praxis.filesUpload.copyLink': 'Copiar link',
940
+ 'praxis.filesUpload.details': 'Detalhes',
941
+ 'praxis.filesUpload.detailsMetadata': 'Metadados',
942
+ 'praxis.filesUpload.dropzoneHint': 'Arraste e solte arquivos aqui ou clique para selecionar',
943
+ 'praxis.filesUpload.dropzoneProximityHint': 'Solte aqui para enviar',
944
+ 'praxis.filesUpload.placeholder': 'Selecione ou solte arquivos…',
945
+ 'praxis.filesUpload.partialUploadError': 'Upload parcial concluído com falhas em parte do lote.',
946
+ 'praxis.filesUpload.presignMetadataMissing': 'Upload assinado concluído sem metadados do arquivo.',
947
+ 'praxis.filesUpload.fieldChipAria': 'Arquivos adicionais selecionados',
948
+ 'praxis.filesUpload.fieldPendingCountSuffix': 'arquivo(s) selecionado(s)',
949
+ 'praxis.filesUpload.removePendingFile': 'Remover arquivo selecionado',
950
+ 'praxis.filesUpload.fieldPolicyTypes': 'Tipos',
951
+ 'praxis.filesUpload.fieldPolicyMaxPerFile': 'Máx/arquivo',
952
+ 'praxis.filesUpload.fieldPolicyMaxItems': 'Máx/itens',
953
+ 'praxis.filesUpload.fieldPolicyNotConfigured': 'Não configurado',
954
+ 'praxis.filesUpload.sizeUnitBytes': 'Bytes',
955
+ 'praxis.filesUpload.sizeUnitKB': 'KB',
956
+ 'praxis.filesUpload.sizeUnitMB': 'MB',
957
+ 'praxis.filesUpload.sizeUnitGB': 'GB',
958
+ 'praxis.filesUpload.sizeUnitTB': 'TB',
959
+ 'praxis.filesUpload.errors.INVALID_FILE_TYPE': 'Tipo de arquivo inválido.',
960
+ 'praxis.filesUpload.errors.FILE_TOO_LARGE': 'Arquivo muito grande.',
961
+ 'praxis.filesUpload.errors.NOT_FOUND': 'Arquivo não encontrado.',
962
+ 'praxis.filesUpload.errors.UNAUTHORIZED': 'Requisição não autorizada.',
963
+ 'praxis.filesUpload.errors.RATE_LIMIT_EXCEEDED': 'Limite de requisições excedido.',
964
+ 'praxis.filesUpload.errors.INTERNAL_ERROR': 'Erro interno do servidor.',
965
+ 'praxis.filesUpload.errors.QUOTA_EXCEEDED': 'Cota excedida.',
966
+ 'praxis.filesUpload.errors.SEC_VIRUS_DETECTED': 'Vírus detectado no arquivo.',
967
+ 'praxis.filesUpload.errors.SEC_MALICIOUS_CONTENT': 'Conteúdo malicioso detectado.',
968
+ 'praxis.filesUpload.errors.SEC_DANGEROUS_TYPE': 'Tipo de arquivo perigoso.',
969
+ 'praxis.filesUpload.errors.FMT_MAGIC_MISMATCH': 'Conteúdo do arquivo incompatível.',
970
+ 'praxis.filesUpload.errors.FMT_CORRUPTED': 'Arquivo corrompido.',
971
+ 'praxis.filesUpload.errors.FMT_UNSUPPORTED': 'Formato de arquivo não suportado.',
972
+ 'praxis.filesUpload.errors.SYS_STORAGE_ERROR': 'Erro de armazenamento.',
973
+ 'praxis.filesUpload.errors.SYS_SERVICE_DOWN': 'Serviço indisponível.',
974
+ 'praxis.filesUpload.errors.SYS_RATE_LIMIT': 'Limite de requisições excedido.',
975
+ // Aliases PT-BR do backend e códigos adicionais
976
+ 'praxis.filesUpload.errors.ARQUIVO_MUITO_GRANDE': 'Arquivo muito grande.',
977
+ 'praxis.filesUpload.errors.TIPO_ARQUIVO_INVALIDO': 'Tipo de arquivo inválido.',
978
+ 'praxis.filesUpload.errors.TIPO_MIDIA_NAO_SUPORTADO': 'Tipo de mídia não suportado.',
979
+ 'praxis.filesUpload.errors.CAMPO_OBRIGATORIO_AUSENTE': 'Campo obrigatório ausente.',
980
+ 'praxis.filesUpload.errors.OPCOES_JSON_INVALIDAS': 'JSON de opções inválido.',
981
+ 'praxis.filesUpload.errors.ARGUMENTO_INVALIDO': 'Argumento inválido.',
982
+ 'praxis.filesUpload.errors.NAO_AUTORIZADO': 'Autenticação necessária.',
983
+ 'praxis.filesUpload.errors.ERRO_INTERNO': 'Erro interno do servidor.',
984
+ 'praxis.filesUpload.errors.LIMITE_TAXA_EXCEDIDO': 'Limite de taxa excedido.',
985
+ 'praxis.filesUpload.errors.COTA_EXCEDIDA': 'Cota excedida.',
986
+ 'praxis.filesUpload.errors.ARQUIVO_JA_EXISTE': 'Arquivo já existe.',
987
+ // Outros possíveis códigos mapeados no catálogo
988
+ 'praxis.filesUpload.errors.INVALID_JSON_OPTIONS': 'JSON de opções inválido.',
989
+ 'praxis.filesUpload.errors.EMPTY_FILENAME': 'Nome do arquivo obrigatório.',
990
+ 'praxis.filesUpload.errors.FILE_EXISTS': 'Arquivo já existe.',
991
+ 'praxis.filesUpload.errors.PATH_TRAVERSAL': 'Path traversal detectado.',
992
+ 'praxis.filesUpload.errors.INSUFFICIENT_STORAGE': 'Sem espaço em disco.',
993
+ 'praxis.filesUpload.errors.UPLOAD_TIMEOUT': 'Tempo esgotado no upload.',
994
+ 'praxis.filesUpload.errors.BULK_UPLOAD_TIMEOUT': 'Tempo esgotado no upload em lote.',
995
+ 'praxis.filesUpload.errors.BULK_UPLOAD_CANCELLED': 'Upload em lote cancelado.',
996
+ 'praxis.filesUpload.errors.USER_CANCELLED': 'Upload cancelado pelo usuário.',
997
+ 'praxis.filesUpload.errors.UNKNOWN_ERROR': 'Erro desconhecido.',
998
+ 'praxis.filesUpload.fieldPendingSummary': 'Arquivos prontos para envio',
999
+ 'praxis.filesUpload.fieldSelectedSummary': 'Arquivo selecionado',
1000
+ 'praxis.filesUpload.fieldSelectedPlural': 'arquivos vinculados',
1001
+ 'praxis.filesUpload.fieldEmptySummary': 'Nenhum arquivo selecionado',
1002
+ 'praxis.filesUpload.fieldErrorSummary': 'Corrija o erro para continuar',
1003
+ 'praxis.filesUpload.fieldActionUpload': 'Enviar arquivos selecionados',
1004
+ 'praxis.filesUpload.fieldActionUploadShort': 'Enviar',
1005
+ 'praxis.filesUpload.fieldActionCancel': 'Cancelar seleção atual',
1006
+ 'praxis.filesUpload.fieldActionCancelShort': 'Cancelar',
1007
+ 'praxis.filesUpload.fieldActionSelect': 'Selecionar arquivo(s)',
1008
+ 'praxis.filesUpload.fieldActionClear': 'Limpar',
1009
+ 'praxis.filesUpload.fieldInfoAction': 'Informações',
1010
+ 'praxis.filesUpload.fieldMoreActions': 'Mais ações',
1011
+ 'praxis.filesUpload.fieldPendingAriaSuffix': 'arquivos prontos para envio',
1012
+ 'praxis.filesUpload.selectedOverlayAriaLabel': 'Arquivos selecionados',
1013
+ 'praxis.filesUpload.selectedOverlayTitle': 'Selecionados',
1014
+ 'praxis.filesUpload.bulkSummaryTotal': 'Total',
1015
+ 'praxis.filesUpload.bulkSummarySuccess': 'Sucesso',
1016
+ 'praxis.filesUpload.bulkSummaryFailed': 'Falhas',
1017
+ 'praxis.filesUpload.resultMetaIdLabel': 'ID',
1018
+ 'praxis.filesUpload.resultMetaTypeLabel': 'Tipo',
1019
+ 'praxis.filesUpload.resultMetaSizeLabel': 'Tamanho',
1020
+ 'praxis.filesUpload.resultMetaUploadedAtLabel': 'Enviado em',
1021
+ 'praxis.filesUpload.policySummaryAny': 'qualquer',
1022
+ 'praxis.filesUpload.policySummaryNotConfigured': '—',
1023
+ 'praxis.filesUpload.policySummaryTypesLabel': 'Tipos',
1024
+ 'praxis.filesUpload.policySummaryMaxPerFileLabel': 'Máx. por arquivo',
1025
+ 'praxis.filesUpload.policySummaryQuantityLabel': 'Qtde',
1026
+ 'praxis.filesUpload.showAll': 'Ver todos',
1027
+ 'praxis.filesUpload.showLess': 'Ver menos',
1028
+ };
1029
+
1030
+ /** @deprecated Transitional compatibility token. Prefer PraxisI18nService. */
1031
+ const FILES_UPLOAD_TEXTS = new InjectionToken('FILES_UPLOAD_TEXTS', {
1032
+ providedIn: 'root',
1033
+ factory: () => ({
1034
+ settingsAriaLabel: 'Abrir configurações',
1035
+ dropzoneLabel: 'Arraste arquivos ou',
1036
+ dropzoneButton: 'selecionar',
1037
+ conflictPolicyLabel: 'Política de conflito',
1038
+ metadataLabel: 'Metadados (JSON)',
1039
+ progressAriaLabel: 'Progresso do upload',
1040
+ rateLimitBanner: 'Limite de requisições excedido. Tente novamente às',
1041
+ }),
1042
+ });
1043
+ /** @deprecated Transitional compatibility token. Prefer PraxisI18nService. */
1044
+ const TRANSLATE_LIKE = new InjectionToken('TRANSLATE_LIKE');
1045
+ function createPraxisFilesUploadI18nConfig(options = {}) {
1046
+ const dictionaries = {
1047
+ 'pt-BR': {
1048
+ ...FILES_UPLOAD_PT_BR,
1049
+ ...(options.dictionaries?.['pt-BR'] ?? {}),
1050
+ },
1051
+ 'en-US': {
1052
+ ...FILES_UPLOAD_EN_US,
1053
+ ...(options.dictionaries?.['en-US'] ?? {}),
1054
+ },
1055
+ };
1056
+ for (const [locale, dictionary] of Object.entries(options.dictionaries ?? {})) {
1057
+ if (locale === 'pt-BR' || locale === 'en-US') {
1058
+ continue;
1059
+ }
1060
+ dictionaries[locale] = {
1061
+ ...(dictionaries[locale] ?? {}),
1062
+ ...dictionary,
1063
+ };
1064
+ }
1065
+ return {
1066
+ locale: options.locale,
1067
+ fallbackLocale: options.fallbackLocale ?? 'pt-BR',
1068
+ dictionaries,
1069
+ };
1556
1070
  }
1557
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisFilesUploadConfigEditor, decorators: [{
1558
- type: Component,
1559
- args: [{ selector: 'praxis-files-upload-config-editor', standalone: true, imports: [
1560
- CommonModule,
1561
- ReactiveFormsModule,
1562
- MatTabsModule,
1563
- MatFormFieldModule,
1564
- MatInputModule,
1565
- MatCheckboxModule,
1566
- MatSelectModule,
1567
- MatButtonModule,
1568
- MatIconModule,
1569
- MatTooltipModule,
1570
- MatSnackBarModule,
1571
- FormsModule,
1572
- ], template: `
1071
+ function providePraxisFilesUploadI18n(options = {}) {
1072
+ return providePraxisI18n(createPraxisFilesUploadI18nConfig(options));
1073
+ }
1074
+ function resolvePraxisFilesUploadText(i18n, key, fallback, legacyTexts, legacyTranslate) {
1075
+ const namespacedKey = key.startsWith('praxis.filesUpload.')
1076
+ ? key
1077
+ : `praxis.filesUpload.${key}`;
1078
+ const coreTranslation = i18n.t(namespacedKey, undefined, '');
1079
+ if (coreTranslation) {
1080
+ return coreTranslation;
1081
+ }
1082
+ const legacyText = legacyTexts?.[key];
1083
+ if (legacyText) {
1084
+ return legacyText;
1085
+ }
1086
+ const translatedByLegacyService = legacyTranslate?.t?.(namespacedKey, undefined, '') ||
1087
+ legacyTranslate?.t?.(key, undefined, '') ||
1088
+ resolveLegacyInstantTranslation(legacyTranslate, namespacedKey) ||
1089
+ resolveLegacyInstantTranslation(legacyTranslate, key);
1090
+ if (translatedByLegacyService) {
1091
+ return translatedByLegacyService;
1092
+ }
1093
+ return fallback;
1094
+ }
1095
+ function resolveLegacyInstantTranslation(legacyTranslate, key) {
1096
+ const translated = legacyTranslate?.instant?.(key);
1097
+ return translated && translated !== key ? translated : '';
1098
+ }
1099
+
1100
+ class PraxisFilesUploadConfigEditor {
1101
+ fb;
1102
+ panelData;
1103
+ snackBar;
1104
+ destroyRef;
1105
+ i18n = inject(PraxisI18nService);
1106
+ form;
1107
+ // Integração com backend: baseUrl e sinais de configuração efetiva
1108
+ baseUrl;
1109
+ state;
1110
+ serverLoading = () => (this.state ? this.state.loading() : false);
1111
+ serverData = () => (this.state ? this.state.data() : undefined);
1112
+ serverError = () => (this.state ? this.state.error() : undefined);
1113
+ get uiGroup() {
1114
+ return this.form.get('ui');
1115
+ }
1116
+ get dropzoneGroup() {
1117
+ return this.form.get('ui').get('dropzone');
1118
+ }
1119
+ get listGroup() {
1120
+ return this.form.get('ui').get('list');
1121
+ }
1122
+ get limitsGroup() {
1123
+ return this.form.get('limits');
1124
+ }
1125
+ get optionsGroup() {
1126
+ return this.form.get('options');
1127
+ }
1128
+ get quotasGroup() {
1129
+ return this.form.get('quotas');
1130
+ }
1131
+ get rateLimitGroup() {
1132
+ return this.form.get('rateLimit');
1133
+ }
1134
+ get bulkGroup() {
1135
+ return this.form.get('bulk');
1136
+ }
1137
+ get messagesGroup() {
1138
+ return this.form.get('messages');
1139
+ }
1140
+ get headersGroup() {
1141
+ return this.form.get('headers');
1142
+ }
1143
+ get errorsGroup() {
1144
+ return this.messagesGroup.get('errors');
1145
+ }
1146
+ errorCodes = Object.values(ErrorCode);
1147
+ errorEntries = [];
1148
+ isDirty$ = new BehaviorSubject(false);
1149
+ isValid$;
1150
+ isBusy$ = new BehaviorSubject(false);
1151
+ jsonError = null;
1152
+ tx(key, fallback) {
1153
+ return resolvePraxisFilesUploadText(this.i18n, key, fallback);
1154
+ }
1155
+ getFriendlyErrorLabel(code) {
1156
+ const normalized = String(code);
1157
+ return this.tx(`editor.errors.${normalized}`, normalized);
1158
+ }
1159
+ constructor(fb, panelData, snackBar, destroyRef) {
1160
+ this.fb = fb;
1161
+ this.panelData = panelData;
1162
+ this.snackBar = snackBar;
1163
+ this.destroyRef = destroyRef;
1164
+ this.form = this.fb.group({
1165
+ strategy: ['direct'],
1166
+ ui: this.fb.group({
1167
+ showDropzone: [true],
1168
+ showProgress: [true],
1169
+ showConflictPolicySelector: [true],
1170
+ manualUpload: [false],
1171
+ dense: [false],
1172
+ accept: [''],
1173
+ showMetadataForm: [false],
1174
+ // Grupos específicos de UI
1175
+ dropzone: this.fb.group({
1176
+ expandOnDragProximity: [true],
1177
+ proximityPx: [64],
1178
+ expandMode: ['overlay'],
1179
+ expandHeight: [200],
1180
+ expandDebounceMs: [120],
1181
+ }),
1182
+ list: this.fb.group({
1183
+ collapseAfter: [5],
1184
+ detailsMode: ['auto'],
1185
+ detailsMaxWidth: [480],
1186
+ detailsShowTechnical: [false],
1187
+ detailsFields: [''], // CSV na UI
1188
+ detailsAnchor: ['item'],
1189
+ }),
1190
+ }),
1191
+ limits: this.fb.group({
1192
+ maxFileSizeBytes: [null],
1193
+ maxFilesPerBulk: [null],
1194
+ maxBulkSizeBytes: [null],
1195
+ }),
1196
+ options: this.fb.group({
1197
+ defaultConflictPolicy: ['RENAME'],
1198
+ strictValidation: [true],
1199
+ maxUploadSizeMb: [50],
1200
+ allowedExtensions: [''],
1201
+ acceptMimeTypes: [''],
1202
+ targetDirectory: [''],
1203
+ enableVirusScanning: [false],
1204
+ }),
1205
+ bulk: this.fb.group({
1206
+ failFast: [false],
1207
+ parallelUploads: [1],
1208
+ retryCount: [0],
1209
+ retryBackoffMs: [0],
1210
+ }),
1211
+ quotas: this.fb.group({
1212
+ showQuotaWarnings: [false],
1213
+ blockOnExceed: [false],
1214
+ }),
1215
+ rateLimit: this.fb.group({
1216
+ autoRetryOn429: [false],
1217
+ showBannerOn429: [true],
1218
+ maxAutoRetry: [0],
1219
+ baseBackoffMs: [0],
1220
+ }),
1221
+ headers: this.fb.group({
1222
+ tenantHeader: ['X-Tenant-Id'],
1223
+ userHeader: ['X-User-Id'],
1224
+ // Valores usados nas consultas de configuração do servidor
1225
+ tenantValue: [''],
1226
+ userValue: [''],
1227
+ }),
1228
+ messages: this.fb.group({
1229
+ successSingle: [''],
1230
+ successBulk: [''],
1231
+ errors: this.fb.group({}),
1232
+ }),
1233
+ });
1234
+ this.errorEntries = this.errorCodes.map((code) => ({
1235
+ code,
1236
+ label: this.getFriendlyErrorLabel(code),
1237
+ }));
1238
+ const errorsGroup = this.errorsGroup;
1239
+ this.errorCodes.forEach((code) => {
1240
+ errorsGroup.addControl(code, this.fb.control(''));
1241
+ });
1242
+ this.isValid$ = this.form.statusChanges.pipe(map$1((s) => s === 'VALID'), startWith(this.form.valid));
1243
+ // Definir baseUrl e inicializar hook em contexto de injeção
1244
+ this.baseUrl = this.panelData?.baseUrl ?? this.panelData?.__baseUrl;
1245
+ this.state = useEffectiveUploadConfig(this.baseUrl ?? '/api/files', () => this.getHeadersForFetch());
1246
+ // Observa mudanças na configuração efetiva do servidor (em contexto de injeção)
1247
+ toObservable(this.state.data)
1248
+ .pipe(takeUntilDestroyed(this.destroyRef))
1249
+ .subscribe((cfg) => this.applyServerConfig(cfg));
1250
+ }
1251
+ ngOnInit() {
1252
+ // baseUrl opcional vinda do componente pai (Settings Panel inputs)
1253
+ // (já definida no construtor)
1254
+ if (this.panelData) {
1255
+ const patch = { ...this.panelData };
1256
+ if (patch.ui?.accept) {
1257
+ patch.ui = { ...patch.ui, accept: patch.ui.accept.join(',') };
1258
+ }
1259
+ // Normalizar lista de detalhes para CSV
1260
+ if (patch.ui?.list?.detailsFields) {
1261
+ patch.ui = {
1262
+ ...patch.ui,
1263
+ list: {
1264
+ ...patch.ui.list,
1265
+ detailsFields: patch.ui.list.detailsFields.join(','),
1266
+ },
1267
+ };
1268
+ }
1269
+ if (patch.options?.allowedExtensions) {
1270
+ patch.options = {
1271
+ ...patch.options,
1272
+ allowedExtensions: patch.options.allowedExtensions.join(','),
1273
+ };
1274
+ }
1275
+ if (patch.options?.acceptMimeTypes) {
1276
+ patch.options = {
1277
+ ...patch.options,
1278
+ acceptMimeTypes: patch.options.acceptMimeTypes.join(','),
1279
+ };
1280
+ }
1281
+ this.form.patchValue(patch);
1282
+ }
1283
+ // Sempre que cabeçalhos mudarem, atualiza contexto da consulta (e refaz fetch)
1284
+ this.headersGroup.valueChanges.subscribe(() => {
1285
+ this.state.refetch();
1286
+ });
1287
+ this.form.valueChanges.subscribe(() => this.isDirty$.next(true));
1288
+ }
1289
+ applyServerConfig(cfg) {
1290
+ if (!cfg)
1291
+ return;
1292
+ // Configuração efetiva do backend separada em validações locais, opções
1293
+ // de backend e execução de lote.
1294
+ const options = cfg.options ?? {};
1295
+ const bulk = cfg.bulk ?? {};
1296
+ // Validações locais alimentadas pelo backend
1297
+ this.limitsGroup.patchValue({
1298
+ // Preencher máx. arquivos por lote a partir do backend
1299
+ maxFilesPerBulk: typeof bulk.maxFilesPerBatch === 'number'
1300
+ ? bulk.maxFilesPerBatch
1301
+ : null,
1302
+ }, { emitEvent: false });
1303
+ // Opções de backend (normalizar arrays em string CSV)
1304
+ const allowed = Array.isArray(options.allowedExtensions)
1305
+ ? options.allowedExtensions.join(',')
1306
+ : '';
1307
+ const mimes = Array.isArray(options.acceptMimeTypes)
1308
+ ? options.acceptMimeTypes.join(',')
1309
+ : '';
1310
+ this.optionsGroup.patchValue({
1311
+ defaultConflictPolicy: options.nameConflictPolicy ?? null,
1312
+ strictValidation: options.strictValidation ?? null,
1313
+ maxUploadSizeMb: options.maxUploadSizeMb ?? null,
1314
+ allowedExtensions: allowed,
1315
+ acceptMimeTypes: mimes,
1316
+ targetDirectory: options.targetDirectory ?? '',
1317
+ enableVirusScanning: !!options.enableVirusScanning,
1318
+ }, { emitEvent: false });
1319
+ // Execução de lote
1320
+ this.bulkGroup.patchValue({
1321
+ failFast: bulk.failFastModeDefault ?? false,
1322
+ parallelUploads: typeof bulk.maxConcurrentUploads === 'number'
1323
+ ? bulk.maxConcurrentUploads
1324
+ : 1,
1325
+ }, { emitEvent: false });
1326
+ // rate limit (somente leitura na UI)
1327
+ this.rateLimitGroup.patchValue({
1328
+ // Mantemos flags de UI; os números vêm do servidor e são mostrados no resumo
1329
+ }, { emitEvent: false });
1330
+ // quotas (somente preferências de UI)
1331
+ this.quotasGroup.patchValue({}, { emitEvent: false });
1332
+ // Mensagens de erro do servidor
1333
+ const serverMessages = cfg.messages ?? {};
1334
+ const errorsGroup = this.errorsGroup;
1335
+ const alias = {
1336
+ INVALID_TYPE: ErrorCode.INVALID_FILE_TYPE,
1337
+ UNSUPPORTED_FILE_TYPE: ErrorCode.FMT_UNSUPPORTED,
1338
+ MAGIC_NUMBER_MISMATCH: ErrorCode.FMT_MAGIC_MISMATCH,
1339
+ CORRUPTED_FILE: ErrorCode.FMT_CORRUPTED,
1340
+ FILE_TOO_LARGE: ErrorCode.FILE_TOO_LARGE,
1341
+ RATE_LIMIT_EXCEEDED: ErrorCode.RATE_LIMIT_EXCEEDED,
1342
+ QUOTA_EXCEEDED: ErrorCode.QUOTA_EXCEEDED,
1343
+ INTERNAL_ERROR: ErrorCode.INTERNAL_ERROR,
1344
+ UNKNOWN_ERROR: ErrorCode.INTERNAL_ERROR,
1345
+ MALWARE_DETECTED: ErrorCode.SEC_MALICIOUS_CONTENT,
1346
+ VIRUS_DETECTED: ErrorCode.SEC_VIRUS_DETECTED,
1347
+ DANGEROUS_FILE_TYPE: ErrorCode.SEC_DANGEROUS_TYPE,
1348
+ DANGEROUS_EXECUTABLE: ErrorCode.SEC_DANGEROUS_TYPE,
1349
+ FILE_STORE_ERROR: ErrorCode.SYS_STORAGE_ERROR,
1350
+ SYS_STORAGE_ERROR: ErrorCode.SYS_STORAGE_ERROR,
1351
+ };
1352
+ Object.keys(serverMessages).forEach((rawCode) => {
1353
+ const mapped = alias[rawCode] ?? ErrorCode[rawCode];
1354
+ const ctrlName = mapped ? String(mapped) : rawCode;
1355
+ if (!errorsGroup.get(ctrlName)) {
1356
+ errorsGroup.addControl(ctrlName, this.fb.control(''));
1357
+ }
1358
+ if (!this.errorEntries.some((e) => e.code === ctrlName)) {
1359
+ const meta = getErrorMeta(rawCode);
1360
+ const label = meta.title || ctrlName;
1361
+ this.errorEntries.push({ code: ctrlName, label });
1362
+ }
1363
+ errorsGroup
1364
+ .get(ctrlName)
1365
+ ?.patchValue(serverMessages[rawCode], { emitEvent: false });
1366
+ });
1367
+ }
1368
+ getSettingsValue() {
1369
+ const value = this.form.value;
1370
+ // accept (UI)
1371
+ const accept = this.uiGroup.get('accept')?.value;
1372
+ if (accept !== undefined) {
1373
+ value.ui = value.ui ?? {};
1374
+ value.ui.accept = accept
1375
+ .split(',')
1376
+ .map((s) => s.trim())
1377
+ .filter((s) => !!s);
1378
+ }
1379
+ // NOVO: detailsFields (UI)
1380
+ const df = this.uiGroup.get('list')?.get('detailsFields')
1381
+ ?.value;
1382
+ if (df !== undefined) {
1383
+ value.ui = value.ui ?? {};
1384
+ value.ui.list = value.ui.list ?? {};
1385
+ value.ui.list.detailsFields = df
1386
+ .split(',')
1387
+ .map((s) => s.trim())
1388
+ .filter((s) => !!s);
1389
+ }
1390
+ // options.allowedExtensions
1391
+ const allowedExt = this.optionsGroup.get('allowedExtensions')
1392
+ ?.value;
1393
+ if (allowedExt !== undefined) {
1394
+ value.options = value.options ?? {};
1395
+ value.options.allowedExtensions = allowedExt
1396
+ .split(',')
1397
+ .map((s) => s.trim())
1398
+ .filter((s) => !!s);
1399
+ }
1400
+ // options.acceptMimeTypes
1401
+ const mime = this.optionsGroup.get('acceptMimeTypes')?.value;
1402
+ if (mime !== undefined) {
1403
+ value.options = value.options ?? {};
1404
+ value.options.acceptMimeTypes = mime
1405
+ .split(',')
1406
+ .map((s) => s.trim())
1407
+ .filter((s) => !!s);
1408
+ }
1409
+ return value;
1410
+ }
1411
+ copyServerConfig() {
1412
+ const data = this.serverData();
1413
+ if (!data) {
1414
+ return;
1415
+ }
1416
+ const json = JSON.stringify(data, null, 2);
1417
+ if (navigator?.clipboard?.writeText) {
1418
+ navigator.clipboard.writeText(json);
1419
+ this.snackBar.open(this.tx('editor.snackbar.configCopied', 'Configuration copied'), undefined, {
1420
+ duration: 2000,
1421
+ });
1422
+ }
1423
+ }
1424
+ onJsonChange(json) {
1425
+ try {
1426
+ const parsed = JSON.parse(json);
1427
+ this.form.patchValue(parsed);
1428
+ this.jsonError = null;
1429
+ }
1430
+ catch {
1431
+ this.jsonError = this.tx('editor.json.invalid', 'Invalid JSON: check the syntax.');
1432
+ this.snackBar.open(this.tx('editor.snackbar.invalidJson', 'Invalid JSON'), undefined, { duration: 2000 });
1433
+ }
1434
+ }
1435
+ getHeadersForFetch() {
1436
+ const h = this.headersGroup.value;
1437
+ const headers = {};
1438
+ if (h?.tenantHeader && h?.tenantValue)
1439
+ headers[h.tenantHeader] = h.tenantValue;
1440
+ if (h?.userHeader && h?.userValue)
1441
+ headers[h.userHeader] = h.userValue;
1442
+ return headers;
1443
+ }
1444
+ refetchServerConfig() {
1445
+ this.state.refetch();
1446
+ }
1447
+ get summaryStrategyTitle() {
1448
+ const strategy = this.form.get('strategy')?.value;
1449
+ if (strategy === 'presign') {
1450
+ return this.tx('editor.summary.strategy.presign', 'Upload with presigned URL');
1451
+ }
1452
+ if (strategy === 'auto') {
1453
+ return this.tx('editor.summary.strategy.auto', 'Automatic strategy selection');
1454
+ }
1455
+ return this.tx('editor.summary.strategy.direct', 'Direct upload to backend');
1456
+ }
1457
+ get summaryStrategyDetail() {
1458
+ const strategy = this.form.get('strategy')?.value;
1459
+ if (strategy === 'presign') {
1460
+ return this.tx('editor.summary.strategy.presignDetail', 'Depends on a presign endpoint and usually reduces direct load on the main server.');
1461
+ }
1462
+ if (strategy === 'auto') {
1463
+ return this.tx('editor.summary.strategy.autoDetail', 'Tries presign first and falls back to the direct flow when needed.');
1464
+ }
1465
+ return this.tx('editor.summary.strategy.directDetail', 'Simpler flow to integrate and diagnose in controlled environments.');
1466
+ }
1467
+ get summaryExperienceTitle() {
1468
+ const ui = this.uiGroup.value;
1469
+ if (ui?.manualUpload) {
1470
+ return this.tx('editor.summary.experience.manual', 'Manual review before upload');
1471
+ }
1472
+ if (ui?.showDropzone === false) {
1473
+ return this.tx('editor.summary.experience.compact', 'Selection guided by compact field');
1474
+ }
1475
+ return this.tx('editor.summary.experience.dropzone', 'Immediate upload with visible dropzone');
1476
+ }
1477
+ get summaryExperienceDetail() {
1478
+ const ui = this.uiGroup.value;
1479
+ const tokens = [
1480
+ ui?.showProgress
1481
+ ? this.tx('editor.summary.experience.progressVisible', 'visible progress')
1482
+ : this.tx('editor.summary.experience.progressHidden', 'no progress bar'),
1483
+ ui?.showConflictPolicySelector
1484
+ ? this.tx('editor.summary.experience.conflictVisible', 'conflict selection available')
1485
+ : this.tx('editor.summary.experience.conflictHidden', 'conflict policy hidden'),
1486
+ ui?.showMetadataForm
1487
+ ? this.tx('editor.summary.experience.metadataVisible', 'editable metadata in the UI')
1488
+ : this.tx('editor.summary.experience.metadataHidden', 'no extra form'),
1489
+ ];
1490
+ return tokens.join(' • ');
1491
+ }
1492
+ get summaryLimitsTitle() {
1493
+ const maxFile = this.optionsGroup.get('maxUploadSizeMb')?.value;
1494
+ const maxBulk = this.limitsGroup.get('maxFilesPerBulk')?.value;
1495
+ return this.tx('editor.summary.limits.titleValue', '{maxFile} MB per file • {maxBulk} items per batch')
1496
+ .replace('{maxFile}', String(maxFile || '—'))
1497
+ .replace('{maxBulk}', String(maxBulk || '—'));
1498
+ }
1499
+ get summaryLimitsDetail() {
1500
+ const parallel = this.bulkGroup.get('parallelUploads')?.value;
1501
+ const failFast = this.bulkGroup.get('failFast')?.value;
1502
+ return this.tx('editor.summary.limits.detailValue', '{parallel} parallel upload(s) • {mode}')
1503
+ .replace('{parallel}', String(parallel || 1))
1504
+ .replace('{mode}', failFast
1505
+ ? this.tx('editor.summary.limits.failFast', 'fail-fast enabled')
1506
+ : this.tx('editor.summary.limits.partialFailures', 'partial failures allowed'));
1507
+ }
1508
+ get summaryServerTitle() {
1509
+ const conflict = this.optionsGroup.get('defaultConflictPolicy')?.value || 'RENAME';
1510
+ return this.tx('editor.summary.server.titleValue', 'Default conflict: {conflict}').replace('{conflict}', String(conflict));
1511
+ }
1512
+ get summaryServerDetail() {
1513
+ const strict = this.optionsGroup.get('strictValidation')?.value;
1514
+ const virus = this.optionsGroup.get('enableVirusScanning')?.value;
1515
+ return this.tx('editor.summary.server.detailValue', '{strict} • {virus}')
1516
+ .replace('{strict}', strict
1517
+ ? this.tx('editor.summary.server.strictOn', 'strict validation enabled')
1518
+ : this.tx('editor.summary.server.strictOff', 'strict validation disabled'))
1519
+ .replace('{virus}', virus
1520
+ ? this.tx('editor.summary.server.virusOn', 'forced antivirus')
1521
+ : this.tx('editor.summary.server.virusOff', 'optional/inactive antivirus'));
1522
+ }
1523
+ get activeRisks() {
1524
+ const risks = [];
1525
+ if (this.optionsGroup.get('defaultConflictPolicy')?.value === 'OVERWRITE') {
1526
+ risks.push({
1527
+ title: this.tx('editor.risks.overwrite.title', 'Overwrite enabled'),
1528
+ detail: this.tx('editor.risks.overwrite.detail', 'Existing files may be silently replaced if the backend accepts the operation.'),
1529
+ });
1530
+ }
1531
+ if (this.optionsGroup.get('enableVirusScanning')?.value === true) {
1532
+ risks.push({
1533
+ title: this.tx('editor.risks.virus.title', 'Antivirus may increase latency'),
1534
+ detail: this.tx('editor.risks.virus.detail', 'Forced scanning improves security, but may impact throughput and response time.'),
1535
+ });
1536
+ }
1537
+ if (this.bulkGroup.get('failFast')?.value === true) {
1538
+ risks.push({
1539
+ title: this.tx('editor.risks.failFast.title', 'Batch stops on the first error'),
1540
+ detail: this.tx('editor.risks.failFast.detail', 'Users may need to resend valid items when the first error interrupts the rest of the batch.'),
1541
+ });
1542
+ }
1543
+ if ((this.rateLimitGroup.get('maxAutoRetry')?.value ?? 0) >= 5) {
1544
+ risks.push({
1545
+ title: this.tx('editor.risks.retry.title', 'Aggressive automatic retry'),
1546
+ detail: this.tx('editor.risks.retry.detail', 'Too many automatic retries may increase perceived wait time and operational noise.'),
1547
+ });
1548
+ }
1549
+ if (this.uiGroup.get('manualUpload')?.value === true) {
1550
+ risks.push({
1551
+ title: this.tx('editor.risks.manual.title', 'Flow requires manual action'),
1552
+ detail: this.tx('editor.risks.manual.detail', 'Suitable for review, but adds upload steps and may reduce completion rate in simple scenarios.'),
1553
+ });
1554
+ }
1555
+ return risks;
1556
+ }
1557
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisFilesUploadConfigEditor, deps: [{ token: i1$1.FormBuilder }, { token: SETTINGS_PANEL_DATA }, { token: i2.MatSnackBar }, { token: i0.DestroyRef }], target: i0.ɵɵFactoryTarget.Component });
1558
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.17", type: PraxisFilesUploadConfigEditor, isStandalone: true, selector: "praxis-files-upload-config-editor", providers: [providePraxisFilesUploadI18n()], ngImport: i0, template: `
1573
1559
  <div class="editor-layout">
1574
- <aside class="summary-panel" aria-label="Resumo executivo da configuração">
1575
- <h3>Resumo executivo</h3>
1560
+ <aside class="summary-panel" [attr.aria-label]="tx('editor.summary.ariaLabel', 'Executive summary for the current configuration')">
1561
+ <h3>{{ tx('editor.summary.title', 'Executive summary') }}</h3>
1576
1562
  <p class="summary-intro">
1577
- Revise aqui o impacto operacional antes de percorrer todos os grupos
1578
- de configuração.
1563
+ {{ tx('editor.summary.intro', 'Review the operational impact here before going through all configuration groups.') }}
1579
1564
  </p>
1580
1565
 
1581
1566
  <div class="summary-grid">
1582
1567
  <section class="summary-card">
1583
- <span class="eyebrow">Estratégia</span>
1568
+ <span class="eyebrow">{{ tx('editor.summary.strategy', 'Strategy') }}</span>
1584
1569
  <strong>{{ summaryStrategyTitle }}</strong>
1585
1570
  <p>{{ summaryStrategyDetail }}</p>
1586
1571
  </section>
1587
1572
 
1588
1573
  <section class="summary-card">
1589
- <span class="eyebrow">Experiência</span>
1574
+ <span class="eyebrow">{{ tx('editor.summary.experience', 'Experience') }}</span>
1590
1575
  <strong>{{ summaryExperienceTitle }}</strong>
1591
1576
  <p>{{ summaryExperienceDetail }}</p>
1592
1577
  </section>
1593
1578
 
1594
1579
  <section class="summary-card">
1595
- <span class="eyebrow">Limites</span>
1580
+ <span class="eyebrow">{{ tx('editor.summary.limits', 'Limits') }}</span>
1596
1581
  <strong>{{ summaryLimitsTitle }}</strong>
1597
1582
  <p>{{ summaryLimitsDetail }}</p>
1598
1583
  </section>
1599
1584
 
1600
1585
  <section class="summary-card">
1601
- <span class="eyebrow">Servidor</span>
1586
+ <span class="eyebrow">{{ tx('editor.summary.server', 'Server') }}</span>
1602
1587
  <strong>{{ summaryServerTitle }}</strong>
1603
1588
  <p>{{ summaryServerDetail }}</p>
1604
1589
  </section>
@@ -1609,7 +1594,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1609
1594
  <mat-icon aria-hidden="true">{{
1610
1595
  activeRisks.length === 0 ? 'verified' : 'warning'
1611
1596
  }}</mat-icon>
1612
- Riscos e atenção
1597
+ {{ tx('editor.risks.title', 'Risks and attention points') }}
1613
1598
  </h4>
1614
1599
  <div *ngIf="activeRisks.length > 0; else noRiskState" class="risk-list">
1615
1600
  <article class="risk-item" *ngFor="let risk of activeRisks">
@@ -1619,7 +1604,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1619
1604
  </div>
1620
1605
  <ng-template #noRiskState>
1621
1606
  <p class="safe-copy">
1622
- Nenhum risco operacional evidente na configuração atual.
1607
+ {{ tx('editor.risks.none', 'No evident operational risk in the current configuration.') }}
1623
1608
  </p>
1624
1609
  </ng-template>
1625
1610
  </section>
@@ -1627,24 +1612,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1627
1612
 
1628
1613
  <div class="editor-main">
1629
1614
  <mat-tab-group>
1630
- <mat-tab label="Comportamento">
1615
+ <mat-tab [label]="tx('editor.tabs.behavior', 'Behavior')">
1631
1616
  <div class="tab-panel">
1632
1617
  <section class="tab-intro">
1633
- <h3>Estratégia e cadência do envio</h3>
1618
+ <h3>{{ tx('editor.behavior.intro.title', 'Upload strategy and cadence') }}</h3>
1634
1619
  <p>
1635
- Defina aqui como o usuário inicia o upload e como o lote se
1636
- comporta em termos de paralelismo e retentativa.
1620
+ {{ tx('editor.behavior.intro.body', 'Define how the user starts the upload and how the batch behaves in terms of parallelism and retries.') }}
1637
1621
  </p>
1638
1622
  </section>
1639
1623
  <section class="config-section">
1640
1624
  <form [formGroup]="form">
1641
1625
  <mat-form-field appearance="fill">
1642
- <mat-label>Estratégia de envio <span class="opt-tag">opcional</span></mat-label>
1626
+ <mat-label>{{ tx('editor.behavior.strategy.label', 'Upload strategy') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1643
1627
  <mat-select formControlName="strategy">
1644
- <mat-option value="direct">Direto (HTTP padrão)</mat-option>
1645
- <mat-option value="presign">URL pré-assinada (S3/GCS)</mat-option>
1628
+ <mat-option value="direct">{{ tx('editor.behavior.strategy.direct', 'Direct (standard HTTP)') }}</mat-option>
1629
+ <mat-option value="presign">{{ tx('editor.behavior.strategy.presign', 'Presigned URL (S3/GCS)') }}</mat-option>
1646
1630
  <mat-option value="auto"
1647
- >Automático (tenta pré-assinada e volta ao direto)</mat-option
1631
+ >{{ tx('editor.behavior.strategy.auto', 'Automatic (tries presigned first and falls back to direct)') }}</mat-option
1648
1632
  >
1649
1633
  </mat-select>
1650
1634
  <button
@@ -1652,7 +1636,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1652
1636
  matSuffix
1653
1637
  class="help-icon-button"
1654
1638
  type="button"
1655
- [matTooltip]="'Como os arquivos serão enviados ao servidor.'"
1639
+ [matTooltip]="tx('editor.behavior.strategy.hint', 'How files will be sent to the server.')"
1656
1640
  matTooltipPosition="above"
1657
1641
  >
1658
1642
  <mat-icon>help_outline</mat-icon>
@@ -1664,50 +1648,48 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1664
1648
  <form [formGroup]="bulkGroup">
1665
1649
  <h4 class="section-subtitle">
1666
1650
  <mat-icon aria-hidden="true">build</mat-icon>
1667
- Opções configuráveisLote
1651
+ {{ tx('editor.behavior.bulk.title', 'Configurable options Batch') }}
1668
1652
  </h4>
1669
1653
  <p class="section-note">
1670
- Use paralelismo e retries com moderação. Em ambientes corporativos,
1671
- esses controles afetam throughput, experiência percebida e carga no
1672
- backend.
1654
+ {{ tx('editor.behavior.bulk.note', 'Use parallelism and retries in moderation. In enterprise environments these controls affect throughput, perceived experience and backend load.') }}
1673
1655
  </p>
1674
1656
  <mat-form-field appearance="fill">
1675
- <mat-label>Uploads paralelos <span class="opt-tag">opcional</span></mat-label>
1657
+ <mat-label>{{ tx('editor.behavior.bulk.parallel', 'Parallel uploads') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1676
1658
  <input matInput type="number" formControlName="parallelUploads" />
1677
1659
  <button
1678
1660
  mat-icon-button
1679
1661
  matSuffix
1680
1662
  class="help-icon-button"
1681
1663
  type="button"
1682
- [matTooltip]="'Quantos arquivos enviar ao mesmo tempo.'"
1664
+ [matTooltip]="tx('editor.behavior.bulk.parallelHint', 'How many files to send at the same time.')"
1683
1665
  matTooltipPosition="above"
1684
1666
  >
1685
1667
  <mat-icon>help_outline</mat-icon>
1686
1668
  </button>
1687
1669
  </mat-form-field>
1688
1670
  <mat-form-field appearance="fill">
1689
- <mat-label>Número de tentativas <span class="opt-tag">opcional</span></mat-label>
1671
+ <mat-label>{{ tx('editor.behavior.bulk.retryCount', 'Retry count') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1690
1672
  <input matInput type="number" formControlName="retryCount" />
1691
1673
  <button
1692
1674
  mat-icon-button
1693
1675
  matSuffix
1694
1676
  class="help-icon-button"
1695
1677
  type="button"
1696
- [matTooltip]="'Tentativas automáticas em caso de falha.'"
1678
+ [matTooltip]="tx('editor.behavior.bulk.retryCountHint', 'Automatic attempts in case of failure.')"
1697
1679
  matTooltipPosition="above"
1698
1680
  >
1699
1681
  <mat-icon>help_outline</mat-icon>
1700
1682
  </button>
1701
1683
  </mat-form-field>
1702
1684
  <mat-form-field appearance="fill">
1703
- <mat-label>Intervalo entre tentativas (ms) <span class="opt-tag">opcional</span></mat-label>
1685
+ <mat-label>{{ tx('editor.behavior.bulk.retryBackoff', 'Retry interval (ms)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1704
1686
  <input matInput type="number" formControlName="retryBackoffMs" />
1705
1687
  <button
1706
1688
  mat-icon-button
1707
1689
  matSuffix
1708
1690
  class="help-icon-button"
1709
1691
  type="button"
1710
- [matTooltip]="'Tempo de espera entre tentativas.'"
1692
+ [matTooltip]="tx('editor.behavior.bulk.retryBackoffHint', 'Wait time between attempts.')"
1711
1693
  matTooltipPosition="above"
1712
1694
  >
1713
1695
  <mat-icon>help_outline</mat-icon>
@@ -1717,86 +1699,83 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1717
1699
  </section>
1718
1700
  </div>
1719
1701
  </mat-tab>
1720
- <mat-tab label="Interface">
1702
+ <mat-tab [label]="tx('editor.tabs.interface', 'Interface')">
1721
1703
  <div class="tab-panel">
1722
1704
  <section class="tab-intro">
1723
- <h3>Experiência visível para o usuário</h3>
1705
+ <h3>{{ tx('editor.interface.intro.title', 'Visible experience for the user') }}</h3>
1724
1706
  <p>
1725
- Configure densidade, dropzone, metadados e detalhes da lista.
1726
- Priorize clareza operacional antes de habilitar recursos avançados.
1707
+ {{ tx('editor.interface.intro.body', 'Configure density, dropzone, metadata and list details. Prioritize operational clarity before enabling advanced capabilities.') }}
1727
1708
  </p>
1728
1709
  </section>
1729
1710
  <section class="config-section">
1730
1711
  <form [formGroup]="uiGroup">
1731
1712
  <h4 class="section-subtitle">
1732
1713
  <mat-icon aria-hidden="true">edit</mat-icon>
1733
- Opções configuráveis — Interface
1714
+ {{ tx('editor.interface.section.title', 'Configurable options — Interface') }}
1734
1715
  </h4>
1735
1716
  <p class="section-note">
1736
- Esta seção controla a ergonomia do componente. Mudanças aqui afetam
1737
- descoberta, esforço de uso e taxa de conclusão do envio.
1717
+ {{ tx('editor.interface.section.note', 'This section controls the component ergonomics. Changes here affect discoverability, usage effort and upload completion rate.') }}
1738
1718
  </p>
1739
1719
  <mat-checkbox formControlName="showDropzone"
1740
- >Exibir área de soltar</mat-checkbox
1720
+ >{{ tx('editor.interface.showDropzone', 'Show drop area') }}</mat-checkbox
1741
1721
  >
1742
1722
  <mat-checkbox formControlName="showProgress"
1743
- >Exibir barra de progresso</mat-checkbox
1723
+ >{{ tx('editor.interface.showProgress', 'Show progress bar') }}</mat-checkbox
1744
1724
  >
1745
1725
  <mat-checkbox formControlName="showConflictPolicySelector"
1746
- >Permitir escolher a política de conflito</mat-checkbox
1726
+ >{{ tx('editor.interface.showConflictPolicySelector', 'Allow choosing the conflict policy') }}</mat-checkbox
1747
1727
  >
1748
1728
  <mat-checkbox formControlName="manualUpload"
1749
- >Exigir clique em “Enviar” (modo manual)</mat-checkbox
1750
- >
1751
- <mat-checkbox formControlName="dense">Layout compacto</mat-checkbox>
1729
+ >{{ tx('editor.interface.manualUpload', 'Require clicking "Upload" (manual mode)') }}</mat-checkbox>
1730
+ <mat-checkbox formControlName="dense">{{ tx('editor.interface.dense', 'Compact layout') }}</mat-checkbox>
1752
1731
  <mat-form-field appearance="fill">
1753
- <mat-label>Tipos permitidos (accept) <span class="opt-tag">opcional</span></mat-label>
1732
+ <mat-label>{{ tx('editor.interface.accept', 'Allowed types (accept)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1754
1733
  <input
1755
1734
  matInput
1756
1735
  formControlName="accept"
1757
- placeholder="ex.: pdf,jpg,png"
1736
+ [placeholder]="tx('editor.placeholders.accept', 'e.g.: pdf,jpg,png')"
1758
1737
  />
1759
1738
  <button
1760
1739
  mat-icon-button
1761
1740
  matSuffix
1762
1741
  class="help-icon-button"
1763
1742
  type="button"
1764
- [matTooltip]="'Lista separada por vírgula (opcional).'"
1743
+ [matTooltip]="tx('editor.shared.csvOptional', 'Comma-separated list (optional).')"
1765
1744
  matTooltipPosition="above"
1766
1745
  >
1767
1746
  <mat-icon>help_outline</mat-icon>
1768
1747
  </button>
1769
1748
  </mat-form-field>
1770
1749
  <mat-checkbox formControlName="showMetadataForm"
1771
- >Exibir formulário de metadados (JSON)</mat-checkbox
1750
+ >{{ tx('editor.interface.metadataForm', 'Show metadata form (JSON)') }}</mat-checkbox
1772
1751
  >
1773
1752
 
1774
1753
  <!-- NOVO: Grupo Dropzone -->
1775
1754
  <fieldset [formGroup]="dropzoneGroup" class="subgroup">
1776
1755
  <legend>
1777
1756
  <mat-icon aria-hidden="true">download</mat-icon>
1778
- Dropzone (expansão por proximidade)
1757
+ {{ tx('editor.interface.dropzone.title', 'Dropzone (proximity expansion)') }}
1779
1758
  </legend>
1780
1759
  <mat-checkbox formControlName="expandOnDragProximity">
1781
- Expandir ao aproximar arquivo durante arraste
1760
+ {{ tx('editor.interface.dropzone.expandOnProximity', 'Expand when a file gets close during drag') }}
1782
1761
  </mat-checkbox>
1783
1762
  <mat-form-field appearance="fill">
1784
- <mat-label>Raio de proximidade (px) <span class="opt-tag">opcional</span></mat-label>
1763
+ <mat-label>{{ tx('editor.interface.dropzone.proximity', 'Proximity radius (px)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1785
1764
  <input matInput type="number" formControlName="proximityPx" />
1786
1765
  </mat-form-field>
1787
1766
  <mat-form-field appearance="fill">
1788
- <mat-label>Modo de expansão</mat-label>
1767
+ <mat-label>{{ tx('editor.interface.dropzone.mode', 'Expansion mode') }}</mat-label>
1789
1768
  <mat-select formControlName="expandMode">
1790
- <mat-option value="overlay">Overlay (recomendado)</mat-option>
1791
- <mat-option value="inline">Inline</mat-option>
1769
+ <mat-option value="overlay">{{ tx('editor.interface.dropzone.mode.overlay', 'Overlay (recommended)') }}</mat-option>
1770
+ <mat-option value="inline">{{ tx('editor.interface.dropzone.mode.inline', 'Inline') }}</mat-option>
1792
1771
  </mat-select>
1793
1772
  </mat-form-field>
1794
1773
  <mat-form-field appearance="fill">
1795
- <mat-label>Altura do overlay (px) <span class="opt-tag">opcional</span></mat-label>
1774
+ <mat-label>{{ tx('editor.interface.dropzone.height', 'Overlay height (px)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1796
1775
  <input matInput type="number" formControlName="expandHeight" />
1797
1776
  </mat-form-field>
1798
1777
  <mat-form-field appearance="fill">
1799
- <mat-label>Debounce de arraste (ms) <span class="opt-tag">opcional</span></mat-label>
1778
+ <mat-label>{{ tx('editor.interface.dropzone.debounce', 'Drag debounce (ms)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1800
1779
  <input matInput type="number" formControlName="expandDebounceMs" />
1801
1780
  </mat-form-field>
1802
1781
  </fieldset>
@@ -1805,46 +1784,46 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1805
1784
  <fieldset [formGroup]="listGroup" class="subgroup">
1806
1785
  <legend>
1807
1786
  <mat-icon aria-hidden="true">view_list</mat-icon>
1808
- Lista e detalhes
1787
+ {{ tx('editor.interface.list.title', 'List and details') }}
1809
1788
  </legend>
1810
1789
  <mat-form-field appearance="fill">
1811
- <mat-label>Colapsar após (itens) <span class="opt-tag">opcional</span></mat-label>
1790
+ <mat-label>{{ tx('editor.interface.list.collapseAfter', 'Collapse after (items)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1812
1791
  <input matInput type="number" formControlName="collapseAfter" />
1813
1792
  </mat-form-field>
1814
1793
  <mat-form-field appearance="fill">
1815
- <mat-label>Modo de detalhes</mat-label>
1794
+ <mat-label>{{ tx('editor.interface.list.detailsMode', 'Details mode') }}</mat-label>
1816
1795
  <mat-select formControlName="detailsMode">
1817
- <mat-option value="auto">Automático</mat-option>
1818
- <mat-option value="card">Card (overlay)</mat-option>
1819
- <mat-option value="sidesheet">Side-sheet</mat-option>
1796
+ <mat-option value="auto">{{ tx('editor.interface.list.detailsMode.auto', 'Automatic') }}</mat-option>
1797
+ <mat-option value="card">{{ tx('editor.interface.list.detailsMode.card', 'Card (overlay)') }}</mat-option>
1798
+ <mat-option value="sidesheet">{{ tx('editor.interface.list.detailsMode.sidesheet', 'Side-sheet') }}</mat-option>
1820
1799
  </mat-select>
1821
1800
  </mat-form-field>
1822
1801
  <mat-form-field appearance="fill">
1823
- <mat-label>Largura máxima do card (px) <span class="opt-tag">opcional</span></mat-label>
1802
+ <mat-label>{{ tx('editor.interface.list.detailsWidth', 'Maximum card width (px)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1824
1803
  <input matInput type="number" formControlName="detailsMaxWidth" />
1825
1804
  </mat-form-field>
1826
1805
  <mat-checkbox formControlName="detailsShowTechnical">
1827
- Mostrar detalhes técnicos por padrão
1806
+ {{ tx('editor.interface.list.showTechnical', 'Show technical details by default') }}
1828
1807
  </mat-checkbox>
1829
1808
  <mat-form-field appearance="fill">
1830
- <mat-label>Campos de metadados (whitelist) <span class="opt-tag">opcional</span></mat-label>
1831
- <input matInput formControlName="detailsFields" placeholder="ex.: id,fileName,contentType" />
1809
+ <mat-label>{{ tx('editor.interface.list.detailsFields', 'Metadata fields (whitelist)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1810
+ <input matInput formControlName="detailsFields" [placeholder]="tx('editor.placeholders.detailsFields', 'e.g.: id,fileName,contentType')" />
1832
1811
  <button
1833
1812
  mat-icon-button
1834
1813
  matSuffix
1835
1814
  class="help-icon-button"
1836
1815
  type="button"
1837
- [matTooltip]="'Lista separada por vírgula; vazio = todos.'"
1816
+ [matTooltip]="tx('editor.interface.list.detailsFieldsHint', 'Comma-separated list; empty = all.')"
1838
1817
  matTooltipPosition="above"
1839
1818
  >
1840
1819
  <mat-icon>help_outline</mat-icon>
1841
1820
  </button>
1842
1821
  </mat-form-field>
1843
1822
  <mat-form-field appearance="fill">
1844
- <mat-label>Âncora do overlay</mat-label>
1823
+ <mat-label>{{ tx('editor.interface.list.anchor', 'Overlay anchor') }}</mat-label>
1845
1824
  <mat-select formControlName="detailsAnchor">
1846
- <mat-option value="item">Item</mat-option>
1847
- <mat-option value="field">Campo</mat-option>
1825
+ <mat-option value="item">{{ tx('editor.interface.list.anchor.item', 'Item') }}</mat-option>
1826
+ <mat-option value="field">{{ tx('editor.interface.list.anchor.field', 'Field') }}</mat-option>
1848
1827
  </mat-select>
1849
1828
  </mat-form-field>
1850
1829
  </fieldset>
@@ -1852,45 +1831,43 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1852
1831
  </section>
1853
1832
  </div>
1854
1833
  </mat-tab>
1855
- <mat-tab label="Validações">
1834
+ <mat-tab [label]="tx('editor.tabs.validation', 'Validation')">
1856
1835
  <div class="tab-panel">
1857
1836
  <section class="tab-intro">
1858
- <h3>Regras de aceite e comportamento operacional</h3>
1837
+ <h3>{{ tx('editor.validation.intro.title', 'Acceptance rules and operational behavior') }}</h3>
1859
1838
  <p>
1860
- Separe mentalmente o que é validação de UX local do que é regra
1861
- efetiva de backend. Revise com atenção as opções destrutivas.
1839
+ {{ tx('editor.validation.intro.body', 'Separate local UX validation from effective backend rules. Review destructive options carefully.') }}
1862
1840
  </p>
1863
1841
  </section>
1864
1842
  <section class="config-section">
1865
1843
  <form [formGroup]="limitsGroup">
1866
1844
  <h4 class="section-subtitle">
1867
1845
  <mat-icon aria-hidden="true">build</mat-icon>
1868
- Opções configuráveisValidações
1846
+ {{ tx('editor.validation.local.title', 'Configurable options Validation') }}
1869
1847
  </h4>
1870
1848
  <p class="section-note">
1871
- Limites locais melhoram feedback imediato, mas não substituem a
1872
- política efetiva do servidor.
1849
+ {{ tx('editor.validation.local.note', 'Local limits improve immediate feedback, but do not replace the effective server policy.') }}
1873
1850
  </p>
1874
1851
  <mat-form-field appearance="fill">
1875
- <mat-label>Tamanho máximo do arquivo (bytes) <span class="opt-tag">opcional</span></mat-label>
1852
+ <mat-label>{{ tx('editor.validation.local.maxFileSize', 'Maximum file size (bytes)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1876
1853
  <input matInput type="number" formControlName="maxFileSizeBytes" />
1877
1854
  <button
1878
1855
  mat-icon-button
1879
1856
  matSuffix
1880
1857
  class="help-icon-button"
1881
1858
  type="button"
1882
- [matTooltip]="'Limite de validação no cliente (opcional).'"
1859
+ [matTooltip]="tx('editor.validation.local.maxFileSizeHint', 'Client-side validation limit (optional).')"
1883
1860
  matTooltipPosition="above"
1884
1861
  >
1885
1862
  <mat-icon>help_outline</mat-icon>
1886
1863
  </button>
1887
1864
  </mat-form-field>
1888
1865
  <mat-form-field appearance="fill">
1889
- <mat-label>Máx. arquivos por lote <span class="opt-tag">opcional</span></mat-label>
1866
+ <mat-label>{{ tx('editor.validation.local.maxFilesPerBulk', 'Max files per batch') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1890
1867
  <input matInput type="number" formControlName="maxFilesPerBulk" />
1891
1868
  </mat-form-field>
1892
1869
  <mat-form-field appearance="fill">
1893
- <mat-label>Tamanho máximo do lote (bytes) <span class="opt-tag">opcional</span></mat-label>
1870
+ <mat-label>{{ tx('editor.validation.local.maxBulkSize', 'Maximum batch size (bytes)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1894
1871
  <input matInput type="number" formControlName="maxBulkSizeBytes" />
1895
1872
  </mat-form-field>
1896
1873
  </form>
@@ -1899,104 +1876,102 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
1899
1876
  <form [formGroup]="optionsGroup">
1900
1877
  <h4 class="section-subtitle">
1901
1878
  <mat-icon aria-hidden="true">tune</mat-icon>
1902
- Opções configuráveis — Backend
1879
+ {{ tx('editor.validation.backend.title', 'Configurable options — Backend') }}
1903
1880
  </h4>
1904
1881
  <p class="section-note">
1905
- Estas opções têm impacto direto no contrato com o servidor e em
1906
- governança operacional. Trate alterações aqui como decisão de
1907
- política, não apenas de interface.
1882
+ {{ tx('editor.validation.backend.note', 'These options directly impact the server contract and operational governance. Treat changes here as policy decisions, not only UI changes.') }}
1908
1883
  </p>
1909
1884
  <mat-form-field appearance="fill">
1910
- <mat-label>Política de conflito (padrão) <span class="opt-tag">opcional</span></mat-label>
1885
+ <mat-label>{{ tx('editor.validation.backend.conflictPolicy', 'Conflict policy (default)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1911
1886
  <mat-select formControlName="defaultConflictPolicy">
1912
- <mat-option value="RENAME">Renomear automaticamente</mat-option>
1913
- <mat-option value="MAKE_UNIQUE">Gerar nome único</mat-option>
1914
- <mat-option value="OVERWRITE">Sobrescrever arquivo existente</mat-option>
1915
- <mat-option value="SKIP">Pular se existir</mat-option>
1916
- <mat-option value="ERROR">Falhar (erro)</mat-option>
1887
+ <mat-option value="RENAME">{{ tx('editor.validation.backend.conflictPolicy.rename', 'Rename automatically') }}</mat-option>
1888
+ <mat-option value="MAKE_UNIQUE">{{ tx('editor.validation.backend.conflictPolicy.unique', 'Generate unique name') }}</mat-option>
1889
+ <mat-option value="OVERWRITE">{{ tx('editor.validation.backend.conflictPolicy.overwrite', 'Overwrite existing file') }}</mat-option>
1890
+ <mat-option value="SKIP">{{ tx('editor.validation.backend.conflictPolicy.skip', 'Skip if already exists') }}</mat-option>
1891
+ <mat-option value="ERROR">{{ tx('editor.validation.backend.conflictPolicy.error', 'Fail (error)') }}</mat-option>
1917
1892
  </mat-select>
1918
1893
  <button
1919
1894
  mat-icon-button
1920
1895
  matSuffix
1921
1896
  class="help-icon-button"
1922
1897
  type="button"
1923
- [matTooltip]="'O que fazer quando o nome do arquivo existe.'"
1898
+ [matTooltip]="tx('editor.validation.backend.conflictPolicyHint', 'What to do when the file name already exists.')"
1924
1899
  matTooltipPosition="above"
1925
1900
  >
1926
1901
  <mat-icon>help_outline</mat-icon>
1927
1902
  </button>
1928
1903
  <div class="warn" *ngIf="optionsGroup.get('defaultConflictPolicy')?.value === 'OVERWRITE'">
1929
1904
  <mat-icon color="warn" aria-hidden="true">warning</mat-icon>
1930
- Atenção: OVERWRITE pode sobrescrever arquivos existentes.
1905
+ {{ tx('editor.validation.backend.overwriteWarn', 'Warning: OVERWRITE may replace existing files.') }}
1931
1906
  </div>
1932
1907
  </mat-form-field>
1933
1908
  <mat-checkbox formControlName="strictValidation"
1934
- >Validação rigorosa (backend)</mat-checkbox
1909
+ >{{ tx('editor.validation.backend.strictValidation', 'Strict validation (backend)') }}</mat-checkbox
1935
1910
  >
1936
1911
  <mat-form-field appearance="fill">
1937
- <mat-label>Tamanho máx. por arquivo (MB) <span class="req-tag">mandatório</span></mat-label>
1912
+ <mat-label>{{ tx('editor.validation.backend.maxUploadSizeMb', 'Maximum size per file (MB)') }} <span class="req-tag">mandatório</span></mat-label>
1938
1913
  <input matInput type="number" formControlName="maxUploadSizeMb" required />
1939
1914
  <button
1940
1915
  mat-icon-button
1941
1916
  matSuffix
1942
1917
  class="help-icon-button"
1943
1918
  type="button"
1944
- [matTooltip]="'Validado pelo backend (1–500 MB).'"
1919
+ [matTooltip]="tx('editor.validation.backend.maxUploadSizeMbHint', 'Validated by the backend (1–500 MB).')"
1945
1920
  matTooltipPosition="above"
1946
1921
  >
1947
1922
  <mat-icon>help_outline</mat-icon>
1948
1923
  </button>
1949
1924
  </mat-form-field>
1950
1925
  <mat-form-field appearance="fill">
1951
- <mat-label>Extensões permitidas <span class="opt-tag">opcional</span></mat-label>
1926
+ <mat-label>{{ tx('editor.validation.backend.allowedExtensions', 'Allowed extensions') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1952
1927
  <input
1953
1928
  matInput
1954
1929
  formControlName="allowedExtensions"
1955
- placeholder="ex.: pdf,docx,xlsx"
1930
+ [placeholder]="tx('editor.placeholders.allowedExtensions', 'e.g.: pdf,docx,xlsx')"
1956
1931
  />
1957
1932
  <button
1958
1933
  mat-icon-button
1959
1934
  matSuffix
1960
1935
  class="help-icon-button"
1961
1936
  type="button"
1962
- [matTooltip]="'Lista separada por vírgula (opcional).'"
1937
+ [matTooltip]="tx('editor.shared.csvOptional', 'Comma-separated list (optional).')"
1963
1938
  matTooltipPosition="above"
1964
1939
  >
1965
1940
  <mat-icon>help_outline</mat-icon>
1966
1941
  </button>
1967
1942
  </mat-form-field>
1968
1943
  <mat-form-field appearance="fill">
1969
- <mat-label>MIME types aceitos <span class="opt-tag">opcional</span></mat-label>
1944
+ <mat-label>{{ tx('editor.validation.backend.acceptMimeTypes', 'Accepted MIME types') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1970
1945
  <input
1971
1946
  matInput
1972
1947
  formControlName="acceptMimeTypes"
1973
- placeholder="ex.: application/pdf,image/png"
1948
+ [placeholder]="tx('editor.placeholders.acceptMimeTypes', 'e.g.: application/pdf,image/png')"
1974
1949
  />
1975
1950
  <button
1976
1951
  mat-icon-button
1977
1952
  matSuffix
1978
1953
  class="help-icon-button"
1979
1954
  type="button"
1980
- [matTooltip]="'Lista separada por vírgula (opcional).'"
1955
+ [matTooltip]="tx('editor.shared.csvOptional', 'Comma-separated list (optional).')"
1981
1956
  matTooltipPosition="above"
1982
1957
  >
1983
1958
  <mat-icon>help_outline</mat-icon>
1984
1959
  </button>
1985
1960
  </mat-form-field>
1986
1961
  <mat-form-field appearance="fill">
1987
- <mat-label>Diretório destino <span class="opt-tag">opcional</span></mat-label>
1962
+ <mat-label>{{ tx('editor.validation.backend.targetDirectory', 'Target directory') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
1988
1963
  <input
1989
1964
  matInput
1990
1965
  formControlName="targetDirectory"
1991
- placeholder="ex.: documentos/notas"
1966
+ [placeholder]="tx('editor.placeholders.targetDirectory', 'e.g.: documents/invoices')"
1992
1967
  />
1993
1968
  </mat-form-field>
1994
1969
  <mat-checkbox formControlName="enableVirusScanning">
1995
- Forçar antivírus (quando disponível)
1970
+ {{ tx('editor.validation.backend.enableVirusScanning', 'Force antivirus (when available)') }}
1996
1971
  </mat-checkbox>
1997
1972
  <div class="warn" *ngIf="optionsGroup.get('enableVirusScanning')?.value === true">
1998
1973
  <mat-icon color="warn" aria-hidden="true">warning</mat-icon>
1999
- Pode impactar desempenho e latência de upload.
1974
+ {{ tx('editor.validation.backend.virusWarn', 'May impact upload performance and latency.') }}
2000
1975
  </div>
2001
1976
  </form>
2002
1977
  </section>
@@ -2004,13 +1979,13 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
2004
1979
  <form [formGroup]="quotasGroup">
2005
1980
  <h4 class="section-subtitle">
2006
1981
  <mat-icon aria-hidden="true">edit</mat-icon>
2007
- Opções configuráveis — Quotas (UI)
1982
+ {{ tx('editor.validation.quotas.title', 'Configurable options — Quotas (UI)') }}
2008
1983
  </h4>
2009
1984
  <mat-checkbox formControlName="showQuotaWarnings"
2010
- >Exibir avisos de cota</mat-checkbox
1985
+ >{{ tx('editor.validation.quotas.showWarnings', 'Show quota warnings') }}</mat-checkbox
2011
1986
  >
2012
1987
  <mat-checkbox formControlName="blockOnExceed"
2013
- >Bloquear ao exceder cota</mat-checkbox
1988
+ >{{ tx('editor.validation.quotas.blockOnExceed', 'Block on quota exceed') }}</mat-checkbox
2014
1989
  >
2015
1990
  </form>
2016
1991
  </section>
@@ -2018,24 +1993,23 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
2018
1993
  <form [formGroup]="rateLimitGroup">
2019
1994
  <h4 class="section-subtitle">
2020
1995
  <mat-icon aria-hidden="true">edit</mat-icon>
2021
- Opções configuráveis — Rate Limit (UI)
1996
+ {{ tx('editor.validation.rateLimit.title', 'Configurable options — Rate Limit (UI)') }}
2022
1997
  </h4>
2023
1998
  <p class="section-note">
2024
- Prefira poucos retries automáticos. Mais tentativas aliviam atrito
2025
- imediato, mas podem mascarar gargalos reais do ambiente.
1999
+ {{ tx('editor.validation.rateLimit.note', 'Prefer a small number of automatic retries. More attempts reduce immediate friction, but may hide real environment bottlenecks.') }}
2026
2000
  </p>
2027
2001
  <mat-checkbox formControlName="showBannerOn429"
2028
- >Exibir banner quando atingir o limite</mat-checkbox
2002
+ >{{ tx('editor.validation.rateLimit.showBanner', 'Show banner when the limit is reached') }}</mat-checkbox
2029
2003
  >
2030
2004
  <mat-checkbox formControlName="autoRetryOn429"
2031
- >Tentar novamente automaticamente</mat-checkbox
2005
+ >{{ tx('editor.validation.rateLimit.autoRetry', 'Retry automatically') }}</mat-checkbox
2032
2006
  >
2033
2007
  <mat-form-field appearance="fill">
2034
- <mat-label>Máximo de tentativas automáticas <span class="opt-tag">opcional</span></mat-label>
2008
+ <mat-label>{{ tx('editor.validation.rateLimit.maxAutoRetry', 'Maximum automatic retries') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2035
2009
  <input matInput type="number" formControlName="maxAutoRetry" />
2036
2010
  </mat-form-field>
2037
2011
  <mat-form-field appearance="fill">
2038
- <mat-label>Intervalo base entre tentativas (ms) <span class="opt-tag">opcional</span></mat-label>
2012
+ <mat-label>{{ tx('editor.validation.rateLimit.baseBackoff', 'Base retry interval (ms)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2039
2013
  <input matInput type="number" formControlName="baseBackoffMs" />
2040
2014
  </mat-form-field>
2041
2015
  </form>
@@ -2044,55 +2018,52 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
2044
2018
  <form [formGroup]="bulkGroup">
2045
2019
  <h4 class="section-subtitle">
2046
2020
  <mat-icon aria-hidden="true">bolt</mat-icon>
2047
- Opções configuráveisExecução do lote
2021
+ {{ tx('editor.validation.batch.title', 'Configurable options Batch execution') }}
2048
2022
  </h4>
2049
2023
  <mat-checkbox formControlName="failFast"
2050
- >Parar no primeiro erro (fail-fast)</mat-checkbox
2024
+ >{{ tx('editor.validation.batch.failFast', 'Stop on the first error (fail-fast)') }}</mat-checkbox
2051
2025
  >
2052
2026
  </form>
2053
2027
  </section>
2054
2028
  </div>
2055
2029
  </mat-tab>
2056
- <mat-tab label="Mensagens">
2030
+ <mat-tab [label]="tx('editor.tabs.messages', 'Messages')">
2057
2031
  <div class="tab-panel">
2058
2032
  <section class="tab-intro">
2059
- <h3>Mensagens orientadas à operação</h3>
2033
+ <h3>{{ tx('editor.messages.intro.title', 'Messages oriented to operations') }}</h3>
2060
2034
  <p>
2061
- Ajuste o texto exibido ao usuário final. Priorize clareza,
2062
- consistência e linguagem acionável.
2035
+ {{ tx('editor.messages.intro.body', 'Adjust the text shown to the end user. Prioritize clarity, consistency and actionable language.') }}
2063
2036
  </p>
2064
2037
  </section>
2065
2038
  <section class="config-section">
2066
2039
  <form [formGroup]="messagesGroup">
2067
2040
  <h4 class="section-subtitle">
2068
2041
  <mat-icon aria-hidden="true">edit</mat-icon>
2069
- Opções configuráveisMensagens (UI)
2042
+ {{ tx('editor.messages.section.title', 'Configurable options Messages (UI)') }}
2070
2043
  </h4>
2071
2044
  <p class="section-note">
2072
- Mensagens curtas e objetivas funcionam melhor em contextos de alta
2073
- frequência. Use este grupo para adequar a voz da interface ao seu
2074
- ambiente.
2045
+ {{ tx('editor.messages.section.note', 'Short and objective messages work better in high-frequency contexts. Use this group to adapt the interface voice to your environment.') }}
2075
2046
  </p>
2076
2047
  <mat-form-field appearance="fill">
2077
- <mat-label>Sucesso (individual) <span class="opt-tag">opcional</span></mat-label>
2048
+ <mat-label>{{ tx('editor.messages.successSingle', 'Success (single)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2078
2049
  <input
2079
2050
  matInput
2080
2051
  formControlName="successSingle"
2081
- placeholder="ex.: Arquivo enviado com sucesso"
2052
+ [placeholder]="tx('editor.placeholders.successSingle', 'e.g.: File uploaded successfully')"
2082
2053
  />
2083
2054
  </mat-form-field>
2084
2055
  <mat-form-field appearance="fill">
2085
- <mat-label>Sucesso (em lote) <span class="opt-tag">opcional</span></mat-label>
2056
+ <mat-label>{{ tx('editor.messages.successBulk', 'Success (batch)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2086
2057
  <input
2087
2058
  matInput
2088
2059
  formControlName="successBulk"
2089
- placeholder="ex.: Upload concluído"
2060
+ [placeholder]="tx('editor.placeholders.successBulk', 'e.g.: Upload completed')"
2090
2061
  />
2091
2062
  </mat-form-field>
2092
2063
  <div [formGroup]="errorsGroup">
2093
2064
  <ng-container *ngFor="let e of errorEntries">
2094
2065
  <mat-form-field appearance="fill">
2095
- <mat-label>{{ e.label }} <span class="opt-tag">opcional</span></mat-label>
2066
+ <mat-label>{{ e.label }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2096
2067
  <input matInput [formControlName]="e.code" />
2097
2068
  <mat-hint class="code-hint">{{ e.code }}</mat-hint>
2098
2069
  </mat-form-field>
@@ -2102,23 +2073,22 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
2102
2073
  </section>
2103
2074
  </div>
2104
2075
  </mat-tab>
2105
- <mat-tab label="Cabeçalhos">
2076
+ <mat-tab [label]="tx('editor.tabs.headers', 'Headers')">
2106
2077
  <div class="tab-panel">
2107
2078
  <section class="tab-intro">
2108
- <h3>Contexto de tenant e usuário</h3>
2079
+ <h3>{{ tx('editor.headers.intro.title', 'Tenant and user context') }}</h3>
2109
2080
  <p>
2110
- Use cabeçalhos para consultar configuração efetiva do servidor no
2111
- mesmo contexto que o componente usará em produção.
2081
+ {{ tx('editor.headers.intro.body', 'Use headers to query the effective server configuration in the same context the component will use in production.') }}
2112
2082
  </p>
2113
2083
  </section>
2114
2084
  <section class="config-section">
2115
2085
  <form [formGroup]="headersGroup">
2116
2086
  <h4 class="section-subtitle">
2117
2087
  <mat-icon aria-hidden="true">edit</mat-icon>
2118
- Opções configuráveisCabeçalhos (consulta)
2088
+ {{ tx('editor.headers.section.title', 'Configurable options Headers (query)') }}
2119
2089
  </h4>
2120
2090
  <mat-form-field appearance="fill">
2121
- <mat-label>Cabeçalho de tenant <span class="opt-tag">opcional</span></mat-label>
2091
+ <mat-label>{{ tx('editor.headers.tenantHeader', 'Tenant header') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2122
2092
  <input
2123
2093
  matInput
2124
2094
  formControlName="tenantHeader"
@@ -2126,15 +2096,15 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
2126
2096
  />
2127
2097
  </mat-form-field>
2128
2098
  <mat-form-field appearance="fill">
2129
- <mat-label>Valor do tenant <span class="opt-tag">opcional</span></mat-label>
2099
+ <mat-label>{{ tx('editor.headers.tenantValue', 'Tenant value') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2130
2100
  <input
2131
2101
  matInput
2132
2102
  formControlName="tenantValue"
2133
- placeholder="ex.: demo-tenant"
2103
+ [placeholder]="tx('editor.placeholders.tenantValue', 'e.g.: demo-tenant')"
2134
2104
  />
2135
2105
  </mat-form-field>
2136
2106
  <mat-form-field appearance="fill">
2137
- <mat-label>Cabeçalho de usuário <span class="opt-tag">opcional</span></mat-label>
2107
+ <mat-label>{{ tx('editor.headers.userHeader', 'User header') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2138
2108
  <input
2139
2109
  matInput
2140
2110
  formControlName="userHeader"
@@ -2142,20 +2112,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
2142
2112
  />
2143
2113
  </mat-form-field>
2144
2114
  <mat-form-field appearance="fill">
2145
- <mat-label>Valor do usuário <span class="opt-tag">opcional</span></mat-label>
2146
- <input matInput formControlName="userValue" placeholder="ex.: 42" />
2115
+ <mat-label>{{ tx('editor.headers.userValue', 'User value') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2116
+ <input matInput formControlName="userValue" [placeholder]="tx('editor.placeholders.userValue', 'e.g.: 42')" />
2147
2117
  </mat-form-field>
2148
2118
  </form>
2149
2119
  </section>
2150
2120
  </div>
2151
2121
  </mat-tab>
2152
- <mat-tab label="Servidor">
2122
+ <mat-tab [label]="tx('editor.tabs.server', 'Server')">
2153
2123
  <div class="tab-panel">
2154
2124
  <section class="tab-intro">
2155
- <h3>Contrato efetivo retornado pelo backend</h3>
2125
+ <h3>{{ tx('editor.server.intro.title', 'Effective contract returned by the backend') }}</h3>
2156
2126
  <p>
2157
- Esta aba é a fonte de verdade do ambiente ativo. Compare o resumo
2158
- do servidor com o que foi configurado nos formulários anteriores.
2127
+ {{ tx('editor.server.intro.body', 'This tab is the source of truth for the active environment. Compare the server summary with what was configured in the previous forms.') }}
2159
2128
  </p>
2160
2129
  </section>
2161
2130
  <section class="config-section">
@@ -2163,427 +2132,823 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
2163
2132
  <div class="toolbar">
2164
2133
  <h4 class="section-subtitle ro">
2165
2134
  <mat-icon aria-hidden="true">info</mat-icon>
2166
- Servidor (somente leitura)
2135
+ {{ tx('editor.server.readonly', 'Server (read-only)') }}
2167
2136
  <span class="badge">read-only</span>
2168
2137
  </h4>
2169
2138
  <button type="button" (click)="refetchServerConfig()">
2170
- Recarregar do servidor
2139
+ {{ tx('editor.server.reload', 'Reload from server') }}
2171
2140
  </button>
2172
2141
  <span class="hint" *ngIf="!baseUrl"
2173
- >Defina a baseUrl no componente pai para consultar
2174
- /api/files/config.</span
2142
+ >{{ tx('editor.server.baseUrlHint', 'Set baseUrl on the parent component to query /api/files/config.') }}</span
2175
2143
  >
2176
2144
  </div>
2177
2145
  <div *ngIf="serverLoading(); else serverLoaded">
2178
- Carregando configuração do servidor…
2146
+ {{ tx('editor.server.loading', 'Loading server configuration...') }}
2179
2147
  </div>
2180
2148
  <ng-template #serverLoaded>
2181
2149
  <div *ngIf="serverError(); else serverOk" class="error">
2182
- Falha ao carregar: {{ serverError() | json }}
2150
+ {{ tx('editor.server.loadError', 'Failed to load:') }} {{ serverError() | json }}
2183
2151
  </div>
2184
2152
  <ng-template #serverOk>
2185
2153
  <section *ngIf="serverData() as _">
2186
- <h3>Resumo da configuração ativa</h3>
2154
+ <h3>{{ tx('editor.server.summaryTitle', 'Active configuration summary') }}</h3>
2187
2155
  <ul class="summary">
2188
2156
  <li>
2189
- <strong>Max por arquivo (MB):</strong>
2157
+ <strong>{{ tx('editor.server.summary.maxPerFile', 'Max per file (MB):') }}</strong>
2190
2158
  {{ serverData()?.options?.maxUploadSizeMb }}
2191
2159
  </li>
2192
2160
  <li>
2193
- <strong>Validação rigorosa:</strong>
2161
+ <strong>{{ tx('editor.server.summary.strictValidation', 'Strict validation:') }}</strong>
2194
2162
  {{ serverData()?.options?.strictValidation }}
2195
2163
  </li>
2196
2164
  <li>
2197
- <strong>Antivírus:</strong>
2165
+ <strong>{{ tx('editor.server.summary.antivirus', 'Antivirus:') }}</strong>
2198
2166
  {{ serverData()?.options?.enableVirusScanning }}
2199
2167
  </li>
2200
2168
  <li>
2201
- <strong>Conflito de nome (padrão):</strong>
2169
+ <strong>{{ tx('editor.server.summary.conflictPolicy', 'Default name conflict:') }}</strong>
2202
2170
  {{ serverData()?.options?.nameConflictPolicy }}
2203
2171
  </li>
2204
2172
  <li>
2205
- <strong>MIME aceitos:</strong>
2173
+ <strong>{{ tx('editor.server.summary.mimeTypes', 'Accepted MIME types:') }}</strong>
2206
2174
  {{
2207
2175
  (serverData()?.options?.acceptMimeTypes || []).join(', ')
2208
2176
  }}
2209
2177
  </li>
2210
2178
  <li>
2211
- <strong>Bulk - fail-fast padrão:</strong>
2179
+ <strong>{{ tx('editor.server.summary.bulkFailFast', 'Default bulk fail-fast:') }}</strong>
2212
2180
  {{ serverData()?.bulk?.failFastModeDefault }}
2213
2181
  </li>
2214
2182
  <li>
2215
- <strong>Rate limit:</strong>
2183
+ <strong>{{ tx('editor.server.summary.rateLimit', 'Rate limit:') }}</strong>
2216
2184
  {{ serverData()?.rateLimit?.enabled }} ({{
2217
2185
  serverData()?.rateLimit?.perMinute
2218
2186
  }}/min, {{ serverData()?.rateLimit?.perHour }}/h)
2219
2187
  </li>
2220
2188
  <li>
2221
- <strong>Quotas:</strong> {{ serverData()?.quotas?.enabled }}
2189
+ <strong>{{ tx('editor.server.summary.quotas', 'Quotas:') }}</strong> {{ serverData()?.quotas?.enabled }}
2222
2190
  </li>
2223
2191
  <li>
2224
- <strong>Servidor:</strong> v{{
2192
+ <strong>{{ tx('editor.server.summary.server', 'Server:') }}</strong> v{{
2225
2193
  serverData()?.metadata?.version
2226
2194
  }}
2227
2195
  • {{ serverData()?.metadata?.locale }}
2228
2196
  </li>
2229
2197
  </ul>
2230
2198
  <details>
2231
- <summary>Ver JSON</summary>
2199
+ <summary>{{ tx('editor.server.viewJson', 'View JSON') }}</summary>
2232
2200
  <button
2233
2201
  mat-icon-button
2234
- aria-label="Copiar JSON"
2202
+ [attr.aria-label]="tx('editor.server.copyJson', 'Copy JSON')"
2235
2203
  (click)="copyServerConfig()"
2236
2204
  type="button"
2237
- title="Copiar JSON"
2205
+ [title]="tx('editor.server.copyJson', 'Copy JSON')"
2238
2206
  >
2239
2207
  <mat-icon>content_copy</mat-icon>
2240
2208
  </button>
2241
2209
  <pre>{{ serverData() | json }}</pre>
2242
- </details>
2243
- <p class="note">
2244
- As opções acima que podem ser alteradas via payload são:
2245
- conflito de nome, validação rigorosa, tamanho máximo (MB),
2246
- extensões/MIME aceitos, diretório destino, antivírus,
2247
- metadados personalizados e fail-fast (no bulk).
2248
- </p>
2249
- </section>
2250
- </ng-template>
2251
- </ng-template>
2252
- </div>
2253
- </section>
2254
- </div>
2255
- </mat-tab>
2256
- <mat-tab label="JSON">
2257
- <div class="tab-panel">
2258
- <section class="tab-intro">
2259
- <h3>Modo avançado</h3>
2260
- <p>
2261
- Edite o payload bruto apenas quando precisar de controle fino.
2262
- Alterações aqui exigem mais rigor de revisão e validação.
2263
- </p>
2264
- </section>
2265
- <div class="advanced-note">
2266
- O JSON é uma visão de baixo nível da configuração. Prefira as abas
2267
- guiadas sempre que possível para reduzir erro humano e manter a
2268
- configuração auditável.
2269
- </div>
2270
- <textarea
2271
- class="json-textarea"
2272
- rows="10"
2273
- [ngModel]="form.value | json"
2274
- (ngModelChange)="onJsonChange($event)"
2275
- ></textarea>
2276
- <div class="error" *ngIf="jsonError">{{ jsonError }}</div>
2277
- </div>
2278
- </mat-tab>
2279
- </mat-tab-group>
2280
- </div>
2281
- </div>
2282
- `, styles: [".editor-layout{display:grid;grid-template-columns:minmax(260px,320px) minmax(0,1fr);gap:16px;align-items:start}.summary-panel{position:sticky;top:0;display:flex;flex-direction:column;gap:12px;padding:16px;border:1px solid var(--pfx-surface-border, #d8d8d8);border-radius:16px;background:linear-gradient(180deg,#fafbfc,#fff)}.summary-panel h3{margin:0;font-size:1rem;font-weight:600}.summary-panel .summary-intro{margin:0;color:#000000a6;line-height:1.4;font-size:.88rem}.summary-grid{display:grid;gap:10px}.summary-card{padding:12px;border-radius:12px;background:#fff;border:1px solid #e7e9ee}.summary-card .eyebrow{display:block;margin-bottom:4px;font-size:.72rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:#5f6b7a}.summary-card strong{display:block;margin-bottom:4px;font-size:.95rem}.summary-card p{margin:0;font-size:.85rem;line-height:1.45;color:#000000b8}.risk-panel{padding:12px;border-radius:12px;background:#fff8e8;border:1px solid #f0d29a}.risk-panel.safe{background:#f4fbf6;border-color:#b8ddc1}.risk-panel h4{display:flex;align-items:center;gap:6px;margin:0 0 8px;font-size:.92rem}.risk-list{display:grid;gap:8px}.risk-item{padding:10px;border-radius:10px;background:#ffffffb8}.risk-item strong{display:block;margin-bottom:2px;font-size:.84rem}.risk-item p,.risk-panel .safe-copy{margin:0;font-size:.82rem;line-height:1.4;color:#000000b8}.editor-main{min-width:0}.tab-panel{display:grid;gap:16px;padding-top:8px}.tab-intro{padding:16px 18px;border:1px solid #e5e8ef;border-radius:16px;background:linear-gradient(180deg,#fff,#f7f9fc)}.tab-intro h3{margin:0 0 6px;font-size:1rem;font-weight:600}.tab-intro p{margin:0;font-size:.9rem;line-height:1.5;color:#000000b8}.config-section{padding:18px;border:1px solid #e5e8ef;border-radius:16px;background:#fff}.config-section+.config-section{margin-top:0}.section-note{margin:0 0 16px;color:#000000b3;font-size:.88rem;line-height:1.45}.advanced-note{padding:14px 16px;border-radius:14px;border:1px solid #f0d29a;background:#fff8e8;color:#000000c7;font-size:.88rem;line-height:1.45}.json-textarea{width:100%;min-height:280px;padding:14px 16px;border-radius:14px;border:1px solid #d7dce5;background:#0f172a;color:#e5eefc;font:500 .84rem/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;resize:vertical}.server-tab{display:grid;gap:16px}.toolbar{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.summary{display:grid;gap:8px;padding-left:18px}.summary li{line-height:1.45}details{margin-top:12px;padding:14px 16px;border:1px solid #e5e8ef;border-radius:14px;background:#fbfcfe}details pre{white-space:pre-wrap;word-break:break-word}@media(max-width:1100px){.editor-layout{grid-template-columns:1fr}.summary-panel{position:static}}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"] }]
2283
- }], ctorParameters: () => [{ type: i1$1.FormBuilder }, { type: undefined, decorators: [{
2284
- type: Inject,
2285
- args: [SETTINGS_PANEL_DATA]
2286
- }] }, { type: i2.MatSnackBar }, { type: i0.DestroyRef }] });
2287
-
2288
- const FILES_UPLOAD_EN_US = {
2289
- 'praxis.filesUpload.settingsAriaLabel': 'Open settings',
2290
- 'praxis.filesUpload.dropzoneLabel': 'Drag files or',
2291
- 'praxis.filesUpload.dropzoneButton': 'select',
2292
- 'praxis.filesUpload.conflictPolicyLabel': 'Conflict policy',
2293
- 'praxis.filesUpload.metadataLabel': 'Metadata (JSON)',
2294
- 'praxis.filesUpload.progressAriaLabel': 'Upload progress',
2295
- 'praxis.filesUpload.rateLimitBanner': 'Rate limit exceeded. Try again at',
2296
- 'praxis.filesUpload.settingsTitle': 'Files Upload Configuration',
2297
- 'praxis.filesUpload.statusSuccess': 'Uploaded',
2298
- 'praxis.filesUpload.statusError': 'Error',
2299
- 'praxis.filesUpload.invalidMetadata': 'Invalid metadata.',
2300
- 'praxis.filesUpload.genericUploadError': 'File upload error.',
2301
- 'praxis.filesUpload.acceptError': 'File type not allowed.',
2302
- 'praxis.filesUpload.maxFileSizeError': 'File exceeds the maximum size.',
2303
- 'praxis.filesUpload.maxFilesPerBulkError': 'Too many files selected.',
2304
- 'praxis.filesUpload.maxBulkSizeError': 'Total files size exceeded.',
2305
- 'praxis.filesUpload.serviceUnavailable': 'Upload service unavailable.',
2306
- 'praxis.filesUpload.baseUrlMissing': 'Base URL not configured. Provide [baseUrl] to enable uploads.',
2307
- 'praxis.filesUpload.selectFiles': 'Select file(s)',
2308
- 'praxis.filesUpload.remove': 'Remove',
2309
- 'praxis.filesUpload.moreActions': 'More actions',
2310
- 'praxis.filesUpload.policySummaryAction': 'Information about upload policies',
2311
- 'praxis.filesUpload.retry': 'Retry',
2312
- 'praxis.filesUpload.download': 'Download',
2313
- 'praxis.filesUpload.copyLink': 'Copy link',
2314
- 'praxis.filesUpload.details': 'Details',
2315
- 'praxis.filesUpload.detailsMetadata': 'Metadata',
2316
- 'praxis.filesUpload.dropzoneHint': 'Drag and drop files here or click to select',
2317
- 'praxis.filesUpload.dropzoneProximityHint': 'Drop here to upload',
2318
- 'praxis.filesUpload.placeholder': 'Select or drop files…',
2319
- 'praxis.filesUpload.partialUploadError': 'Partial upload completed with failures in part of the batch.',
2320
- 'praxis.filesUpload.presignMetadataMissing': 'Presigned upload completed without file metadata.',
2321
- 'praxis.filesUpload.fieldChipAria': 'Additional files selected',
2322
- 'praxis.filesUpload.fieldPendingCountSuffix': 'file(s) selected',
2323
- 'praxis.filesUpload.removePendingFile': 'Remove selected file',
2324
- 'praxis.filesUpload.fieldPolicyTypes': 'Types',
2325
- 'praxis.filesUpload.fieldPolicyMaxPerFile': 'Max/file',
2326
- 'praxis.filesUpload.fieldPolicyMaxItems': 'Max/items',
2327
- 'praxis.filesUpload.fieldPolicyNotConfigured': 'Not configured',
2328
- 'praxis.filesUpload.sizeUnitBytes': 'Bytes',
2329
- 'praxis.filesUpload.sizeUnitKB': 'KB',
2330
- 'praxis.filesUpload.sizeUnitMB': 'MB',
2331
- 'praxis.filesUpload.sizeUnitGB': 'GB',
2332
- 'praxis.filesUpload.sizeUnitTB': 'TB',
2333
- 'praxis.filesUpload.errors.INVALID_FILE_TYPE': 'Invalid file type.',
2334
- 'praxis.filesUpload.errors.FILE_TOO_LARGE': 'File is too large.',
2335
- 'praxis.filesUpload.errors.NOT_FOUND': 'File not found.',
2336
- 'praxis.filesUpload.errors.UNAUTHORIZED': 'Unauthorized request.',
2337
- 'praxis.filesUpload.errors.RATE_LIMIT_EXCEEDED': 'Request limit exceeded.',
2338
- 'praxis.filesUpload.errors.INTERNAL_ERROR': 'Internal server error.',
2339
- 'praxis.filesUpload.errors.QUOTA_EXCEEDED': 'Quota exceeded.',
2340
- 'praxis.filesUpload.errors.SEC_VIRUS_DETECTED': 'Virus detected in file.',
2341
- 'praxis.filesUpload.errors.SEC_MALICIOUS_CONTENT': 'Malicious content detected.',
2342
- 'praxis.filesUpload.errors.SEC_DANGEROUS_TYPE': 'Dangerous file type.',
2343
- 'praxis.filesUpload.errors.FMT_MAGIC_MISMATCH': 'File content does not match the declared format.',
2344
- 'praxis.filesUpload.errors.FMT_CORRUPTED': 'Corrupted file.',
2345
- 'praxis.filesUpload.errors.FMT_UNSUPPORTED': 'Unsupported file format.',
2346
- 'praxis.filesUpload.errors.SYS_STORAGE_ERROR': 'Storage error.',
2347
- 'praxis.filesUpload.errors.SYS_SERVICE_DOWN': 'Service unavailable.',
2348
- 'praxis.filesUpload.errors.SYS_RATE_LIMIT': 'Request limit exceeded.',
2349
- 'praxis.filesUpload.errors.ARQUIVO_MUITO_GRANDE': 'File is too large.',
2350
- 'praxis.filesUpload.errors.TIPO_ARQUIVO_INVALIDO': 'Invalid file type.',
2351
- 'praxis.filesUpload.errors.TIPO_MIDIA_NAO_SUPORTADO': 'Unsupported media type.',
2352
- 'praxis.filesUpload.errors.CAMPO_OBRIGATORIO_AUSENTE': 'Required field is missing.',
2353
- 'praxis.filesUpload.errors.OPCOES_JSON_INVALIDAS': 'Invalid JSON options.',
2354
- 'praxis.filesUpload.errors.ARGUMENTO_INVALIDO': 'Invalid argument.',
2355
- 'praxis.filesUpload.errors.NAO_AUTORIZADO': 'Authentication required.',
2356
- 'praxis.filesUpload.errors.ERRO_INTERNO': 'Internal server error.',
2357
- 'praxis.filesUpload.errors.LIMITE_TAXA_EXCEDIDO': 'Rate limit exceeded.',
2358
- 'praxis.filesUpload.errors.COTA_EXCEDIDA': 'Quota exceeded.',
2359
- 'praxis.filesUpload.errors.ARQUIVO_JA_EXISTE': 'File already exists.',
2360
- 'praxis.filesUpload.errors.INVALID_JSON_OPTIONS': 'Invalid JSON options.',
2361
- 'praxis.filesUpload.errors.EMPTY_FILENAME': 'File name is required.',
2362
- 'praxis.filesUpload.errors.FILE_EXISTS': 'File already exists.',
2363
- 'praxis.filesUpload.errors.PATH_TRAVERSAL': 'Path traversal detected.',
2364
- 'praxis.filesUpload.errors.INSUFFICIENT_STORAGE': 'Insufficient storage.',
2365
- 'praxis.filesUpload.errors.UPLOAD_TIMEOUT': 'Upload timed out.',
2366
- 'praxis.filesUpload.errors.BULK_UPLOAD_TIMEOUT': 'Bulk upload timed out.',
2367
- 'praxis.filesUpload.errors.BULK_UPLOAD_CANCELLED': 'Bulk upload cancelled.',
2368
- 'praxis.filesUpload.errors.USER_CANCELLED': 'Upload cancelled by user.',
2369
- 'praxis.filesUpload.errors.UNKNOWN_ERROR': 'Unknown error.',
2370
- 'praxis.filesUpload.fieldPendingSummary': 'Files ready to upload',
2371
- 'praxis.filesUpload.fieldSelectedSummary': 'Selected file',
2372
- 'praxis.filesUpload.fieldSelectedPlural': 'files attached',
2373
- 'praxis.filesUpload.fieldEmptySummary': 'No file selected',
2374
- 'praxis.filesUpload.fieldErrorSummary': 'Fix the error to continue',
2375
- 'praxis.filesUpload.fieldActionUpload': 'Upload selected files',
2376
- 'praxis.filesUpload.fieldActionUploadShort': 'Upload',
2377
- 'praxis.filesUpload.fieldActionCancel': 'Cancel current selection',
2378
- 'praxis.filesUpload.fieldActionCancelShort': 'Cancel',
2379
- 'praxis.filesUpload.fieldActionSelect': 'Select file(s)',
2380
- 'praxis.filesUpload.fieldActionClear': 'Clear',
2381
- 'praxis.filesUpload.fieldInfoAction': 'Information',
2382
- 'praxis.filesUpload.fieldMoreActions': 'More actions',
2383
- 'praxis.filesUpload.fieldPendingAriaSuffix': 'files ready to upload',
2384
- 'praxis.filesUpload.selectedOverlayAriaLabel': 'Selected files',
2385
- 'praxis.filesUpload.selectedOverlayTitle': 'Selected',
2386
- 'praxis.filesUpload.bulkSummaryTotal': 'Total',
2387
- 'praxis.filesUpload.bulkSummarySuccess': 'Success',
2388
- 'praxis.filesUpload.bulkSummaryFailed': 'Failed',
2389
- 'praxis.filesUpload.resultMetaIdLabel': 'ID',
2390
- 'praxis.filesUpload.resultMetaTypeLabel': 'Type',
2391
- 'praxis.filesUpload.resultMetaSizeLabel': 'Size',
2392
- 'praxis.filesUpload.resultMetaUploadedAtLabel': 'Uploaded at',
2393
- 'praxis.filesUpload.policySummaryAny': 'any',
2394
- 'praxis.filesUpload.policySummaryNotConfigured': '—',
2395
- 'praxis.filesUpload.policySummaryTypesLabel': 'Types',
2396
- 'praxis.filesUpload.policySummaryMaxPerFileLabel': 'Max. per file',
2397
- 'praxis.filesUpload.policySummaryQuantityLabel': 'Qty',
2398
- 'praxis.filesUpload.showAll': 'Show all',
2399
- 'praxis.filesUpload.showLess': 'Show less',
2400
- };
2210
+ </details>
2211
+ <p class="note">
2212
+ {{ tx('editor.server.mutableOptionsText', 'The options above that can be changed via payload are: name conflict, strict validation, maximum size (MB), accepted extensions/MIME, target directory, antivirus, custom metadata and fail-fast (in bulk).') }}
2213
+ </p>
2214
+ </section>
2215
+ </ng-template>
2216
+ </ng-template>
2217
+ </div>
2218
+ </section>
2219
+ </div>
2220
+ </mat-tab>
2221
+ <mat-tab [label]="tx('editor.tabs.json', 'JSON')">
2222
+ <div class="tab-panel">
2223
+ <section class="tab-intro">
2224
+ <h3>{{ tx('editor.json.intro.title', 'Advanced mode') }}</h3>
2225
+ <p>
2226
+ {{ tx('editor.json.intro.body', 'Edit the raw payload only when you need fine-grained control. Changes here require stricter review and validation.') }}
2227
+ </p>
2228
+ </section>
2229
+ <div class="advanced-note">
2230
+ {{ tx('editor.json.note', 'JSON is a low-level view of the configuration. Prefer the guided tabs whenever possible to reduce human error and keep the configuration auditable.') }}
2231
+ </div>
2232
+ <textarea
2233
+ class="json-textarea"
2234
+ rows="10"
2235
+ [ngModel]="form.value | json"
2236
+ (ngModelChange)="onJsonChange($event)"
2237
+ ></textarea>
2238
+ <div class="error" *ngIf="jsonError">{{ jsonError }}</div>
2239
+ </div>
2240
+ </mat-tab>
2241
+ </mat-tab-group>
2242
+ </div>
2243
+ </div>
2244
+ `, isInline: true, styles: [".editor-layout{display:grid;grid-template-columns:minmax(260px,320px) minmax(0,1fr);gap:16px;align-items:start}.summary-panel{position:sticky;top:0;display:flex;flex-direction:column;gap:12px;padding:16px;border:1px solid var(--pfx-surface-border, #d8d8d8);border-radius:16px;background:linear-gradient(180deg,#fafbfc,#fff)}.summary-panel h3{margin:0;font-size:1rem;font-weight:600}.summary-panel .summary-intro{margin:0;color:#000000a6;line-height:1.4;font-size:.88rem}.summary-grid{display:grid;gap:10px}.summary-card{padding:12px;border-radius:12px;background:#fff;border:1px solid #e7e9ee}.summary-card .eyebrow{display:block;margin-bottom:4px;font-size:.72rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:#5f6b7a}.summary-card strong{display:block;margin-bottom:4px;font-size:.95rem}.summary-card p{margin:0;font-size:.85rem;line-height:1.45;color:#000000b8}.risk-panel{padding:12px;border-radius:12px;background:#fff8e8;border:1px solid #f0d29a}.risk-panel.safe{background:#f4fbf6;border-color:#b8ddc1}.risk-panel h4{display:flex;align-items:center;gap:6px;margin:0 0 8px;font-size:.92rem}.risk-list{display:grid;gap:8px}.risk-item{padding:10px;border-radius:10px;background:#ffffffb8}.risk-item strong{display:block;margin-bottom:2px;font-size:.84rem}.risk-item p,.risk-panel .safe-copy{margin:0;font-size:.82rem;line-height:1.4;color:#000000b8}.editor-main{min-width:0}.tab-panel{display:grid;gap:16px;padding-top:8px}.tab-intro{padding:16px 18px;border:1px solid #e5e8ef;border-radius:16px;background:linear-gradient(180deg,#fff,#f7f9fc)}.tab-intro h3{margin:0 0 6px;font-size:1rem;font-weight:600}.tab-intro p{margin:0;font-size:.9rem;line-height:1.5;color:#000000b8}.config-section{padding:18px;border:1px solid #e5e8ef;border-radius:16px;background:#fff}.config-section+.config-section{margin-top:0}.section-note{margin:0 0 16px;color:#000000b3;font-size:.88rem;line-height:1.45}.advanced-note{padding:14px 16px;border-radius:14px;border:1px solid #f0d29a;background:#fff8e8;color:#000000c7;font-size:.88rem;line-height:1.45}.json-textarea{width:100%;min-height:280px;padding:14px 16px;border-radius:14px;border:1px solid #d7dce5;background:#0f172a;color:#e5eefc;font:500 .84rem/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;resize:vertical}.server-tab{display:grid;gap:16px}.toolbar{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.summary{display:grid;gap:8px;padding-left:18px}.summary li{line-height:1.45}details{margin-top:12px;padding:14px 16px;border:1px solid #e5e8ef;border-radius:14px;background:#fbfcfe}details pre{white-space:pre-wrap;word-break:break-word}@media(max-width:1100px){.editor-layout{grid-template-columns:1fr}.summary-panel{position:static}}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i9.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i9.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i1$1.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i1$1.NumberValueAccessor, selector: "input[type=number][formControlName],input[type=number][formControl],input[type=number][ngModel]" }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i1$1.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i1$1.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i1$1.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "ngmodule", type: MatTabsModule }, { kind: "component", type: i4.MatTab, selector: "mat-tab", inputs: ["disabled", "label", "aria-label", "aria-labelledby", "labelClass", "bodyClass", "id"], exportAs: ["matTab"] }, { kind: "component", type: i4.MatTabGroup, selector: "mat-tab-group", inputs: ["color", "fitInkBarToContent", "mat-stretch-tabs", "mat-align-tabs", "dynamicHeight", "selectedIndex", "headerPosition", "animationDuration", "contentTabIndex", "disablePagination", "disableRipple", "preserveContent", "backgroundColor", "aria-label", "aria-labelledby"], outputs: ["selectedIndexChange", "focusChange", "animationDone", "selectedTabChange"], exportAs: ["matTabGroup"] }, { kind: "ngmodule", type: MatFormFieldModule }, { kind: "component", type: i5.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "directive", type: i5.MatLabel, selector: "mat-label" }, { kind: "directive", type: i5.MatHint, selector: "mat-hint", inputs: ["align", "id"] }, { kind: "directive", type: i5.MatSuffix, selector: "[matSuffix], [matIconSuffix], [matTextSuffix]", inputs: ["matTextSuffix"] }, { kind: "ngmodule", type: MatInputModule }, { kind: "directive", type: i6.MatInput, selector: "input[matInput], textarea[matInput], select[matNativeControl], input[matNativeControl], textarea[matNativeControl]", inputs: ["disabled", "id", "placeholder", "name", "required", "type", "errorStateMatcher", "aria-describedby", "value", "readonly", "disabledInteractive"], exportAs: ["matInput"] }, { kind: "ngmodule", type: MatCheckboxModule }, { kind: "component", type: i7.MatCheckbox, selector: "mat-checkbox", inputs: ["aria-label", "aria-labelledby", "aria-describedby", "aria-expanded", "aria-controls", "aria-owns", "id", "required", "labelPosition", "name", "value", "disableRipple", "tabIndex", "color", "disabledInteractive", "checked", "disabled", "indeterminate"], outputs: ["change", "indeterminateChange"], exportAs: ["matCheckbox"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["aria-describedby", "panelClass", "disabled", "disableRipple", "tabIndex", "hideSingleSelectionIndicator", "placeholder", "required", "multiple", "disableOptionCentering", "compareWith", "value", "aria-label", "aria-labelledby", "errorStateMatcher", "typeaheadDebounceInterval", "sortComparator", "id", "panelWidth", "canSelectNullableOptions"], outputs: ["openedChange", "opened", "closed", "selectionChange", "valueChange"], exportAs: ["matSelect"] }, { kind: "component", type: i8.MatOption, selector: "mat-option", inputs: ["value", "id", "disabled"], outputs: ["onSelectionChange"], exportAs: ["matOption"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i11.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i10.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i11$1.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "ngmodule", type: MatSnackBarModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i1$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "pipe", type: i9.JsonPipe, name: "json" }] });
2245
+ }
2246
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: PraxisFilesUploadConfigEditor, decorators: [{
2247
+ type: Component,
2248
+ args: [{ selector: 'praxis-files-upload-config-editor', standalone: true, providers: [providePraxisFilesUploadI18n()], imports: [
2249
+ CommonModule,
2250
+ ReactiveFormsModule,
2251
+ MatTabsModule,
2252
+ MatFormFieldModule,
2253
+ MatInputModule,
2254
+ MatCheckboxModule,
2255
+ MatSelectModule,
2256
+ MatButtonModule,
2257
+ MatIconModule,
2258
+ MatTooltipModule,
2259
+ MatSnackBarModule,
2260
+ FormsModule,
2261
+ ], template: `
2262
+ <div class="editor-layout">
2263
+ <aside class="summary-panel" [attr.aria-label]="tx('editor.summary.ariaLabel', 'Executive summary for the current configuration')">
2264
+ <h3>{{ tx('editor.summary.title', 'Executive summary') }}</h3>
2265
+ <p class="summary-intro">
2266
+ {{ tx('editor.summary.intro', 'Review the operational impact here before going through all configuration groups.') }}
2267
+ </p>
2401
2268
 
2402
- const FILES_UPLOAD_PT_BR = {
2403
- 'praxis.filesUpload.settingsAriaLabel': 'Abrir configurações',
2404
- 'praxis.filesUpload.dropzoneLabel': 'Arraste arquivos ou',
2405
- 'praxis.filesUpload.dropzoneButton': 'selecionar',
2406
- 'praxis.filesUpload.conflictPolicyLabel': 'Política de conflito',
2407
- 'praxis.filesUpload.metadataLabel': 'Metadados (JSON)',
2408
- 'praxis.filesUpload.progressAriaLabel': 'Progresso do upload',
2409
- 'praxis.filesUpload.rateLimitBanner': 'Limite de requisições excedido. Tente novamente às',
2410
- 'praxis.filesUpload.settingsTitle': 'Configuração de Upload de Arquivos',
2411
- 'praxis.filesUpload.statusSuccess': 'Enviado',
2412
- 'praxis.filesUpload.statusError': 'Erro',
2413
- 'praxis.filesUpload.invalidMetadata': 'Metadados inválidos.',
2414
- 'praxis.filesUpload.genericUploadError': 'Erro no envio de arquivo.',
2415
- 'praxis.filesUpload.acceptError': 'Tipo de arquivo não permitido.',
2416
- 'praxis.filesUpload.maxFileSizeError': 'Arquivo excede o tamanho máximo.',
2417
- 'praxis.filesUpload.maxFilesPerBulkError': 'Quantidade de arquivos excedida.',
2418
- 'praxis.filesUpload.maxBulkSizeError': 'Tamanho total dos arquivos excedido.',
2419
- 'praxis.filesUpload.serviceUnavailable': 'Serviço de upload indisponível.',
2420
- 'praxis.filesUpload.baseUrlMissing': 'Base URL não configurada. Informe [baseUrl] para habilitar o envio.',
2421
- 'praxis.filesUpload.selectFiles': 'Selecionar arquivo(s)',
2422
- 'praxis.filesUpload.remove': 'Remover',
2423
- 'praxis.filesUpload.moreActions': 'Mais ações',
2424
- 'praxis.filesUpload.policySummaryAction': 'Informações sobre políticas de upload',
2425
- 'praxis.filesUpload.retry': 'Reenviar',
2426
- 'praxis.filesUpload.download': 'Baixar',
2427
- 'praxis.filesUpload.copyLink': 'Copiar link',
2428
- 'praxis.filesUpload.details': 'Detalhes',
2429
- 'praxis.filesUpload.detailsMetadata': 'Metadados',
2430
- 'praxis.filesUpload.dropzoneHint': 'Arraste e solte arquivos aqui ou clique para selecionar',
2431
- 'praxis.filesUpload.dropzoneProximityHint': 'Solte aqui para enviar',
2432
- 'praxis.filesUpload.placeholder': 'Selecione ou solte arquivos…',
2433
- 'praxis.filesUpload.partialUploadError': 'Upload parcial concluído com falhas em parte do lote.',
2434
- 'praxis.filesUpload.presignMetadataMissing': 'Upload assinado concluído sem metadados do arquivo.',
2435
- 'praxis.filesUpload.fieldChipAria': 'Arquivos adicionais selecionados',
2436
- 'praxis.filesUpload.fieldPendingCountSuffix': 'arquivo(s) selecionado(s)',
2437
- 'praxis.filesUpload.removePendingFile': 'Remover arquivo selecionado',
2438
- 'praxis.filesUpload.fieldPolicyTypes': 'Tipos',
2439
- 'praxis.filesUpload.fieldPolicyMaxPerFile': 'Máx/arquivo',
2440
- 'praxis.filesUpload.fieldPolicyMaxItems': 'Máx/itens',
2441
- 'praxis.filesUpload.fieldPolicyNotConfigured': 'Não configurado',
2442
- 'praxis.filesUpload.sizeUnitBytes': 'Bytes',
2443
- 'praxis.filesUpload.sizeUnitKB': 'KB',
2444
- 'praxis.filesUpload.sizeUnitMB': 'MB',
2445
- 'praxis.filesUpload.sizeUnitGB': 'GB',
2446
- 'praxis.filesUpload.sizeUnitTB': 'TB',
2447
- 'praxis.filesUpload.errors.INVALID_FILE_TYPE': 'Tipo de arquivo inválido.',
2448
- 'praxis.filesUpload.errors.FILE_TOO_LARGE': 'Arquivo muito grande.',
2449
- 'praxis.filesUpload.errors.NOT_FOUND': 'Arquivo não encontrado.',
2450
- 'praxis.filesUpload.errors.UNAUTHORIZED': 'Requisição não autorizada.',
2451
- 'praxis.filesUpload.errors.RATE_LIMIT_EXCEEDED': 'Limite de requisições excedido.',
2452
- 'praxis.filesUpload.errors.INTERNAL_ERROR': 'Erro interno do servidor.',
2453
- 'praxis.filesUpload.errors.QUOTA_EXCEEDED': 'Cota excedida.',
2454
- 'praxis.filesUpload.errors.SEC_VIRUS_DETECTED': 'Vírus detectado no arquivo.',
2455
- 'praxis.filesUpload.errors.SEC_MALICIOUS_CONTENT': 'Conteúdo malicioso detectado.',
2456
- 'praxis.filesUpload.errors.SEC_DANGEROUS_TYPE': 'Tipo de arquivo perigoso.',
2457
- 'praxis.filesUpload.errors.FMT_MAGIC_MISMATCH': 'Conteúdo do arquivo incompatível.',
2458
- 'praxis.filesUpload.errors.FMT_CORRUPTED': 'Arquivo corrompido.',
2459
- 'praxis.filesUpload.errors.FMT_UNSUPPORTED': 'Formato de arquivo não suportado.',
2460
- 'praxis.filesUpload.errors.SYS_STORAGE_ERROR': 'Erro de armazenamento.',
2461
- 'praxis.filesUpload.errors.SYS_SERVICE_DOWN': 'Serviço indisponível.',
2462
- 'praxis.filesUpload.errors.SYS_RATE_LIMIT': 'Limite de requisições excedido.',
2463
- // Aliases PT-BR do backend e códigos adicionais
2464
- 'praxis.filesUpload.errors.ARQUIVO_MUITO_GRANDE': 'Arquivo muito grande.',
2465
- 'praxis.filesUpload.errors.TIPO_ARQUIVO_INVALIDO': 'Tipo de arquivo inválido.',
2466
- 'praxis.filesUpload.errors.TIPO_MIDIA_NAO_SUPORTADO': 'Tipo de mídia não suportado.',
2467
- 'praxis.filesUpload.errors.CAMPO_OBRIGATORIO_AUSENTE': 'Campo obrigatório ausente.',
2468
- 'praxis.filesUpload.errors.OPCOES_JSON_INVALIDAS': 'JSON de opções inválido.',
2469
- 'praxis.filesUpload.errors.ARGUMENTO_INVALIDO': 'Argumento inválido.',
2470
- 'praxis.filesUpload.errors.NAO_AUTORIZADO': 'Autenticação necessária.',
2471
- 'praxis.filesUpload.errors.ERRO_INTERNO': 'Erro interno do servidor.',
2472
- 'praxis.filesUpload.errors.LIMITE_TAXA_EXCEDIDO': 'Limite de taxa excedido.',
2473
- 'praxis.filesUpload.errors.COTA_EXCEDIDA': 'Cota excedida.',
2474
- 'praxis.filesUpload.errors.ARQUIVO_JA_EXISTE': 'Arquivo já existe.',
2475
- // Outros possíveis códigos mapeados no catálogo
2476
- 'praxis.filesUpload.errors.INVALID_JSON_OPTIONS': 'JSON de opções inválido.',
2477
- 'praxis.filesUpload.errors.EMPTY_FILENAME': 'Nome do arquivo obrigatório.',
2478
- 'praxis.filesUpload.errors.FILE_EXISTS': 'Arquivo já existe.',
2479
- 'praxis.filesUpload.errors.PATH_TRAVERSAL': 'Path traversal detectado.',
2480
- 'praxis.filesUpload.errors.INSUFFICIENT_STORAGE': 'Sem espaço em disco.',
2481
- 'praxis.filesUpload.errors.UPLOAD_TIMEOUT': 'Tempo esgotado no upload.',
2482
- 'praxis.filesUpload.errors.BULK_UPLOAD_TIMEOUT': 'Tempo esgotado no upload em lote.',
2483
- 'praxis.filesUpload.errors.BULK_UPLOAD_CANCELLED': 'Upload em lote cancelado.',
2484
- 'praxis.filesUpload.errors.USER_CANCELLED': 'Upload cancelado pelo usuário.',
2485
- 'praxis.filesUpload.errors.UNKNOWN_ERROR': 'Erro desconhecido.',
2486
- 'praxis.filesUpload.fieldPendingSummary': 'Arquivos prontos para envio',
2487
- 'praxis.filesUpload.fieldSelectedSummary': 'Arquivo selecionado',
2488
- 'praxis.filesUpload.fieldSelectedPlural': 'arquivos vinculados',
2489
- 'praxis.filesUpload.fieldEmptySummary': 'Nenhum arquivo selecionado',
2490
- 'praxis.filesUpload.fieldErrorSummary': 'Corrija o erro para continuar',
2491
- 'praxis.filesUpload.fieldActionUpload': 'Enviar arquivos selecionados',
2492
- 'praxis.filesUpload.fieldActionUploadShort': 'Enviar',
2493
- 'praxis.filesUpload.fieldActionCancel': 'Cancelar seleção atual',
2494
- 'praxis.filesUpload.fieldActionCancelShort': 'Cancelar',
2495
- 'praxis.filesUpload.fieldActionSelect': 'Selecionar arquivo(s)',
2496
- 'praxis.filesUpload.fieldActionClear': 'Limpar',
2497
- 'praxis.filesUpload.fieldInfoAction': 'Informações',
2498
- 'praxis.filesUpload.fieldMoreActions': 'Mais ações',
2499
- 'praxis.filesUpload.fieldPendingAriaSuffix': 'arquivos prontos para envio',
2500
- 'praxis.filesUpload.selectedOverlayAriaLabel': 'Arquivos selecionados',
2501
- 'praxis.filesUpload.selectedOverlayTitle': 'Selecionados',
2502
- 'praxis.filesUpload.bulkSummaryTotal': 'Total',
2503
- 'praxis.filesUpload.bulkSummarySuccess': 'Sucesso',
2504
- 'praxis.filesUpload.bulkSummaryFailed': 'Falhas',
2505
- 'praxis.filesUpload.resultMetaIdLabel': 'ID',
2506
- 'praxis.filesUpload.resultMetaTypeLabel': 'Tipo',
2507
- 'praxis.filesUpload.resultMetaSizeLabel': 'Tamanho',
2508
- 'praxis.filesUpload.resultMetaUploadedAtLabel': 'Enviado em',
2509
- 'praxis.filesUpload.policySummaryAny': 'qualquer',
2510
- 'praxis.filesUpload.policySummaryNotConfigured': '—',
2511
- 'praxis.filesUpload.policySummaryTypesLabel': 'Tipos',
2512
- 'praxis.filesUpload.policySummaryMaxPerFileLabel': 'Máx. por arquivo',
2513
- 'praxis.filesUpload.policySummaryQuantityLabel': 'Qtde',
2514
- 'praxis.filesUpload.showAll': 'Ver todos',
2515
- 'praxis.filesUpload.showLess': 'Ver menos',
2516
- };
2269
+ <div class="summary-grid">
2270
+ <section class="summary-card">
2271
+ <span class="eyebrow">{{ tx('editor.summary.strategy', 'Strategy') }}</span>
2272
+ <strong>{{ summaryStrategyTitle }}</strong>
2273
+ <p>{{ summaryStrategyDetail }}</p>
2274
+ </section>
2275
+
2276
+ <section class="summary-card">
2277
+ <span class="eyebrow">{{ tx('editor.summary.experience', 'Experience') }}</span>
2278
+ <strong>{{ summaryExperienceTitle }}</strong>
2279
+ <p>{{ summaryExperienceDetail }}</p>
2280
+ </section>
2281
+
2282
+ <section class="summary-card">
2283
+ <span class="eyebrow">{{ tx('editor.summary.limits', 'Limits') }}</span>
2284
+ <strong>{{ summaryLimitsTitle }}</strong>
2285
+ <p>{{ summaryLimitsDetail }}</p>
2286
+ </section>
2287
+
2288
+ <section class="summary-card">
2289
+ <span class="eyebrow">{{ tx('editor.summary.server', 'Server') }}</span>
2290
+ <strong>{{ summaryServerTitle }}</strong>
2291
+ <p>{{ summaryServerDetail }}</p>
2292
+ </section>
2293
+ </div>
2294
+
2295
+ <section class="risk-panel" [class.safe]="activeRisks.length === 0">
2296
+ <h4>
2297
+ <mat-icon aria-hidden="true">{{
2298
+ activeRisks.length === 0 ? 'verified' : 'warning'
2299
+ }}</mat-icon>
2300
+ {{ tx('editor.risks.title', 'Risks and attention points') }}
2301
+ </h4>
2302
+ <div *ngIf="activeRisks.length > 0; else noRiskState" class="risk-list">
2303
+ <article class="risk-item" *ngFor="let risk of activeRisks">
2304
+ <strong>{{ risk.title }}</strong>
2305
+ <p>{{ risk.detail }}</p>
2306
+ </article>
2307
+ </div>
2308
+ <ng-template #noRiskState>
2309
+ <p class="safe-copy">
2310
+ {{ tx('editor.risks.none', 'No evident operational risk in the current configuration.') }}
2311
+ </p>
2312
+ </ng-template>
2313
+ </section>
2314
+ </aside>
2315
+
2316
+ <div class="editor-main">
2317
+ <mat-tab-group>
2318
+ <mat-tab [label]="tx('editor.tabs.behavior', 'Behavior')">
2319
+ <div class="tab-panel">
2320
+ <section class="tab-intro">
2321
+ <h3>{{ tx('editor.behavior.intro.title', 'Upload strategy and cadence') }}</h3>
2322
+ <p>
2323
+ {{ tx('editor.behavior.intro.body', 'Define how the user starts the upload and how the batch behaves in terms of parallelism and retries.') }}
2324
+ </p>
2325
+ </section>
2326
+ <section class="config-section">
2327
+ <form [formGroup]="form">
2328
+ <mat-form-field appearance="fill">
2329
+ <mat-label>{{ tx('editor.behavior.strategy.label', 'Upload strategy') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2330
+ <mat-select formControlName="strategy">
2331
+ <mat-option value="direct">{{ tx('editor.behavior.strategy.direct', 'Direct (standard HTTP)') }}</mat-option>
2332
+ <mat-option value="presign">{{ tx('editor.behavior.strategy.presign', 'Presigned URL (S3/GCS)') }}</mat-option>
2333
+ <mat-option value="auto"
2334
+ >{{ tx('editor.behavior.strategy.auto', 'Automatic (tries presigned first and falls back to direct)') }}</mat-option
2335
+ >
2336
+ </mat-select>
2337
+ <button
2338
+ mat-icon-button
2339
+ matSuffix
2340
+ class="help-icon-button"
2341
+ type="button"
2342
+ [matTooltip]="tx('editor.behavior.strategy.hint', 'How files will be sent to the server.')"
2343
+ matTooltipPosition="above"
2344
+ >
2345
+ <mat-icon>help_outline</mat-icon>
2346
+ </button>
2347
+ </mat-form-field>
2348
+ </form>
2349
+ </section>
2350
+ <section class="config-section">
2351
+ <form [formGroup]="bulkGroup">
2352
+ <h4 class="section-subtitle">
2353
+ <mat-icon aria-hidden="true">build</mat-icon>
2354
+ {{ tx('editor.behavior.bulk.title', 'Configurable options — Batch') }}
2355
+ </h4>
2356
+ <p class="section-note">
2357
+ {{ tx('editor.behavior.bulk.note', 'Use parallelism and retries in moderation. In enterprise environments these controls affect throughput, perceived experience and backend load.') }}
2358
+ </p>
2359
+ <mat-form-field appearance="fill">
2360
+ <mat-label>{{ tx('editor.behavior.bulk.parallel', 'Parallel uploads') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2361
+ <input matInput type="number" formControlName="parallelUploads" />
2362
+ <button
2363
+ mat-icon-button
2364
+ matSuffix
2365
+ class="help-icon-button"
2366
+ type="button"
2367
+ [matTooltip]="tx('editor.behavior.bulk.parallelHint', 'How many files to send at the same time.')"
2368
+ matTooltipPosition="above"
2369
+ >
2370
+ <mat-icon>help_outline</mat-icon>
2371
+ </button>
2372
+ </mat-form-field>
2373
+ <mat-form-field appearance="fill">
2374
+ <mat-label>{{ tx('editor.behavior.bulk.retryCount', 'Retry count') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2375
+ <input matInput type="number" formControlName="retryCount" />
2376
+ <button
2377
+ mat-icon-button
2378
+ matSuffix
2379
+ class="help-icon-button"
2380
+ type="button"
2381
+ [matTooltip]="tx('editor.behavior.bulk.retryCountHint', 'Automatic attempts in case of failure.')"
2382
+ matTooltipPosition="above"
2383
+ >
2384
+ <mat-icon>help_outline</mat-icon>
2385
+ </button>
2386
+ </mat-form-field>
2387
+ <mat-form-field appearance="fill">
2388
+ <mat-label>{{ tx('editor.behavior.bulk.retryBackoff', 'Retry interval (ms)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2389
+ <input matInput type="number" formControlName="retryBackoffMs" />
2390
+ <button
2391
+ mat-icon-button
2392
+ matSuffix
2393
+ class="help-icon-button"
2394
+ type="button"
2395
+ [matTooltip]="tx('editor.behavior.bulk.retryBackoffHint', 'Wait time between attempts.')"
2396
+ matTooltipPosition="above"
2397
+ >
2398
+ <mat-icon>help_outline</mat-icon>
2399
+ </button>
2400
+ </mat-form-field>
2401
+ </form>
2402
+ </section>
2403
+ </div>
2404
+ </mat-tab>
2405
+ <mat-tab [label]="tx('editor.tabs.interface', 'Interface')">
2406
+ <div class="tab-panel">
2407
+ <section class="tab-intro">
2408
+ <h3>{{ tx('editor.interface.intro.title', 'Visible experience for the user') }}</h3>
2409
+ <p>
2410
+ {{ tx('editor.interface.intro.body', 'Configure density, dropzone, metadata and list details. Prioritize operational clarity before enabling advanced capabilities.') }}
2411
+ </p>
2412
+ </section>
2413
+ <section class="config-section">
2414
+ <form [formGroup]="uiGroup">
2415
+ <h4 class="section-subtitle">
2416
+ <mat-icon aria-hidden="true">edit</mat-icon>
2417
+ {{ tx('editor.interface.section.title', 'Configurable options — Interface') }}
2418
+ </h4>
2419
+ <p class="section-note">
2420
+ {{ tx('editor.interface.section.note', 'This section controls the component ergonomics. Changes here affect discoverability, usage effort and upload completion rate.') }}
2421
+ </p>
2422
+ <mat-checkbox formControlName="showDropzone"
2423
+ >{{ tx('editor.interface.showDropzone', 'Show drop area') }}</mat-checkbox
2424
+ >
2425
+ <mat-checkbox formControlName="showProgress"
2426
+ >{{ tx('editor.interface.showProgress', 'Show progress bar') }}</mat-checkbox
2427
+ >
2428
+ <mat-checkbox formControlName="showConflictPolicySelector"
2429
+ >{{ tx('editor.interface.showConflictPolicySelector', 'Allow choosing the conflict policy') }}</mat-checkbox
2430
+ >
2431
+ <mat-checkbox formControlName="manualUpload"
2432
+ >{{ tx('editor.interface.manualUpload', 'Require clicking "Upload" (manual mode)') }}</mat-checkbox>
2433
+ <mat-checkbox formControlName="dense">{{ tx('editor.interface.dense', 'Compact layout') }}</mat-checkbox>
2434
+ <mat-form-field appearance="fill">
2435
+ <mat-label>{{ tx('editor.interface.accept', 'Allowed types (accept)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2436
+ <input
2437
+ matInput
2438
+ formControlName="accept"
2439
+ [placeholder]="tx('editor.placeholders.accept', 'e.g.: pdf,jpg,png')"
2440
+ />
2441
+ <button
2442
+ mat-icon-button
2443
+ matSuffix
2444
+ class="help-icon-button"
2445
+ type="button"
2446
+ [matTooltip]="tx('editor.shared.csvOptional', 'Comma-separated list (optional).')"
2447
+ matTooltipPosition="above"
2448
+ >
2449
+ <mat-icon>help_outline</mat-icon>
2450
+ </button>
2451
+ </mat-form-field>
2452
+ <mat-checkbox formControlName="showMetadataForm"
2453
+ >{{ tx('editor.interface.metadataForm', 'Show metadata form (JSON)') }}</mat-checkbox
2454
+ >
2517
2455
 
2518
- /** @deprecated Transitional compatibility token. Prefer PraxisI18nService. */
2519
- const FILES_UPLOAD_TEXTS = new InjectionToken('FILES_UPLOAD_TEXTS', {
2520
- providedIn: 'root',
2521
- factory: () => ({
2522
- settingsAriaLabel: 'Abrir configurações',
2523
- dropzoneLabel: 'Arraste arquivos ou',
2524
- dropzoneButton: 'selecionar',
2525
- conflictPolicyLabel: 'Política de conflito',
2526
- metadataLabel: 'Metadados (JSON)',
2527
- progressAriaLabel: 'Progresso do upload',
2528
- rateLimitBanner: 'Limite de requisições excedido. Tente novamente às',
2529
- }),
2530
- });
2531
- /** @deprecated Transitional compatibility token. Prefer PraxisI18nService. */
2532
- const TRANSLATE_LIKE = new InjectionToken('TRANSLATE_LIKE');
2533
- function createPraxisFilesUploadI18nConfig(options = {}) {
2534
- const dictionaries = {
2535
- 'pt-BR': {
2536
- ...FILES_UPLOAD_PT_BR,
2537
- ...(options.dictionaries?.['pt-BR'] ?? {}),
2538
- },
2539
- 'en-US': {
2540
- ...FILES_UPLOAD_EN_US,
2541
- ...(options.dictionaries?.['en-US'] ?? {}),
2542
- },
2543
- };
2544
- for (const [locale, dictionary] of Object.entries(options.dictionaries ?? {})) {
2545
- if (locale === 'pt-BR' || locale === 'en-US') {
2546
- continue;
2547
- }
2548
- dictionaries[locale] = {
2549
- ...(dictionaries[locale] ?? {}),
2550
- ...dictionary,
2551
- };
2552
- }
2553
- return {
2554
- locale: options.locale,
2555
- fallbackLocale: options.fallbackLocale ?? 'pt-BR',
2556
- dictionaries,
2557
- };
2558
- }
2559
- function providePraxisFilesUploadI18n(options = {}) {
2560
- return providePraxisI18n(createPraxisFilesUploadI18nConfig(options));
2561
- }
2562
- function resolvePraxisFilesUploadText(i18n, key, fallback, legacyTexts, legacyTranslate) {
2563
- const namespacedKey = key.startsWith('praxis.filesUpload.')
2564
- ? key
2565
- : `praxis.filesUpload.${key}`;
2566
- const coreTranslation = i18n.t(namespacedKey, undefined, '');
2567
- if (coreTranslation) {
2568
- return coreTranslation;
2569
- }
2570
- const legacyText = legacyTexts?.[key];
2571
- if (legacyText) {
2572
- return legacyText;
2573
- }
2574
- const translatedByLegacyService = legacyTranslate?.t?.(namespacedKey, undefined, '') ||
2575
- legacyTranslate?.t?.(key, undefined, '') ||
2576
- resolveLegacyInstantTranslation(legacyTranslate, namespacedKey) ||
2577
- resolveLegacyInstantTranslation(legacyTranslate, key);
2578
- if (translatedByLegacyService) {
2579
- return translatedByLegacyService;
2580
- }
2581
- return fallback;
2582
- }
2583
- function resolveLegacyInstantTranslation(legacyTranslate, key) {
2584
- const translated = legacyTranslate?.instant?.(key);
2585
- return translated && translated !== key ? translated : '';
2586
- }
2456
+ <!-- NOVO: Grupo Dropzone -->
2457
+ <fieldset [formGroup]="dropzoneGroup" class="subgroup">
2458
+ <legend>
2459
+ <mat-icon aria-hidden="true">download</mat-icon>
2460
+ {{ tx('editor.interface.dropzone.title', 'Dropzone (proximity expansion)') }}
2461
+ </legend>
2462
+ <mat-checkbox formControlName="expandOnDragProximity">
2463
+ {{ tx('editor.interface.dropzone.expandOnProximity', 'Expand when a file gets close during drag') }}
2464
+ </mat-checkbox>
2465
+ <mat-form-field appearance="fill">
2466
+ <mat-label>{{ tx('editor.interface.dropzone.proximity', 'Proximity radius (px)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2467
+ <input matInput type="number" formControlName="proximityPx" />
2468
+ </mat-form-field>
2469
+ <mat-form-field appearance="fill">
2470
+ <mat-label>{{ tx('editor.interface.dropzone.mode', 'Expansion mode') }}</mat-label>
2471
+ <mat-select formControlName="expandMode">
2472
+ <mat-option value="overlay">{{ tx('editor.interface.dropzone.mode.overlay', 'Overlay (recommended)') }}</mat-option>
2473
+ <mat-option value="inline">{{ tx('editor.interface.dropzone.mode.inline', 'Inline') }}</mat-option>
2474
+ </mat-select>
2475
+ </mat-form-field>
2476
+ <mat-form-field appearance="fill">
2477
+ <mat-label>{{ tx('editor.interface.dropzone.height', 'Overlay height (px)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2478
+ <input matInput type="number" formControlName="expandHeight" />
2479
+ </mat-form-field>
2480
+ <mat-form-field appearance="fill">
2481
+ <mat-label>{{ tx('editor.interface.dropzone.debounce', 'Drag debounce (ms)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2482
+ <input matInput type="number" formControlName="expandDebounceMs" />
2483
+ </mat-form-field>
2484
+ </fieldset>
2485
+
2486
+ <!-- NOVO: Grupo Lista/Detalhes -->
2487
+ <fieldset [formGroup]="listGroup" class="subgroup">
2488
+ <legend>
2489
+ <mat-icon aria-hidden="true">view_list</mat-icon>
2490
+ {{ tx('editor.interface.list.title', 'List and details') }}
2491
+ </legend>
2492
+ <mat-form-field appearance="fill">
2493
+ <mat-label>{{ tx('editor.interface.list.collapseAfter', 'Collapse after (items)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2494
+ <input matInput type="number" formControlName="collapseAfter" />
2495
+ </mat-form-field>
2496
+ <mat-form-field appearance="fill">
2497
+ <mat-label>{{ tx('editor.interface.list.detailsMode', 'Details mode') }}</mat-label>
2498
+ <mat-select formControlName="detailsMode">
2499
+ <mat-option value="auto">{{ tx('editor.interface.list.detailsMode.auto', 'Automatic') }}</mat-option>
2500
+ <mat-option value="card">{{ tx('editor.interface.list.detailsMode.card', 'Card (overlay)') }}</mat-option>
2501
+ <mat-option value="sidesheet">{{ tx('editor.interface.list.detailsMode.sidesheet', 'Side-sheet') }}</mat-option>
2502
+ </mat-select>
2503
+ </mat-form-field>
2504
+ <mat-form-field appearance="fill">
2505
+ <mat-label>{{ tx('editor.interface.list.detailsWidth', 'Maximum card width (px)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2506
+ <input matInput type="number" formControlName="detailsMaxWidth" />
2507
+ </mat-form-field>
2508
+ <mat-checkbox formControlName="detailsShowTechnical">
2509
+ {{ tx('editor.interface.list.showTechnical', 'Show technical details by default') }}
2510
+ </mat-checkbox>
2511
+ <mat-form-field appearance="fill">
2512
+ <mat-label>{{ tx('editor.interface.list.detailsFields', 'Metadata fields (whitelist)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2513
+ <input matInput formControlName="detailsFields" [placeholder]="tx('editor.placeholders.detailsFields', 'e.g.: id,fileName,contentType')" />
2514
+ <button
2515
+ mat-icon-button
2516
+ matSuffix
2517
+ class="help-icon-button"
2518
+ type="button"
2519
+ [matTooltip]="tx('editor.interface.list.detailsFieldsHint', 'Comma-separated list; empty = all.')"
2520
+ matTooltipPosition="above"
2521
+ >
2522
+ <mat-icon>help_outline</mat-icon>
2523
+ </button>
2524
+ </mat-form-field>
2525
+ <mat-form-field appearance="fill">
2526
+ <mat-label>{{ tx('editor.interface.list.anchor', 'Overlay anchor') }}</mat-label>
2527
+ <mat-select formControlName="detailsAnchor">
2528
+ <mat-option value="item">{{ tx('editor.interface.list.anchor.item', 'Item') }}</mat-option>
2529
+ <mat-option value="field">{{ tx('editor.interface.list.anchor.field', 'Field') }}</mat-option>
2530
+ </mat-select>
2531
+ </mat-form-field>
2532
+ </fieldset>
2533
+ </form>
2534
+ </section>
2535
+ </div>
2536
+ </mat-tab>
2537
+ <mat-tab [label]="tx('editor.tabs.validation', 'Validation')">
2538
+ <div class="tab-panel">
2539
+ <section class="tab-intro">
2540
+ <h3>{{ tx('editor.validation.intro.title', 'Acceptance rules and operational behavior') }}</h3>
2541
+ <p>
2542
+ {{ tx('editor.validation.intro.body', 'Separate local UX validation from effective backend rules. Review destructive options carefully.') }}
2543
+ </p>
2544
+ </section>
2545
+ <section class="config-section">
2546
+ <form [formGroup]="limitsGroup">
2547
+ <h4 class="section-subtitle">
2548
+ <mat-icon aria-hidden="true">build</mat-icon>
2549
+ {{ tx('editor.validation.local.title', 'Configurable options — Validation') }}
2550
+ </h4>
2551
+ <p class="section-note">
2552
+ {{ tx('editor.validation.local.note', 'Local limits improve immediate feedback, but do not replace the effective server policy.') }}
2553
+ </p>
2554
+ <mat-form-field appearance="fill">
2555
+ <mat-label>{{ tx('editor.validation.local.maxFileSize', 'Maximum file size (bytes)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2556
+ <input matInput type="number" formControlName="maxFileSizeBytes" />
2557
+ <button
2558
+ mat-icon-button
2559
+ matSuffix
2560
+ class="help-icon-button"
2561
+ type="button"
2562
+ [matTooltip]="tx('editor.validation.local.maxFileSizeHint', 'Client-side validation limit (optional).')"
2563
+ matTooltipPosition="above"
2564
+ >
2565
+ <mat-icon>help_outline</mat-icon>
2566
+ </button>
2567
+ </mat-form-field>
2568
+ <mat-form-field appearance="fill">
2569
+ <mat-label>{{ tx('editor.validation.local.maxFilesPerBulk', 'Max files per batch') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2570
+ <input matInput type="number" formControlName="maxFilesPerBulk" />
2571
+ </mat-form-field>
2572
+ <mat-form-field appearance="fill">
2573
+ <mat-label>{{ tx('editor.validation.local.maxBulkSize', 'Maximum batch size (bytes)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2574
+ <input matInput type="number" formControlName="maxBulkSizeBytes" />
2575
+ </mat-form-field>
2576
+ </form>
2577
+ </section>
2578
+ <section class="config-section">
2579
+ <form [formGroup]="optionsGroup">
2580
+ <h4 class="section-subtitle">
2581
+ <mat-icon aria-hidden="true">tune</mat-icon>
2582
+ {{ tx('editor.validation.backend.title', 'Configurable options — Backend') }}
2583
+ </h4>
2584
+ <p class="section-note">
2585
+ {{ tx('editor.validation.backend.note', 'These options directly impact the server contract and operational governance. Treat changes here as policy decisions, not only UI changes.') }}
2586
+ </p>
2587
+ <mat-form-field appearance="fill">
2588
+ <mat-label>{{ tx('editor.validation.backend.conflictPolicy', 'Conflict policy (default)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2589
+ <mat-select formControlName="defaultConflictPolicy">
2590
+ <mat-option value="RENAME">{{ tx('editor.validation.backend.conflictPolicy.rename', 'Rename automatically') }}</mat-option>
2591
+ <mat-option value="MAKE_UNIQUE">{{ tx('editor.validation.backend.conflictPolicy.unique', 'Generate unique name') }}</mat-option>
2592
+ <mat-option value="OVERWRITE">{{ tx('editor.validation.backend.conflictPolicy.overwrite', 'Overwrite existing file') }}</mat-option>
2593
+ <mat-option value="SKIP">{{ tx('editor.validation.backend.conflictPolicy.skip', 'Skip if already exists') }}</mat-option>
2594
+ <mat-option value="ERROR">{{ tx('editor.validation.backend.conflictPolicy.error', 'Fail (error)') }}</mat-option>
2595
+ </mat-select>
2596
+ <button
2597
+ mat-icon-button
2598
+ matSuffix
2599
+ class="help-icon-button"
2600
+ type="button"
2601
+ [matTooltip]="tx('editor.validation.backend.conflictPolicyHint', 'What to do when the file name already exists.')"
2602
+ matTooltipPosition="above"
2603
+ >
2604
+ <mat-icon>help_outline</mat-icon>
2605
+ </button>
2606
+ <div class="warn" *ngIf="optionsGroup.get('defaultConflictPolicy')?.value === 'OVERWRITE'">
2607
+ <mat-icon color="warn" aria-hidden="true">warning</mat-icon>
2608
+ {{ tx('editor.validation.backend.overwriteWarn', 'Warning: OVERWRITE may replace existing files.') }}
2609
+ </div>
2610
+ </mat-form-field>
2611
+ <mat-checkbox formControlName="strictValidation"
2612
+ >{{ tx('editor.validation.backend.strictValidation', 'Strict validation (backend)') }}</mat-checkbox
2613
+ >
2614
+ <mat-form-field appearance="fill">
2615
+ <mat-label>{{ tx('editor.validation.backend.maxUploadSizeMb', 'Maximum size per file (MB)') }} <span class="req-tag">mandatório</span></mat-label>
2616
+ <input matInput type="number" formControlName="maxUploadSizeMb" required />
2617
+ <button
2618
+ mat-icon-button
2619
+ matSuffix
2620
+ class="help-icon-button"
2621
+ type="button"
2622
+ [matTooltip]="tx('editor.validation.backend.maxUploadSizeMbHint', 'Validated by the backend (1–500 MB).')"
2623
+ matTooltipPosition="above"
2624
+ >
2625
+ <mat-icon>help_outline</mat-icon>
2626
+ </button>
2627
+ </mat-form-field>
2628
+ <mat-form-field appearance="fill">
2629
+ <mat-label>{{ tx('editor.validation.backend.allowedExtensions', 'Allowed extensions') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2630
+ <input
2631
+ matInput
2632
+ formControlName="allowedExtensions"
2633
+ [placeholder]="tx('editor.placeholders.allowedExtensions', 'e.g.: pdf,docx,xlsx')"
2634
+ />
2635
+ <button
2636
+ mat-icon-button
2637
+ matSuffix
2638
+ class="help-icon-button"
2639
+ type="button"
2640
+ [matTooltip]="tx('editor.shared.csvOptional', 'Comma-separated list (optional).')"
2641
+ matTooltipPosition="above"
2642
+ >
2643
+ <mat-icon>help_outline</mat-icon>
2644
+ </button>
2645
+ </mat-form-field>
2646
+ <mat-form-field appearance="fill">
2647
+ <mat-label>{{ tx('editor.validation.backend.acceptMimeTypes', 'Accepted MIME types') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2648
+ <input
2649
+ matInput
2650
+ formControlName="acceptMimeTypes"
2651
+ [placeholder]="tx('editor.placeholders.acceptMimeTypes', 'e.g.: application/pdf,image/png')"
2652
+ />
2653
+ <button
2654
+ mat-icon-button
2655
+ matSuffix
2656
+ class="help-icon-button"
2657
+ type="button"
2658
+ [matTooltip]="tx('editor.shared.csvOptional', 'Comma-separated list (optional).')"
2659
+ matTooltipPosition="above"
2660
+ >
2661
+ <mat-icon>help_outline</mat-icon>
2662
+ </button>
2663
+ </mat-form-field>
2664
+ <mat-form-field appearance="fill">
2665
+ <mat-label>{{ tx('editor.validation.backend.targetDirectory', 'Target directory') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2666
+ <input
2667
+ matInput
2668
+ formControlName="targetDirectory"
2669
+ [placeholder]="tx('editor.placeholders.targetDirectory', 'e.g.: documents/invoices')"
2670
+ />
2671
+ </mat-form-field>
2672
+ <mat-checkbox formControlName="enableVirusScanning">
2673
+ {{ tx('editor.validation.backend.enableVirusScanning', 'Force antivirus (when available)') }}
2674
+ </mat-checkbox>
2675
+ <div class="warn" *ngIf="optionsGroup.get('enableVirusScanning')?.value === true">
2676
+ <mat-icon color="warn" aria-hidden="true">warning</mat-icon>
2677
+ {{ tx('editor.validation.backend.virusWarn', 'May impact upload performance and latency.') }}
2678
+ </div>
2679
+ </form>
2680
+ </section>
2681
+ <section class="config-section">
2682
+ <form [formGroup]="quotasGroup">
2683
+ <h4 class="section-subtitle">
2684
+ <mat-icon aria-hidden="true">edit</mat-icon>
2685
+ {{ tx('editor.validation.quotas.title', 'Configurable options — Quotas (UI)') }}
2686
+ </h4>
2687
+ <mat-checkbox formControlName="showQuotaWarnings"
2688
+ >{{ tx('editor.validation.quotas.showWarnings', 'Show quota warnings') }}</mat-checkbox
2689
+ >
2690
+ <mat-checkbox formControlName="blockOnExceed"
2691
+ >{{ tx('editor.validation.quotas.blockOnExceed', 'Block on quota exceed') }}</mat-checkbox
2692
+ >
2693
+ </form>
2694
+ </section>
2695
+ <section class="config-section">
2696
+ <form [formGroup]="rateLimitGroup">
2697
+ <h4 class="section-subtitle">
2698
+ <mat-icon aria-hidden="true">edit</mat-icon>
2699
+ {{ tx('editor.validation.rateLimit.title', 'Configurable options — Rate Limit (UI)') }}
2700
+ </h4>
2701
+ <p class="section-note">
2702
+ {{ tx('editor.validation.rateLimit.note', 'Prefer a small number of automatic retries. More attempts reduce immediate friction, but may hide real environment bottlenecks.') }}
2703
+ </p>
2704
+ <mat-checkbox formControlName="showBannerOn429"
2705
+ >{{ tx('editor.validation.rateLimit.showBanner', 'Show banner when the limit is reached') }}</mat-checkbox
2706
+ >
2707
+ <mat-checkbox formControlName="autoRetryOn429"
2708
+ >{{ tx('editor.validation.rateLimit.autoRetry', 'Retry automatically') }}</mat-checkbox
2709
+ >
2710
+ <mat-form-field appearance="fill">
2711
+ <mat-label>{{ tx('editor.validation.rateLimit.maxAutoRetry', 'Maximum automatic retries') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2712
+ <input matInput type="number" formControlName="maxAutoRetry" />
2713
+ </mat-form-field>
2714
+ <mat-form-field appearance="fill">
2715
+ <mat-label>{{ tx('editor.validation.rateLimit.baseBackoff', 'Base retry interval (ms)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2716
+ <input matInput type="number" formControlName="baseBackoffMs" />
2717
+ </mat-form-field>
2718
+ </form>
2719
+ </section>
2720
+ <section class="config-section">
2721
+ <form [formGroup]="bulkGroup">
2722
+ <h4 class="section-subtitle">
2723
+ <mat-icon aria-hidden="true">bolt</mat-icon>
2724
+ {{ tx('editor.validation.batch.title', 'Configurable options — Batch execution') }}
2725
+ </h4>
2726
+ <mat-checkbox formControlName="failFast"
2727
+ >{{ tx('editor.validation.batch.failFast', 'Stop on the first error (fail-fast)') }}</mat-checkbox
2728
+ >
2729
+ </form>
2730
+ </section>
2731
+ </div>
2732
+ </mat-tab>
2733
+ <mat-tab [label]="tx('editor.tabs.messages', 'Messages')">
2734
+ <div class="tab-panel">
2735
+ <section class="tab-intro">
2736
+ <h3>{{ tx('editor.messages.intro.title', 'Messages oriented to operations') }}</h3>
2737
+ <p>
2738
+ {{ tx('editor.messages.intro.body', 'Adjust the text shown to the end user. Prioritize clarity, consistency and actionable language.') }}
2739
+ </p>
2740
+ </section>
2741
+ <section class="config-section">
2742
+ <form [formGroup]="messagesGroup">
2743
+ <h4 class="section-subtitle">
2744
+ <mat-icon aria-hidden="true">edit</mat-icon>
2745
+ {{ tx('editor.messages.section.title', 'Configurable options — Messages (UI)') }}
2746
+ </h4>
2747
+ <p class="section-note">
2748
+ {{ tx('editor.messages.section.note', 'Short and objective messages work better in high-frequency contexts. Use this group to adapt the interface voice to your environment.') }}
2749
+ </p>
2750
+ <mat-form-field appearance="fill">
2751
+ <mat-label>{{ tx('editor.messages.successSingle', 'Success (single)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2752
+ <input
2753
+ matInput
2754
+ formControlName="successSingle"
2755
+ [placeholder]="tx('editor.placeholders.successSingle', 'e.g.: File uploaded successfully')"
2756
+ />
2757
+ </mat-form-field>
2758
+ <mat-form-field appearance="fill">
2759
+ <mat-label>{{ tx('editor.messages.successBulk', 'Success (batch)') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2760
+ <input
2761
+ matInput
2762
+ formControlName="successBulk"
2763
+ [placeholder]="tx('editor.placeholders.successBulk', 'e.g.: Upload completed')"
2764
+ />
2765
+ </mat-form-field>
2766
+ <div [formGroup]="errorsGroup">
2767
+ <ng-container *ngFor="let e of errorEntries">
2768
+ <mat-form-field appearance="fill">
2769
+ <mat-label>{{ e.label }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2770
+ <input matInput [formControlName]="e.code" />
2771
+ <mat-hint class="code-hint">{{ e.code }}</mat-hint>
2772
+ </mat-form-field>
2773
+ </ng-container>
2774
+ </div>
2775
+ </form>
2776
+ </section>
2777
+ </div>
2778
+ </mat-tab>
2779
+ <mat-tab [label]="tx('editor.tabs.headers', 'Headers')">
2780
+ <div class="tab-panel">
2781
+ <section class="tab-intro">
2782
+ <h3>{{ tx('editor.headers.intro.title', 'Tenant and user context') }}</h3>
2783
+ <p>
2784
+ {{ tx('editor.headers.intro.body', 'Use headers to query the effective server configuration in the same context the component will use in production.') }}
2785
+ </p>
2786
+ </section>
2787
+ <section class="config-section">
2788
+ <form [formGroup]="headersGroup">
2789
+ <h4 class="section-subtitle">
2790
+ <mat-icon aria-hidden="true">edit</mat-icon>
2791
+ {{ tx('editor.headers.section.title', 'Configurable options — Headers (query)') }}
2792
+ </h4>
2793
+ <mat-form-field appearance="fill">
2794
+ <mat-label>{{ tx('editor.headers.tenantHeader', 'Tenant header') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2795
+ <input
2796
+ matInput
2797
+ formControlName="tenantHeader"
2798
+ placeholder="X-Tenant-Id"
2799
+ />
2800
+ </mat-form-field>
2801
+ <mat-form-field appearance="fill">
2802
+ <mat-label>{{ tx('editor.headers.tenantValue', 'Tenant value') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2803
+ <input
2804
+ matInput
2805
+ formControlName="tenantValue"
2806
+ [placeholder]="tx('editor.placeholders.tenantValue', 'e.g.: demo-tenant')"
2807
+ />
2808
+ </mat-form-field>
2809
+ <mat-form-field appearance="fill">
2810
+ <mat-label>{{ tx('editor.headers.userHeader', 'User header') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2811
+ <input
2812
+ matInput
2813
+ formControlName="userHeader"
2814
+ placeholder="X-User-Id"
2815
+ />
2816
+ </mat-form-field>
2817
+ <mat-form-field appearance="fill">
2818
+ <mat-label>{{ tx('editor.headers.userValue', 'User value') }} <span class="opt-tag">{{ tx('editor.shared.optionalTag', 'optional') }}</span></mat-label>
2819
+ <input matInput formControlName="userValue" [placeholder]="tx('editor.placeholders.userValue', 'e.g.: 42')" />
2820
+ </mat-form-field>
2821
+ </form>
2822
+ </section>
2823
+ </div>
2824
+ </mat-tab>
2825
+ <mat-tab [label]="tx('editor.tabs.server', 'Server')">
2826
+ <div class="tab-panel">
2827
+ <section class="tab-intro">
2828
+ <h3>{{ tx('editor.server.intro.title', 'Effective contract returned by the backend') }}</h3>
2829
+ <p>
2830
+ {{ tx('editor.server.intro.body', 'This tab is the source of truth for the active environment. Compare the server summary with what was configured in the previous forms.') }}
2831
+ </p>
2832
+ </section>
2833
+ <section class="config-section">
2834
+ <div class="server-tab">
2835
+ <div class="toolbar">
2836
+ <h4 class="section-subtitle ro">
2837
+ <mat-icon aria-hidden="true">info</mat-icon>
2838
+ {{ tx('editor.server.readonly', 'Server (read-only)') }}
2839
+ <span class="badge">read-only</span>
2840
+ </h4>
2841
+ <button type="button" (click)="refetchServerConfig()">
2842
+ {{ tx('editor.server.reload', 'Reload from server') }}
2843
+ </button>
2844
+ <span class="hint" *ngIf="!baseUrl"
2845
+ >{{ tx('editor.server.baseUrlHint', 'Set baseUrl on the parent component to query /api/files/config.') }}</span
2846
+ >
2847
+ </div>
2848
+ <div *ngIf="serverLoading(); else serverLoaded">
2849
+ {{ tx('editor.server.loading', 'Loading server configuration...') }}
2850
+ </div>
2851
+ <ng-template #serverLoaded>
2852
+ <div *ngIf="serverError(); else serverOk" class="error">
2853
+ {{ tx('editor.server.loadError', 'Failed to load:') }} {{ serverError() | json }}
2854
+ </div>
2855
+ <ng-template #serverOk>
2856
+ <section *ngIf="serverData() as _">
2857
+ <h3>{{ tx('editor.server.summaryTitle', 'Active configuration summary') }}</h3>
2858
+ <ul class="summary">
2859
+ <li>
2860
+ <strong>{{ tx('editor.server.summary.maxPerFile', 'Max per file (MB):') }}</strong>
2861
+ {{ serverData()?.options?.maxUploadSizeMb }}
2862
+ </li>
2863
+ <li>
2864
+ <strong>{{ tx('editor.server.summary.strictValidation', 'Strict validation:') }}</strong>
2865
+ {{ serverData()?.options?.strictValidation }}
2866
+ </li>
2867
+ <li>
2868
+ <strong>{{ tx('editor.server.summary.antivirus', 'Antivirus:') }}</strong>
2869
+ {{ serverData()?.options?.enableVirusScanning }}
2870
+ </li>
2871
+ <li>
2872
+ <strong>{{ tx('editor.server.summary.conflictPolicy', 'Default name conflict:') }}</strong>
2873
+ {{ serverData()?.options?.nameConflictPolicy }}
2874
+ </li>
2875
+ <li>
2876
+ <strong>{{ tx('editor.server.summary.mimeTypes', 'Accepted MIME types:') }}</strong>
2877
+ {{
2878
+ (serverData()?.options?.acceptMimeTypes || []).join(', ')
2879
+ }}
2880
+ </li>
2881
+ <li>
2882
+ <strong>{{ tx('editor.server.summary.bulkFailFast', 'Default bulk fail-fast:') }}</strong>
2883
+ {{ serverData()?.bulk?.failFastModeDefault }}
2884
+ </li>
2885
+ <li>
2886
+ <strong>{{ tx('editor.server.summary.rateLimit', 'Rate limit:') }}</strong>
2887
+ {{ serverData()?.rateLimit?.enabled }} ({{
2888
+ serverData()?.rateLimit?.perMinute
2889
+ }}/min, {{ serverData()?.rateLimit?.perHour }}/h)
2890
+ </li>
2891
+ <li>
2892
+ <strong>{{ tx('editor.server.summary.quotas', 'Quotas:') }}</strong> {{ serverData()?.quotas?.enabled }}
2893
+ </li>
2894
+ <li>
2895
+ <strong>{{ tx('editor.server.summary.server', 'Server:') }}</strong> v{{
2896
+ serverData()?.metadata?.version
2897
+ }}
2898
+ • {{ serverData()?.metadata?.locale }}
2899
+ </li>
2900
+ </ul>
2901
+ <details>
2902
+ <summary>{{ tx('editor.server.viewJson', 'View JSON') }}</summary>
2903
+ <button
2904
+ mat-icon-button
2905
+ [attr.aria-label]="tx('editor.server.copyJson', 'Copy JSON')"
2906
+ (click)="copyServerConfig()"
2907
+ type="button"
2908
+ [title]="tx('editor.server.copyJson', 'Copy JSON')"
2909
+ >
2910
+ <mat-icon>content_copy</mat-icon>
2911
+ </button>
2912
+ <pre>{{ serverData() | json }}</pre>
2913
+ </details>
2914
+ <p class="note">
2915
+ {{ tx('editor.server.mutableOptionsText', 'The options above that can be changed via payload are: name conflict, strict validation, maximum size (MB), accepted extensions/MIME, target directory, antivirus, custom metadata and fail-fast (in bulk).') }}
2916
+ </p>
2917
+ </section>
2918
+ </ng-template>
2919
+ </ng-template>
2920
+ </div>
2921
+ </section>
2922
+ </div>
2923
+ </mat-tab>
2924
+ <mat-tab [label]="tx('editor.tabs.json', 'JSON')">
2925
+ <div class="tab-panel">
2926
+ <section class="tab-intro">
2927
+ <h3>{{ tx('editor.json.intro.title', 'Advanced mode') }}</h3>
2928
+ <p>
2929
+ {{ tx('editor.json.intro.body', 'Edit the raw payload only when you need fine-grained control. Changes here require stricter review and validation.') }}
2930
+ </p>
2931
+ </section>
2932
+ <div class="advanced-note">
2933
+ {{ tx('editor.json.note', 'JSON is a low-level view of the configuration. Prefer the guided tabs whenever possible to reduce human error and keep the configuration auditable.') }}
2934
+ </div>
2935
+ <textarea
2936
+ class="json-textarea"
2937
+ rows="10"
2938
+ [ngModel]="form.value | json"
2939
+ (ngModelChange)="onJsonChange($event)"
2940
+ ></textarea>
2941
+ <div class="error" *ngIf="jsonError">{{ jsonError }}</div>
2942
+ </div>
2943
+ </mat-tab>
2944
+ </mat-tab-group>
2945
+ </div>
2946
+ </div>
2947
+ `, styles: [".editor-layout{display:grid;grid-template-columns:minmax(260px,320px) minmax(0,1fr);gap:16px;align-items:start}.summary-panel{position:sticky;top:0;display:flex;flex-direction:column;gap:12px;padding:16px;border:1px solid var(--pfx-surface-border, #d8d8d8);border-radius:16px;background:linear-gradient(180deg,#fafbfc,#fff)}.summary-panel h3{margin:0;font-size:1rem;font-weight:600}.summary-panel .summary-intro{margin:0;color:#000000a6;line-height:1.4;font-size:.88rem}.summary-grid{display:grid;gap:10px}.summary-card{padding:12px;border-radius:12px;background:#fff;border:1px solid #e7e9ee}.summary-card .eyebrow{display:block;margin-bottom:4px;font-size:.72rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:#5f6b7a}.summary-card strong{display:block;margin-bottom:4px;font-size:.95rem}.summary-card p{margin:0;font-size:.85rem;line-height:1.45;color:#000000b8}.risk-panel{padding:12px;border-radius:12px;background:#fff8e8;border:1px solid #f0d29a}.risk-panel.safe{background:#f4fbf6;border-color:#b8ddc1}.risk-panel h4{display:flex;align-items:center;gap:6px;margin:0 0 8px;font-size:.92rem}.risk-list{display:grid;gap:8px}.risk-item{padding:10px;border-radius:10px;background:#ffffffb8}.risk-item strong{display:block;margin-bottom:2px;font-size:.84rem}.risk-item p,.risk-panel .safe-copy{margin:0;font-size:.82rem;line-height:1.4;color:#000000b8}.editor-main{min-width:0}.tab-panel{display:grid;gap:16px;padding-top:8px}.tab-intro{padding:16px 18px;border:1px solid #e5e8ef;border-radius:16px;background:linear-gradient(180deg,#fff,#f7f9fc)}.tab-intro h3{margin:0 0 6px;font-size:1rem;font-weight:600}.tab-intro p{margin:0;font-size:.9rem;line-height:1.5;color:#000000b8}.config-section{padding:18px;border:1px solid #e5e8ef;border-radius:16px;background:#fff}.config-section+.config-section{margin-top:0}.section-note{margin:0 0 16px;color:#000000b3;font-size:.88rem;line-height:1.45}.advanced-note{padding:14px 16px;border-radius:14px;border:1px solid #f0d29a;background:#fff8e8;color:#000000c7;font-size:.88rem;line-height:1.45}.json-textarea{width:100%;min-height:280px;padding:14px 16px;border-radius:14px;border:1px solid #d7dce5;background:#0f172a;color:#e5eefc;font:500 .84rem/1.55 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;resize:vertical}.server-tab{display:grid;gap:16px}.toolbar{display:flex;align-items:center;gap:12px;flex-wrap:wrap}.summary{display:grid;gap:8px;padding-left:18px}.summary li{line-height:1.45}details{margin-top:12px;padding:14px 16px;border:1px solid #e5e8ef;border-radius:14px;background:#fbfcfe}details pre{white-space:pre-wrap;word-break:break-word}@media(max-width:1100px){.editor-layout{grid-template-columns:1fr}.summary-panel{position:static}}.help-icon-button{--mdc-icon-button-state-layer-size: 28px;--mdc-icon-button-icon-size: 18px;width:28px;height:28px;padding:0;display:inline-flex;align-items:center;justify-content:center;margin-right:-4px}.help-icon-button mat-icon{font-size:18px;width:18px;height:18px}.mat-mdc-form-field-icon-suffix{align-self:center}\n"] }]
2948
+ }], ctorParameters: () => [{ type: i1$1.FormBuilder }, { type: undefined, decorators: [{
2949
+ type: Inject,
2950
+ args: [SETTINGS_PANEL_DATA]
2951
+ }] }, { type: i2.MatSnackBar }, { type: i0.DestroyRef }] });
2587
2952
 
2588
2953
  function toError(key, info) {
2589
2954
  return { [key]: info };