@praxisui/files-upload 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4213 @@
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';
3
+ import * as i7 from '@angular/common';
4
+ import { CommonModule } from '@angular/common';
5
+ import * as i1$1 from '@angular/forms';
6
+ import { ReactiveFormsModule, FormsModule, FormControl } from '@angular/forms';
7
+ import * as i9 from '@angular/material/button';
8
+ import { MatButtonModule } from '@angular/material/button';
9
+ import * as i10 from '@angular/material/icon';
10
+ import { MatIconModule } from '@angular/material/icon';
11
+ import * as i11 from '@angular/material/progress-bar';
12
+ import { MatProgressBarModule } from '@angular/material/progress-bar';
13
+ import * as i3$1 from '@angular/material/menu';
14
+ import { MatMenuModule } from '@angular/material/menu';
15
+ import * as i1 from '@angular/common/http';
16
+ import { HttpHeaders, HttpEventType, HttpErrorResponse } from '@angular/common/http';
17
+ import { CONFIG_STORAGE, PraxisIconDirective, ComponentMetadataRegistry } from '@praxisui/core';
18
+ import * as i4 from '@angular/material/tabs';
19
+ import { MatTabsModule } from '@angular/material/tabs';
20
+ import * as i5 from '@angular/material/form-field';
21
+ import { MatFormFieldModule } from '@angular/material/form-field';
22
+ import * as i6 from '@angular/material/input';
23
+ import { MatInputModule } from '@angular/material/input';
24
+ import * as i7$1 from '@angular/material/checkbox';
25
+ import { MatCheckboxModule } from '@angular/material/checkbox';
26
+ import * as i8 from '@angular/material/select';
27
+ import { MatSelectModule } from '@angular/material/select';
28
+ import * as i2 from '@angular/material/snack-bar';
29
+ import { MatSnackBarModule } from '@angular/material/snack-bar';
30
+ import { toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
31
+ import { of, throwError, BehaviorSubject, map as map$1, startWith, retry, debounceTime } from 'rxjs';
32
+ import * as i1$2 from '@praxisui/settings-panel';
33
+ import { SETTINGS_PANEL_DATA } from '@praxisui/settings-panel';
34
+ import { map, catchError } from 'rxjs/operators';
35
+ import { TemplatePortal } from '@angular/cdk/portal';
36
+ import * as i13 from '@angular/material/tooltip';
37
+ import { MatTooltipModule } from '@angular/material/tooltip';
38
+ import * as i3 from '@angular/cdk/overlay';
39
+ import { SimpleBaseInputComponent } from '@praxisui/dynamic-fields';
40
+
41
+ var ErrorCode;
42
+ (function (ErrorCode) {
43
+ ErrorCode["INVALID_FILE_TYPE"] = "INVALID_FILE_TYPE";
44
+ ErrorCode["FILE_TOO_LARGE"] = "FILE_TOO_LARGE";
45
+ ErrorCode["NOT_FOUND"] = "NOT_FOUND";
46
+ ErrorCode["UNAUTHORIZED"] = "UNAUTHORIZED";
47
+ ErrorCode["RATE_LIMIT_EXCEEDED"] = "RATE_LIMIT_EXCEEDED";
48
+ ErrorCode["INTERNAL_ERROR"] = "INTERNAL_ERROR";
49
+ ErrorCode["QUOTA_EXCEEDED"] = "QUOTA_EXCEEDED";
50
+ ErrorCode["SEC_VIRUS_DETECTED"] = "SEC_VIRUS_DETECTED";
51
+ ErrorCode["SEC_MALICIOUS_CONTENT"] = "SEC_MALICIOUS_CONTENT";
52
+ ErrorCode["SEC_DANGEROUS_TYPE"] = "SEC_DANGEROUS_TYPE";
53
+ ErrorCode["FMT_MAGIC_MISMATCH"] = "FMT_MAGIC_MISMATCH";
54
+ ErrorCode["FMT_CORRUPTED"] = "FMT_CORRUPTED";
55
+ ErrorCode["FMT_UNSUPPORTED"] = "FMT_UNSUPPORTED";
56
+ ErrorCode["SYS_STORAGE_ERROR"] = "SYS_STORAGE_ERROR";
57
+ ErrorCode["SYS_SERVICE_DOWN"] = "SYS_SERVICE_DOWN";
58
+ ErrorCode["SYS_RATE_LIMIT"] = "SYS_RATE_LIMIT";
59
+ })(ErrorCode || (ErrorCode = {}));
60
+
61
+ var ScanStatus;
62
+ (function (ScanStatus) {
63
+ ScanStatus["PENDING"] = "PENDING";
64
+ ScanStatus["SCANNING"] = "SCANNING";
65
+ ScanStatus["CLEAN"] = "CLEAN";
66
+ ScanStatus["INFECTED"] = "INFECTED";
67
+ ScanStatus["FAILED"] = "FAILED";
68
+ })(ScanStatus || (ScanStatus = {}));
69
+
70
+ /** Result status for each file in a bulk upload operation. */
71
+ var BulkUploadResultStatus;
72
+ (function (BulkUploadResultStatus) {
73
+ BulkUploadResultStatus["SUCCESS"] = "SUCCESS";
74
+ BulkUploadResultStatus["FAILED"] = "FAILED";
75
+ })(BulkUploadResultStatus || (BulkUploadResultStatus = {}));
76
+
77
+ /**
78
+ * Service responsible for retrieving the effective upload configuration
79
+ * from the backend and caching it for a short period (60s).
80
+ */
81
+ class ConfigService {
82
+ http;
83
+ cache = new Map();
84
+ constructor(http) {
85
+ this.http = http;
86
+ }
87
+ buildCacheKey(baseUrl, headers) {
88
+ const headerKey = JSON.stringify(headers);
89
+ return `${baseUrl}|${headerKey}`;
90
+ }
91
+ /**
92
+ * Fetches the effective configuration from the server.
93
+ * @param baseUrl Base URL for the files API (default: `/api/files`).
94
+ * @param headersProvider Optional callback providing additional headers such as tenant/user.
95
+ */
96
+ getEffectiveConfig(baseUrl = '/api/files', headersProvider) {
97
+ const now = Date.now();
98
+ const headersObj = headersProvider ? headersProvider() : {};
99
+ const key = this.buildCacheKey(baseUrl, headersObj);
100
+ const entry = this.cache.get(key);
101
+ if (entry && now - entry.timestamp < 60_000) {
102
+ return of(entry.data);
103
+ }
104
+ const url = `${baseUrl}/config`;
105
+ if (entry?.etag) {
106
+ headersObj['If-None-Match'] = entry.etag;
107
+ }
108
+ const headers = new HttpHeaders(headersObj);
109
+ return this.http
110
+ .get(url, {
111
+ observe: 'response',
112
+ headers,
113
+ })
114
+ .pipe(map((resp) => {
115
+ const etag = resp.headers.get('ETag') || undefined;
116
+ if (resp.status === 304 && entry) {
117
+ entry.timestamp = now;
118
+ this.cache.set(key, entry);
119
+ return entry.data;
120
+ }
121
+ const data = resp.body?.data;
122
+ this.cache.set(key, { etag, data, timestamp: now });
123
+ return data;
124
+ }), catchError((err) => {
125
+ // HttpClient trata 304 como erro; reaproveitamos cache existente
126
+ if (err?.status === 304 && entry) {
127
+ entry.timestamp = Date.now();
128
+ this.cache.set(key, entry);
129
+ return of(entry.data);
130
+ }
131
+ return throwError(() => err);
132
+ }));
133
+ }
134
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: ConfigService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
135
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: ConfigService, providedIn: 'root' });
136
+ }
137
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: ConfigService, decorators: [{
138
+ type: Injectable,
139
+ args: [{ providedIn: 'root' }]
140
+ }], ctorParameters: () => [{ type: i1.HttpClient }] });
141
+
142
+ /**
143
+ * Angular composable that retrieves the effective upload configuration
144
+ * and exposes reactive signals for template binding.
145
+ */
146
+ function useEffectiveUploadConfig(baseUrl, headersProvider) {
147
+ const service = inject(ConfigService, { optional: true });
148
+ const loading = signal(!!service, ...(ngDevMode ? [{ debugName: "loading" }] : []));
149
+ const data = signal(undefined, ...(ngDevMode ? [{ debugName: "data" }] : []));
150
+ const error = signal(undefined, ...(ngDevMode ? [{ debugName: "error" }] : []));
151
+ const context = signal(undefined, ...(ngDevMode ? [{ debugName: "context" }] : []));
152
+ const fetch = () => {
153
+ if (!service) {
154
+ loading.set(false);
155
+ return;
156
+ }
157
+ loading.set(true);
158
+ const headers = headersProvider ? headersProvider() : undefined;
159
+ context.set(headers);
160
+ service
161
+ .getEffectiveConfig(baseUrl, () => headers ?? {})
162
+ .subscribe({
163
+ next: (cfg) => {
164
+ data.set(cfg);
165
+ error.set(undefined);
166
+ loading.set(false);
167
+ },
168
+ error: (err) => {
169
+ error.set(err);
170
+ loading.set(false);
171
+ },
172
+ });
173
+ };
174
+ if (service)
175
+ fetch();
176
+ else
177
+ loading.set(false);
178
+ return { loading, data, error, context, refetch: fetch };
179
+ }
180
+
181
+ // Catálogo central de erros e metadados de apresentação
182
+ // Catálogo padrão por código (aceita string genérica para cobrir códigos não presentes no enum)
183
+ const DEFAULT_CATALOG = {
184
+ [ErrorCode.FILE_TOO_LARGE]: {
185
+ title: 'Arquivo muito grande',
186
+ category: 'validacao',
187
+ phase: 'pre-upload',
188
+ severity: 'error',
189
+ userAction: 'Reduza o tamanho ou aumente o limite permitido.',
190
+ },
191
+ [ErrorCode.INVALID_FILE_TYPE]: {
192
+ title: 'Tipo de arquivo inválido',
193
+ category: 'validacao',
194
+ phase: 'pre-upload',
195
+ severity: 'error',
196
+ userAction: 'Ajuste a extensão/MIME conforme a política.',
197
+ },
198
+ [ErrorCode.FMT_UNSUPPORTED]: {
199
+ title: 'Formato não suportado',
200
+ category: 'validacao',
201
+ phase: 'analise',
202
+ severity: 'error',
203
+ },
204
+ [ErrorCode.FMT_MAGIC_MISMATCH]: {
205
+ title: 'Conteúdo incompatível',
206
+ category: 'validacao',
207
+ phase: 'analise',
208
+ severity: 'error',
209
+ },
210
+ [ErrorCode.FMT_CORRUPTED]: {
211
+ title: 'Arquivo corrompido',
212
+ category: 'validacao',
213
+ phase: 'analise',
214
+ severity: 'error',
215
+ },
216
+ [ErrorCode.SEC_DANGEROUS_TYPE]: {
217
+ title: 'Tipo perigoso bloqueado',
218
+ category: 'seguranca',
219
+ phase: 'pre-upload',
220
+ severity: 'error',
221
+ },
222
+ [ErrorCode.SEC_VIRUS_DETECTED]: {
223
+ title: 'Vírus detectado',
224
+ category: 'seguranca',
225
+ phase: 'analise',
226
+ severity: 'error',
227
+ },
228
+ [ErrorCode.SEC_MALICIOUS_CONTENT]: {
229
+ title: 'Malware detectado',
230
+ category: 'seguranca',
231
+ phase: 'analise',
232
+ severity: 'error',
233
+ },
234
+ [ErrorCode.SYS_STORAGE_ERROR]: {
235
+ title: 'Erro de armazenamento',
236
+ category: 'armazenamento',
237
+ phase: 'armazenamento',
238
+ severity: 'error',
239
+ retriable: true,
240
+ },
241
+ [ErrorCode.RATE_LIMIT_EXCEEDED]: {
242
+ title: 'Limite de taxa excedido',
243
+ category: 'limites',
244
+ phase: 'operacional',
245
+ severity: 'warn',
246
+ retriable: true,
247
+ },
248
+ [ErrorCode.SYS_RATE_LIMIT]: {
249
+ title: 'Limite de requisições excedido',
250
+ category: 'limites',
251
+ phase: 'operacional',
252
+ severity: 'warn',
253
+ retriable: true,
254
+ },
255
+ [ErrorCode.QUOTA_EXCEEDED]: {
256
+ title: 'Cota excedida',
257
+ category: 'limites',
258
+ phase: 'operacional',
259
+ severity: 'warn',
260
+ },
261
+ [ErrorCode.INTERNAL_ERROR]: {
262
+ title: 'Erro interno',
263
+ category: 'operacional',
264
+ phase: 'operacional',
265
+ severity: 'error',
266
+ retriable: true,
267
+ },
268
+ [ErrorCode.UNAUTHORIZED]: {
269
+ title: 'Não autorizado',
270
+ category: 'operacional',
271
+ phase: 'operacional',
272
+ severity: 'error',
273
+ },
274
+ [ErrorCode.NOT_FOUND]: {
275
+ title: 'Não encontrado',
276
+ category: 'operacional',
277
+ phase: 'operacional',
278
+ severity: 'warn',
279
+ },
280
+ // Alias de mensagens de config (strings do backend fora do enum)
281
+ INVALID_TYPE: { title: 'Tipo de arquivo inválido', category: 'validacao', phase: 'pre-upload', severity: 'error' },
282
+ UNSUPPORTED_FILE_TYPE: { title: 'Tipo não suportado', category: 'validacao', phase: 'analise', severity: 'error' },
283
+ MAGIC_NUMBER_MISMATCH: { title: 'Assinatura incompatível', category: 'validacao', phase: 'analise', severity: 'error' },
284
+ CORRUPTED_FILE: { title: 'Arquivo corrompido', category: 'validacao', phase: 'analise', severity: 'error' },
285
+ DANGEROUS_FILE_TYPE: { title: 'Tipo perigoso bloqueado', category: 'seguranca', phase: 'pre-upload', severity: 'error' },
286
+ DANGEROUS_EXECUTABLE: { title: 'Executável bloqueado', category: 'seguranca', phase: 'analise', severity: 'error' },
287
+ MALWARE_DETECTED: { title: 'Malware detectado', category: 'seguranca', phase: 'analise', severity: 'error' },
288
+ VIRUS_DETECTED: { title: 'Vírus detectado', category: 'seguranca', phase: 'analise', severity: 'error' },
289
+ FILE_STORE_ERROR: { title: 'Erro ao armazenar arquivo', category: 'armazenamento', phase: 'armazenamento', severity: 'error', retriable: true },
290
+ FILE_STORAGE_FAILED: { title: 'Falha ao armazenar arquivo', category: 'armazenamento', phase: 'armazenamento', severity: 'error', retriable: true },
291
+ VIRUS_SCAN_ERROR: { title: 'Erro no antivírus', category: 'seguranca', phase: 'analise', severity: 'warn', retriable: true },
292
+ VIRUS_SCAN_FAILED: { title: 'Antivírus falhou', category: 'seguranca', phase: 'analise', severity: 'warn', retriable: true },
293
+ VIRUS_SCAN_UNAVAILABLE: { title: 'Antivírus indisponível', category: 'seguranca', phase: 'analise', severity: 'warn', retriable: true },
294
+ IO_ERROR: { title: 'Erro de entrada/saída', category: 'rede', phase: 'operacional', severity: 'error', retriable: true },
295
+ UPLOAD_TIMEOUT: { title: 'Tempo esgotado no upload', category: 'operacional', phase: 'operacional', severity: 'warn', retriable: true },
296
+ BULK_UPLOAD_TIMEOUT: { title: 'Tempo esgotado no upload em lote', category: 'operacional', phase: 'lote', severity: 'warn', retriable: true },
297
+ BULK_UPLOAD_CANCELLED: { title: 'Upload em lote cancelado', category: 'operacional', phase: 'lote', severity: 'warn' },
298
+ MIME_TYPE_MISMATCH: { title: 'MIME não corresponde', category: 'validacao', phase: 'analise', severity: 'error' },
299
+ FILE_STRUCTURE_ERROR: { title: 'Estrutura inválida', category: 'validacao', phase: 'analise', severity: 'error' },
300
+ STRUCTURE_VALIDATION_FAILED: { title: 'Validação de estrutura falhou', category: 'validacao', phase: 'analise', severity: 'error' },
301
+ EMPTY_FILE: { title: 'Arquivo vazio', category: 'validacao', phase: 'pre-upload', severity: 'error' },
302
+ INVALID_JSON_OPTIONS: { title: 'JSON de opções inválido', category: 'config', phase: 'operacional', severity: 'error' },
303
+ OPCOES_JSON_INVALIDAS: { title: 'JSON de opções inválido', category: 'config', phase: 'operacional', severity: 'error', userAction: 'Corrija a sintaxe do JSON enviado no campo options.' },
304
+ EMPTY_FILENAME: { title: 'Nome do arquivo obrigatório', category: 'validacao', phase: 'pre-upload', severity: 'error' },
305
+ FILE_ANALYSIS_ERROR: { title: 'Erro na análise do arquivo', category: 'operacional', phase: 'analise', severity: 'error', retriable: true },
306
+ CONFIGURATION_ERROR: { title: 'Erro de configuração', category: 'config', phase: 'operacional', severity: 'error' },
307
+ SECURITY_VIOLATION: { title: 'Violação de segurança', category: 'seguranca', phase: 'pre-upload', severity: 'error' },
308
+ CONFLICT_POLICY_ERROR: { title: 'Erro de política de conflito', category: 'conflito', phase: 'operacional', severity: 'error' },
309
+ CONFLICT_POLICY_SKIP: { title: 'Arquivo ignorado por política', category: 'conflito', phase: 'operacional', severity: 'info' },
310
+ FILE_EXISTS: { title: 'Arquivo já existe', category: 'conflito', phase: 'operacional', severity: 'warn' },
311
+ PATH_TRAVERSAL: { title: 'Path traversal detectado', category: 'seguranca', phase: 'pre-upload', severity: 'error' },
312
+ INSUFFICIENT_STORAGE: { title: 'Sem espaço em disco', category: 'armazenamento', phase: 'armazenamento', severity: 'error' },
313
+ SIGNATURE_MISMATCH: { title: 'Assinatura não corresponde', category: 'validacao', phase: 'analise', severity: 'error' },
314
+ SUSPICIOUS_STRUCTURE: { title: 'Estrutura suspeita', category: 'seguranca', phase: 'analise', severity: 'warn' },
315
+ INVALID_FILE_SIZE_CONFIG: { title: 'Configuração de tamanho inválida', category: 'config', phase: 'operacional', severity: 'error' },
316
+ USER_CANCELLED: { title: 'Upload cancelado pelo usuário', category: 'operacional', phase: 'operacional', severity: 'info' },
317
+ SYSTEM_POLICY_BLOCKED: { title: 'Bloqueado por política do sistema', category: 'operacional', phase: 'operacional', severity: 'error' },
318
+ SUSPICIOUS_EXTENSION_BLOCKED: { title: 'Extensão suspeita bloqueada', category: 'seguranca', phase: 'pre-upload', severity: 'warn' },
319
+ DANGEROUS_SCRIPT: { title: 'Script perigoso detectado', category: 'seguranca', phase: 'analise', severity: 'error' },
320
+ BATCH_SIZE_EXCEEDED: { title: 'Quantidade por lote excedida', category: 'validacao', phase: 'pre-upload', severity: 'error' },
321
+ EMBEDDED_EXECUTABLE: { title: 'Executável incorporado', category: 'seguranca', phase: 'analise', severity: 'warn' },
322
+ INVALID_PATH: { title: 'Caminho inválido', category: 'validacao', phase: 'pre-upload', severity: 'error' },
323
+ FAILED_TO_ANALYZE: { title: 'Falha ao analisar', category: 'operacional', phase: 'analise', severity: 'error', retriable: true },
324
+ ZIP_BOMB_DETECTED: { title: 'Possível zip bomb', category: 'seguranca', phase: 'analise', severity: 'error' },
325
+ UNKNOWN_ERROR: { title: 'Erro desconhecido', category: 'operacional', phase: 'operacional', severity: 'error', retriable: true },
326
+ // Aliases PT-BR adicionais para os códigos específicos do backend
327
+ ARQUIVO_VAZIO: { title: 'Arquivo vazio', category: 'validacao', phase: 'pre-upload', severity: 'error' },
328
+ ASSINATURA_INVALIDA: { title: 'Assinatura não corresponde', category: 'validacao', phase: 'analise', severity: 'error' },
329
+ ARQUIVO_EXECUTAVEL_PERIGOSO: { title: 'Executável bloqueado', category: 'seguranca', phase: 'analise', severity: 'error' },
330
+ SCRIPT_PERIGOSO: { title: 'Script perigoso detectado', category: 'seguranca', phase: 'analise', severity: 'error' },
331
+ ARQUIVO_CORROMPIDO: { title: 'Arquivo corrompido', category: 'validacao', phase: 'analise', severity: 'error' },
332
+ ESTRUTURA_SUSPEITA: { title: 'Estrutura suspeita', category: 'seguranca', phase: 'analise', severity: 'warn' },
333
+ MAGIC_NUMBER_INCOMPATIVEL: { title: 'Assinatura incompatível', category: 'validacao', phase: 'analise', severity: 'error' },
334
+ TIPO_ARQUIVO_PERIGOSO: { title: 'Tipo perigoso bloqueado', category: 'seguranca', phase: 'pre-upload', severity: 'error' },
335
+ SCANNER_VIRUS_INDISPONIVEL: { title: 'Antivírus indisponível', category: 'seguranca', phase: 'analise', severity: 'warn', retriable: true },
336
+ EXTENSAO_SUSPEITA_BLOQUEADA: { title: 'Extensão suspeita bloqueada', category: 'seguranca', phase: 'pre-upload', severity: 'warn' },
337
+ TIPO_NAO_SUPORTADO: { title: 'Tipo não suportado', category: 'validacao', phase: 'analise', severity: 'error' },
338
+ NOME_ARQUIVO_INVALIDO: { title: 'Nome de arquivo inválido', category: 'validacao', phase: 'pre-upload', severity: 'error' },
339
+ TEMPO_ESGOTADO_UPLOAD: { title: 'Tempo esgotado no upload', category: 'operacional', phase: 'operacional', severity: 'warn', retriable: true },
340
+ TEMPO_ESGOTADO_UPLOAD_LOTE: { title: 'Tempo esgotado no upload em lote', category: 'operacional', phase: 'lote', severity: 'warn', retriable: true },
341
+ ERRO_IO: { title: 'Erro de entrada/saída', category: 'rede', phase: 'operacional', severity: 'error', retriable: true },
342
+ ESPAÇO_INSUFICIENTE: { title: 'Sem espaço em disco', category: 'armazenamento', phase: 'armazenamento', severity: 'error' },
343
+ UPLOAD_LOTE_CANCELADO: { title: 'Upload em lote cancelado', category: 'operacional', phase: 'lote', severity: 'warn' },
344
+ UPLOAD_CANCELADO_USUARIO: { title: 'Upload cancelado pelo usuário', category: 'operacional', phase: 'operacional', severity: 'info' },
345
+ TAMANHO_LOTE_EXCEDIDO: { title: 'Quantidade por lote excedida', category: 'validacao', phase: 'pre-upload', severity: 'error' },
346
+ TIPO_MIME_INCOMPATIVEL: { title: 'MIME não corresponde', category: 'validacao', phase: 'analise', severity: 'error' },
347
+ VIOLACAO_SEGURANCA: { title: 'Violação de segurança', category: 'seguranca', phase: 'pre-upload', severity: 'error' },
348
+ ERRO_DESCONHECIDO: { title: 'Erro interno do sistema', category: 'operacional', phase: 'operacional', severity: 'error' },
349
+ BLOQUEADO_POLITICA_SISTEMA: { title: 'Bloqueado por política do sistema', category: 'operacional', phase: 'operacional', severity: 'error' },
350
+ FALHA_ESCANEAMENTO_VIRUS: { title: 'Antivírus falhou', category: 'seguranca', phase: 'analise', severity: 'warn', retriable: true },
351
+ FALHA_VALIDACAO_ESTRUTURA: { title: 'Validação de estrutura falhou', category: 'validacao', phase: 'analise', severity: 'error' },
352
+ CONFIGURACAO_TAMANHO_INVALIDA: { title: 'Configuração de tamanho inválida', category: 'config', phase: 'operacional', severity: 'error' },
353
+ FALHA_ARMAZENAMENTO: { title: 'Falha ao armazenar arquivo', category: 'armazenamento', phase: 'armazenamento', severity: 'error', retriable: true },
354
+ ERRO_ARMAZENAMENTO_ARQUIVO: { title: 'Erro ao armazenar arquivo', category: 'armazenamento', phase: 'armazenamento', severity: 'error', retriable: true },
355
+ NOME_ARQUIVO_VAZIO: { title: 'Nome do arquivo obrigatório', category: 'validacao', phase: 'pre-upload', severity: 'error' },
356
+ ERRO_ANALISE_ARQUIVO: { title: 'Erro na análise do arquivo', category: 'operacional', phase: 'analise', severity: 'error', retriable: true },
357
+ ERRO_ESTRUTURA_ARQUIVO: { title: 'Erro na estrutura do arquivo', category: 'validacao', phase: 'analise', severity: 'error' },
358
+ CAMINHO_INVALIDO: { title: 'Caminho inválido', category: 'validacao', phase: 'pre-upload', severity: 'error' },
359
+ ERRO_CONFIGURACAO: { title: 'Erro de configuração', category: 'config', phase: 'operacional', severity: 'error' },
360
+ ERRO_ESCANEAMENTO_VIRUS: { title: 'Erro no antivírus', category: 'seguranca', phase: 'analise', severity: 'warn', retriable: true },
361
+ FALHA_ANALISE_ARQUIVO: { title: 'Falha ao analisar', category: 'operacional', phase: 'analise', severity: 'error', retriable: true },
362
+ ERRO_POLITICA_CONFLITO: { title: 'Erro de política de conflito', category: 'conflito', phase: 'operacional', severity: 'error' },
363
+ POLITICA_CONFLITO_IGNORAR: { title: 'Arquivo ignorado por política', category: 'conflito', phase: 'operacional', severity: 'info' },
364
+ };
365
+ function getErrorMeta(code) {
366
+ return DEFAULT_CATALOG[code] ?? { title: code };
367
+ }
368
+
369
+ class PraxisFilesUploadConfigEditor {
370
+ fb;
371
+ panelData;
372
+ snackBar;
373
+ destroyRef;
374
+ form;
375
+ // Integração com backend: baseUrl e sinais de configuração efetiva
376
+ baseUrl;
377
+ state;
378
+ serverLoading = () => (this.state ? this.state.loading() : false);
379
+ serverData = () => (this.state ? this.state.data() : undefined);
380
+ serverError = () => (this.state ? this.state.error() : undefined);
381
+ get uiGroup() {
382
+ return this.form.get('ui');
383
+ }
384
+ get dropzoneGroup() {
385
+ return this.form.get('ui').get('dropzone');
386
+ }
387
+ get listGroup() {
388
+ return this.form.get('ui').get('list');
389
+ }
390
+ get limitsGroup() {
391
+ return this.form.get('limits');
392
+ }
393
+ get optionsGroup() {
394
+ return this.form.get('options');
395
+ }
396
+ get quotasGroup() {
397
+ return this.form.get('quotas');
398
+ }
399
+ get rateLimitGroup() {
400
+ return this.form.get('rateLimit');
401
+ }
402
+ get bulkGroup() {
403
+ return this.form.get('bulk');
404
+ }
405
+ get messagesGroup() {
406
+ return this.form.get('messages');
407
+ }
408
+ get headersGroup() {
409
+ return this.form.get('headers');
410
+ }
411
+ get errorsGroup() {
412
+ return this.messagesGroup.get('errors');
413
+ }
414
+ errorCodes = Object.values(ErrorCode);
415
+ errorEntries = [];
416
+ isDirty$ = new BehaviorSubject(false);
417
+ isValid$;
418
+ isBusy$ = new BehaviorSubject(false);
419
+ jsonError = null;
420
+ constructor(fb, panelData, snackBar, destroyRef) {
421
+ this.fb = fb;
422
+ this.panelData = panelData;
423
+ this.snackBar = snackBar;
424
+ this.destroyRef = destroyRef;
425
+ this.form = this.fb.group({
426
+ strategy: ['direct'],
427
+ ui: this.fb.group({
428
+ showDropzone: [true],
429
+ showProgress: [true],
430
+ showConflictPolicySelector: [true],
431
+ manualUpload: [false],
432
+ dense: [false],
433
+ accept: [''],
434
+ showMetadataForm: [false],
435
+ // NOVO: grupos específicos
436
+ dropzone: this.fb.group({
437
+ expandOnDragProximity: [true],
438
+ proximityPx: [64],
439
+ expandMode: ['overlay'],
440
+ expandHeight: [200],
441
+ expandDebounceMs: [120],
442
+ }),
443
+ list: this.fb.group({
444
+ collapseAfter: [5],
445
+ detailsMode: ['auto'],
446
+ detailsMaxWidth: [480],
447
+ detailsShowTechnical: [false],
448
+ detailsFields: [''], // CSV na UI
449
+ detailsAnchor: ['item'],
450
+ }),
451
+ }),
452
+ limits: this.fb.group({
453
+ maxFileSizeBytes: [null],
454
+ maxFilesPerBulk: [null],
455
+ maxBulkSizeBytes: [null],
456
+ defaultConflictPolicy: ['RENAME'],
457
+ failFast: [false],
458
+ strictValidation: [true],
459
+ maxUploadSizeMb: [50],
460
+ }),
461
+ options: this.fb.group({
462
+ allowedExtensions: [''],
463
+ acceptMimeTypes: [''],
464
+ targetDirectory: [''],
465
+ enableVirusScanning: [false],
466
+ }),
467
+ bulk: this.fb.group({
468
+ parallelUploads: [1],
469
+ retryCount: [0],
470
+ retryBackoffMs: [0],
471
+ }),
472
+ quotas: this.fb.group({
473
+ showQuotaWarnings: [false],
474
+ blockOnExceed: [false],
475
+ }),
476
+ rateLimit: this.fb.group({
477
+ autoRetryOn429: [false],
478
+ showBannerOn429: [true],
479
+ maxAutoRetry: [0],
480
+ baseBackoffMs: [0],
481
+ }),
482
+ headers: this.fb.group({
483
+ tenantHeader: ['X-Tenant-Id'],
484
+ userHeader: ['X-User-Id'],
485
+ // novos campos: valores para consulta do servidor
486
+ tenantValue: [''],
487
+ userValue: [''],
488
+ }),
489
+ messages: this.fb.group({
490
+ successSingle: [''],
491
+ successBulk: [''],
492
+ errors: this.fb.group({}),
493
+ }),
494
+ });
495
+ // Inicializa labels mais didáticos para os códigos de erro
496
+ const FRIENDLY = {
497
+ INVALID_FILE_TYPE: 'Tipo de arquivo inválido',
498
+ FILE_TOO_LARGE: 'Arquivo muito grande',
499
+ NOT_FOUND: 'Arquivo não encontrado',
500
+ UNAUTHORIZED: 'Acesso não autorizado',
501
+ RATE_LIMIT_EXCEEDED: 'Limite de requisições excedido',
502
+ INTERNAL_ERROR: 'Erro interno do servidor',
503
+ QUOTA_EXCEEDED: 'Cota de upload excedida',
504
+ SEC_VIRUS_DETECTED: 'Vírus detectado no arquivo',
505
+ SEC_MALICIOUS_CONTENT: 'Conteúdo malicioso detectado',
506
+ SEC_DANGEROUS_TYPE: 'Tipo de arquivo perigoso',
507
+ FMT_MAGIC_MISMATCH: 'Conteúdo do arquivo incompatível',
508
+ FMT_CORRUPTED: 'Arquivo corrompido',
509
+ FMT_UNSUPPORTED: 'Formato não suportado',
510
+ SYS_STORAGE_ERROR: 'Erro no armazenamento',
511
+ SYS_SERVICE_DOWN: 'Serviço indisponível',
512
+ SYS_RATE_LIMIT: 'Limite de requisições (sistema) excedido',
513
+ };
514
+ this.errorEntries = this.errorCodes.map((code) => ({
515
+ code,
516
+ label: FRIENDLY[code] ?? String(code),
517
+ }));
518
+ const errorsGroup = this.errorsGroup;
519
+ this.errorCodes.forEach((code) => {
520
+ errorsGroup.addControl(code, this.fb.control(''));
521
+ });
522
+ this.isValid$ = this.form.statusChanges.pipe(map$1((s) => s === 'VALID'), startWith(this.form.valid));
523
+ // Definir baseUrl e inicializar hook em contexto de injeção
524
+ this.baseUrl = this.panelData?.baseUrl ?? this.panelData?.__baseUrl;
525
+ this.state = useEffectiveUploadConfig(this.baseUrl ?? '/api/files', () => this.getHeadersForFetch());
526
+ // Observa mudanças na configuração efetiva do servidor (em contexto de injeção)
527
+ toObservable(this.state.data)
528
+ .pipe(takeUntilDestroyed(this.destroyRef))
529
+ .subscribe((cfg) => this.applyServerConfig(cfg));
530
+ }
531
+ ngOnInit() {
532
+ // baseUrl opcional vinda do componente pai (Settings Panel inputs)
533
+ // (já definida no construtor)
534
+ if (this.panelData) {
535
+ const patch = { ...this.panelData };
536
+ if (patch.ui?.accept) {
537
+ patch.ui = { ...patch.ui, accept: patch.ui.accept.join(',') };
538
+ }
539
+ // NOVO: normalizar lista de detalhes para CSV
540
+ if (patch.ui?.list?.detailsFields) {
541
+ patch.ui = {
542
+ ...patch.ui,
543
+ list: {
544
+ ...patch.ui.list,
545
+ detailsFields: patch.ui.list.detailsFields.join(','),
546
+ },
547
+ };
548
+ }
549
+ if (patch.options?.allowedExtensions) {
550
+ patch.options = {
551
+ ...patch.options,
552
+ allowedExtensions: patch.options.allowedExtensions.join(','),
553
+ };
554
+ }
555
+ if (patch.options?.acceptMimeTypes) {
556
+ patch.options = {
557
+ ...patch.options,
558
+ acceptMimeTypes: patch.options.acceptMimeTypes.join(','),
559
+ };
560
+ }
561
+ this.form.patchValue(patch);
562
+ }
563
+ // Sempre que cabeçalhos mudarem, atualiza contexto da consulta (e refaz fetch)
564
+ this.headersGroup.valueChanges.subscribe(() => {
565
+ this.state.refetch();
566
+ });
567
+ this.form.valueChanges.subscribe(() => this.isDirty$.next(true));
568
+ }
569
+ applyServerConfig(cfg) {
570
+ if (!cfg)
571
+ return;
572
+ // Options mapeadas para "Validações" e "Opções"
573
+ const options = cfg.options ?? {};
574
+ const bulk = cfg.bulk ?? {};
575
+ // limits
576
+ this.limitsGroup.patchValue({
577
+ defaultConflictPolicy: options.nameConflictPolicy ?? null,
578
+ strictValidation: options.strictValidation ?? null,
579
+ maxUploadSizeMb: options.maxUploadSizeMb ?? null,
580
+ // failFast padrão do servidor pode influenciar a UI de validação em lote
581
+ failFast: bulk.failFastModeDefault ?? false,
582
+ // preencher máx. arquivos por lote a partir do backend
583
+ maxFilesPerBulk: typeof bulk.maxFilesPerBatch === 'number'
584
+ ? bulk.maxFilesPerBatch
585
+ : null,
586
+ }, { emitEvent: false });
587
+ // options (normalizar arrays em string CSV)
588
+ const allowed = Array.isArray(options.allowedExtensions)
589
+ ? options.allowedExtensions.join(',')
590
+ : '';
591
+ const mimes = Array.isArray(options.acceptMimeTypes)
592
+ ? options.acceptMimeTypes.join(',')
593
+ : '';
594
+ this.optionsGroup.patchValue({
595
+ allowedExtensions: allowed,
596
+ acceptMimeTypes: mimes,
597
+ targetDirectory: options.targetDirectory ?? '',
598
+ enableVirusScanning: !!options.enableVirusScanning,
599
+ }, { emitEvent: false });
600
+ // bulk (UI)
601
+ this.bulkGroup.patchValue({
602
+ parallelUploads: typeof bulk.maxConcurrentUploads === 'number'
603
+ ? bulk.maxConcurrentUploads
604
+ : 1,
605
+ }, { emitEvent: false });
606
+ // rate limit (somente leitura na UI)
607
+ this.rateLimitGroup.patchValue({
608
+ // Mantemos flags de UI; os números vêm do servidor e são mostrados no resumo
609
+ }, { emitEvent: false });
610
+ // quotas (somente preferências de UI)
611
+ this.quotasGroup.patchValue({}, { emitEvent: false });
612
+ // Mensagens de erro do servidor
613
+ const serverMessages = cfg.messages ?? {};
614
+ const errorsGroup = this.errorsGroup;
615
+ const alias = {
616
+ INVALID_TYPE: ErrorCode.INVALID_FILE_TYPE,
617
+ UNSUPPORTED_FILE_TYPE: ErrorCode.FMT_UNSUPPORTED,
618
+ MAGIC_NUMBER_MISMATCH: ErrorCode.FMT_MAGIC_MISMATCH,
619
+ CORRUPTED_FILE: ErrorCode.FMT_CORRUPTED,
620
+ FILE_TOO_LARGE: ErrorCode.FILE_TOO_LARGE,
621
+ RATE_LIMIT_EXCEEDED: ErrorCode.RATE_LIMIT_EXCEEDED,
622
+ QUOTA_EXCEEDED: ErrorCode.QUOTA_EXCEEDED,
623
+ INTERNAL_ERROR: ErrorCode.INTERNAL_ERROR,
624
+ UNKNOWN_ERROR: ErrorCode.INTERNAL_ERROR,
625
+ MALWARE_DETECTED: ErrorCode.SEC_MALICIOUS_CONTENT,
626
+ VIRUS_DETECTED: ErrorCode.SEC_VIRUS_DETECTED,
627
+ DANGEROUS_FILE_TYPE: ErrorCode.SEC_DANGEROUS_TYPE,
628
+ DANGEROUS_EXECUTABLE: ErrorCode.SEC_DANGEROUS_TYPE,
629
+ FILE_STORE_ERROR: ErrorCode.SYS_STORAGE_ERROR,
630
+ SYS_STORAGE_ERROR: ErrorCode.SYS_STORAGE_ERROR,
631
+ };
632
+ Object.keys(serverMessages).forEach((rawCode) => {
633
+ const mapped = alias[rawCode] ?? ErrorCode[rawCode];
634
+ const ctrlName = mapped ? String(mapped) : rawCode;
635
+ if (!errorsGroup.get(ctrlName)) {
636
+ errorsGroup.addControl(ctrlName, this.fb.control(''));
637
+ }
638
+ if (!this.errorEntries.some((e) => e.code === ctrlName)) {
639
+ const meta = getErrorMeta(rawCode);
640
+ const label = meta.title || ctrlName;
641
+ this.errorEntries.push({ code: ctrlName, label });
642
+ }
643
+ errorsGroup
644
+ .get(ctrlName)
645
+ ?.patchValue(serverMessages[rawCode], { emitEvent: false });
646
+ });
647
+ }
648
+ getSettingsValue() {
649
+ const value = this.form.value;
650
+ // accept (UI)
651
+ const accept = this.uiGroup.get('accept')?.value;
652
+ if (accept !== undefined) {
653
+ value.ui = value.ui ?? {};
654
+ value.ui.accept = accept
655
+ .split(',')
656
+ .map((s) => s.trim())
657
+ .filter((s) => !!s);
658
+ }
659
+ // NOVO: detailsFields (UI)
660
+ const df = this.uiGroup.get('list')?.get('detailsFields')
661
+ ?.value;
662
+ if (df !== undefined) {
663
+ value.ui = value.ui ?? {};
664
+ value.ui.list = value.ui.list ?? {};
665
+ value.ui.list.detailsFields = df
666
+ .split(',')
667
+ .map((s) => s.trim())
668
+ .filter((s) => !!s);
669
+ }
670
+ // options.allowedExtensions
671
+ const allowedExt = this.optionsGroup.get('allowedExtensions')
672
+ ?.value;
673
+ if (allowedExt !== undefined) {
674
+ value.options = value.options ?? {};
675
+ value.options.allowedExtensions = allowedExt
676
+ .split(',')
677
+ .map((s) => s.trim())
678
+ .filter((s) => !!s);
679
+ }
680
+ // options.acceptMimeTypes
681
+ const mime = this.optionsGroup.get('acceptMimeTypes')?.value;
682
+ if (mime !== undefined) {
683
+ value.options = value.options ?? {};
684
+ value.options.acceptMimeTypes = mime
685
+ .split(',')
686
+ .map((s) => s.trim())
687
+ .filter((s) => !!s);
688
+ }
689
+ return value;
690
+ }
691
+ copyServerConfig() {
692
+ const data = this.serverData();
693
+ if (!data) {
694
+ return;
695
+ }
696
+ const json = JSON.stringify(data, null, 2);
697
+ if (navigator?.clipboard?.writeText) {
698
+ navigator.clipboard.writeText(json);
699
+ this.snackBar.open('Configuração copiada', undefined, {
700
+ duration: 2000,
701
+ });
702
+ }
703
+ }
704
+ onJsonChange(json) {
705
+ try {
706
+ const parsed = JSON.parse(json);
707
+ this.form.patchValue(parsed);
708
+ this.jsonError = null;
709
+ }
710
+ catch {
711
+ this.jsonError = 'JSON inválido: verifique a sintaxe.';
712
+ this.snackBar.open('JSON inválido', undefined, { duration: 2000 });
713
+ }
714
+ }
715
+ getHeadersForFetch() {
716
+ const h = this.headersGroup.value;
717
+ const headers = {};
718
+ if (h?.tenantHeader && h?.tenantValue)
719
+ headers[h.tenantHeader] = h.tenantValue;
720
+ if (h?.userHeader && h?.userValue)
721
+ headers[h.userHeader] = h.userValue;
722
+ return headers;
723
+ }
724
+ refetchServerConfig() {
725
+ this.state.refetch();
726
+ }
727
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisFilesUploadConfigEditor, deps: [{ token: i1$1.FormBuilder }, { token: SETTINGS_PANEL_DATA }, { token: i2.MatSnackBar }, { token: i0.DestroyRef }], target: i0.ɵɵFactoryTarget.Component });
728
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: PraxisFilesUploadConfigEditor, isStandalone: true, selector: "praxis-files-upload-config-editor", ngImport: i0, template: `
729
+ <mat-tab-group>
730
+ <mat-tab label="Comportamento">
731
+ <form [formGroup]="form">
732
+ <mat-form-field appearance="fill">
733
+ <mat-label>Estratégia de envio <span class="opt-tag">opcional</span></mat-label>
734
+ <mat-select formControlName="strategy">
735
+ <mat-option value="direct">Direto (HTTP padrão)</mat-option>
736
+ <mat-option value="presign">URL pré-assinada (S3/GCS)</mat-option>
737
+ <mat-option value="auto"
738
+ >Automático (tenta pré-assinada e volta ao direto)</mat-option
739
+ >
740
+ </mat-select>
741
+ <mat-hint>Como os arquivos serão enviados ao servidor.</mat-hint>
742
+ </mat-form-field>
743
+ </form>
744
+ <form [formGroup]="bulkGroup">
745
+ <h4 class="section-subtitle">
746
+ <mat-icon aria-hidden="true">build</mat-icon>
747
+ Opções configuráveis — Lote
748
+ </h4>
749
+ <mat-form-field appearance="fill">
750
+ <mat-label>Uploads paralelos <span class="opt-tag">opcional</span></mat-label>
751
+ <input matInput type="number" formControlName="parallelUploads" />
752
+ <mat-hint>Quantos arquivos enviar ao mesmo tempo.</mat-hint>
753
+ </mat-form-field>
754
+ <mat-form-field appearance="fill">
755
+ <mat-label>Número de tentativas <span class="opt-tag">opcional</span></mat-label>
756
+ <input matInput type="number" formControlName="retryCount" />
757
+ <mat-hint>Tentativas automáticas em caso de falha.</mat-hint>
758
+ </mat-form-field>
759
+ <mat-form-field appearance="fill">
760
+ <mat-label>Intervalo entre tentativas (ms) <span class="opt-tag">opcional</span></mat-label>
761
+ <input matInput type="number" formControlName="retryBackoffMs" />
762
+ <mat-hint>Tempo de espera entre tentativas.</mat-hint>
763
+ </mat-form-field>
764
+ </form>
765
+ </mat-tab>
766
+ <mat-tab label="Interface">
767
+ <form [formGroup]="uiGroup">
768
+ <h4 class="section-subtitle">
769
+ <mat-icon aria-hidden="true">edit</mat-icon>
770
+ Opções configuráveis — Interface
771
+ </h4>
772
+ <mat-checkbox formControlName="showDropzone"
773
+ >Exibir área de soltar</mat-checkbox
774
+ >
775
+ <mat-checkbox formControlName="showProgress"
776
+ >Exibir barra de progresso</mat-checkbox
777
+ >
778
+ <mat-checkbox formControlName="showConflictPolicySelector"
779
+ >Permitir escolher a política de conflito</mat-checkbox
780
+ >
781
+ <mat-checkbox formControlName="manualUpload"
782
+ >Exigir clique em “Enviar” (modo manual)</mat-checkbox
783
+ >
784
+ <mat-checkbox formControlName="dense">Layout compacto</mat-checkbox>
785
+ <mat-form-field appearance="fill">
786
+ <mat-label>Tipos permitidos (accept) <span class="opt-tag">opcional</span></mat-label>
787
+ <input
788
+ matInput
789
+ formControlName="accept"
790
+ placeholder="ex.: pdf,jpg,png"
791
+ />
792
+ <mat-hint>Lista separada por vírgula (opcional).</mat-hint>
793
+ </mat-form-field>
794
+ <mat-checkbox formControlName="showMetadataForm"
795
+ >Exibir formulário de metadados (JSON)</mat-checkbox
796
+ >
797
+
798
+ <!-- NOVO: Grupo Dropzone -->
799
+ <fieldset [formGroup]="dropzoneGroup" class="subgroup">
800
+ <legend>
801
+ <mat-icon aria-hidden="true">download</mat-icon>
802
+ Dropzone (expansão por proximidade)
803
+ </legend>
804
+ <mat-checkbox formControlName="expandOnDragProximity">
805
+ Expandir ao aproximar arquivo durante arraste
806
+ </mat-checkbox>
807
+ <mat-form-field appearance="fill">
808
+ <mat-label>Raio de proximidade (px) <span class="opt-tag">opcional</span></mat-label>
809
+ <input matInput type="number" formControlName="proximityPx" />
810
+ </mat-form-field>
811
+ <mat-form-field appearance="fill">
812
+ <mat-label>Modo de expansão</mat-label>
813
+ <mat-select formControlName="expandMode">
814
+ <mat-option value="overlay">Overlay (recomendado)</mat-option>
815
+ <mat-option value="inline">Inline</mat-option>
816
+ </mat-select>
817
+ </mat-form-field>
818
+ <mat-form-field appearance="fill">
819
+ <mat-label>Altura do overlay (px) <span class="opt-tag">opcional</span></mat-label>
820
+ <input matInput type="number" formControlName="expandHeight" />
821
+ </mat-form-field>
822
+ <mat-form-field appearance="fill">
823
+ <mat-label>Debounce de arraste (ms) <span class="opt-tag">opcional</span></mat-label>
824
+ <input matInput type="number" formControlName="expandDebounceMs" />
825
+ </mat-form-field>
826
+ </fieldset>
827
+
828
+ <!-- NOVO: Grupo Lista/Detalhes -->
829
+ <fieldset [formGroup]="listGroup" class="subgroup">
830
+ <legend>
831
+ <mat-icon aria-hidden="true">view_list</mat-icon>
832
+ Lista e detalhes
833
+ </legend>
834
+ <mat-form-field appearance="fill">
835
+ <mat-label>Colapsar após (itens) <span class="opt-tag">opcional</span></mat-label>
836
+ <input matInput type="number" formControlName="collapseAfter" />
837
+ </mat-form-field>
838
+ <mat-form-field appearance="fill">
839
+ <mat-label>Modo de detalhes</mat-label>
840
+ <mat-select formControlName="detailsMode">
841
+ <mat-option value="auto">Automático</mat-option>
842
+ <mat-option value="card">Card (overlay)</mat-option>
843
+ <mat-option value="sidesheet">Side-sheet</mat-option>
844
+ </mat-select>
845
+ </mat-form-field>
846
+ <mat-form-field appearance="fill">
847
+ <mat-label>Largura máxima do card (px) <span class="opt-tag">opcional</span></mat-label>
848
+ <input matInput type="number" formControlName="detailsMaxWidth" />
849
+ </mat-form-field>
850
+ <mat-checkbox formControlName="detailsShowTechnical">
851
+ Mostrar detalhes técnicos por padrão
852
+ </mat-checkbox>
853
+ <mat-form-field appearance="fill">
854
+ <mat-label>Campos de metadados (whitelist) <span class="opt-tag">opcional</span></mat-label>
855
+ <input matInput formControlName="detailsFields" placeholder="ex.: id,fileName,contentType" />
856
+ <mat-hint>Lista separada por vírgula; vazio = todos.</mat-hint>
857
+ </mat-form-field>
858
+ <mat-form-field appearance="fill">
859
+ <mat-label>Âncora do overlay</mat-label>
860
+ <mat-select formControlName="detailsAnchor">
861
+ <mat-option value="item">Item</mat-option>
862
+ <mat-option value="field">Campo</mat-option>
863
+ </mat-select>
864
+ </mat-form-field>
865
+ </fieldset>
866
+ </form>
867
+ </mat-tab>
868
+ <mat-tab label="Validações">
869
+ <form [formGroup]="limitsGroup">
870
+ <h4 class="section-subtitle">
871
+ <mat-icon aria-hidden="true">build</mat-icon>
872
+ Opções configuráveis — Validações
873
+ </h4>
874
+ <mat-form-field appearance="fill">
875
+ <mat-label>Tamanho máximo do arquivo (bytes) <span class="opt-tag">opcional</span></mat-label>
876
+ <input matInput type="number" formControlName="maxFileSizeBytes" />
877
+ <mat-hint>Limite de validação no cliente (opcional).</mat-hint>
878
+ </mat-form-field>
879
+ <mat-form-field appearance="fill">
880
+ <mat-label>Máx. arquivos por lote <span class="opt-tag">opcional</span></mat-label>
881
+ <input matInput type="number" formControlName="maxFilesPerBulk" />
882
+ </mat-form-field>
883
+ <mat-form-field appearance="fill">
884
+ <mat-label>Tamanho máximo do lote (bytes) <span class="opt-tag">opcional</span></mat-label>
885
+ <input matInput type="number" formControlName="maxBulkSizeBytes" />
886
+ </mat-form-field>
887
+ <mat-form-field appearance="fill">
888
+ <mat-label>Política de conflito (padrão) <span class="opt-tag">opcional</span></mat-label>
889
+ <mat-select formControlName="defaultConflictPolicy">
890
+ <mat-option value="RENAME">Renomear automaticamente</mat-option>
891
+ <mat-option value="MAKE_UNIQUE">Gerar nome único</mat-option>
892
+ <mat-option value="OVERWRITE">Sobrescrever arquivo existente</mat-option>
893
+ <mat-option value="SKIP">Pular se já existir</mat-option>
894
+ <mat-option value="ERROR">Falhar (erro)</mat-option>
895
+ </mat-select>
896
+ <mat-hint>O que fazer quando o nome do arquivo já existe.</mat-hint>
897
+ <div class="warn" *ngIf="limitsGroup.get('defaultConflictPolicy')?.value === 'OVERWRITE'">
898
+ <mat-icon color="warn" aria-hidden="true">warning</mat-icon>
899
+ Atenção: OVERWRITE pode sobrescrever arquivos existentes.
900
+ </div>
901
+ </mat-form-field>
902
+ <mat-checkbox formControlName="failFast"
903
+ >Parar no primeiro erro (fail-fast)</mat-checkbox
904
+ >
905
+ <mat-checkbox formControlName="strictValidation"
906
+ >Validação rigorosa (backend)</mat-checkbox
907
+ >
908
+ <mat-form-field appearance="fill">
909
+ <mat-label>Tamanho máx. por arquivo (MB) <span class="req-tag">mandatório</span></mat-label>
910
+ <input matInput type="number" formControlName="maxUploadSizeMb" required />
911
+ <mat-hint>Validado pelo backend (1–500 MB).</mat-hint>
912
+ </mat-form-field>
913
+ </form>
914
+ <form [formGroup]="optionsGroup">
915
+ <h4 class="section-subtitle">
916
+ <mat-icon aria-hidden="true">tune</mat-icon>
917
+ Opções configuráveis — Avançado
918
+ </h4>
919
+ <mat-form-field appearance="fill">
920
+ <mat-label>Extensões permitidas <span class="opt-tag">opcional</span></mat-label>
921
+ <input
922
+ matInput
923
+ formControlName="allowedExtensions"
924
+ placeholder="ex.: pdf,docx,xlsx"
925
+ />
926
+ <mat-hint>Lista separada por vírgula (opcional).</mat-hint>
927
+ </mat-form-field>
928
+ <mat-form-field appearance="fill">
929
+ <mat-label>MIME types aceitos <span class="opt-tag">opcional</span></mat-label>
930
+ <input
931
+ matInput
932
+ formControlName="acceptMimeTypes"
933
+ placeholder="ex.: application/pdf,image/png"
934
+ />
935
+ <mat-hint>Lista separada por vírgula (opcional).</mat-hint>
936
+ </mat-form-field>
937
+ <mat-form-field appearance="fill">
938
+ <mat-label>Diretório destino <span class="opt-tag">opcional</span></mat-label>
939
+ <input
940
+ matInput
941
+ formControlName="targetDirectory"
942
+ placeholder="ex.: documentos/notas"
943
+ />
944
+ </mat-form-field>
945
+ <mat-checkbox formControlName="enableVirusScanning">
946
+ Forçar antivírus (quando disponível)
947
+ </mat-checkbox>
948
+ <div class="warn" *ngIf="optionsGroup.get('enableVirusScanning')?.value === true">
949
+ <mat-icon color="warn" aria-hidden="true">warning</mat-icon>
950
+ Pode impactar desempenho e latência de upload.
951
+ </div>
952
+ </form>
953
+ <form [formGroup]="quotasGroup">
954
+ <h4 class="section-subtitle">
955
+ <mat-icon aria-hidden="true">edit</mat-icon>
956
+ Opções configuráveis — Quotas (UI)
957
+ </h4>
958
+ <mat-checkbox formControlName="showQuotaWarnings"
959
+ >Exibir avisos de cota</mat-checkbox
960
+ >
961
+ <mat-checkbox formControlName="blockOnExceed"
962
+ >Bloquear ao exceder cota</mat-checkbox
963
+ >
964
+ </form>
965
+ <form [formGroup]="rateLimitGroup">
966
+ <h4 class="section-subtitle">
967
+ <mat-icon aria-hidden="true">edit</mat-icon>
968
+ Opções configuráveis — Rate Limit (UI)
969
+ </h4>
970
+ <mat-checkbox formControlName="showBannerOn429"
971
+ >Exibir banner quando atingir o limite</mat-checkbox
972
+ >
973
+ <mat-checkbox formControlName="autoRetryOn429"
974
+ >Tentar novamente automaticamente</mat-checkbox
975
+ >
976
+ <mat-form-field appearance="fill">
977
+ <mat-label>Máximo de tentativas automáticas <span class="opt-tag">opcional</span></mat-label>
978
+ <input matInput type="number" formControlName="maxAutoRetry" />
979
+ </mat-form-field>
980
+ <mat-form-field appearance="fill">
981
+ <mat-label>Intervalo base entre tentativas (ms) <span class="opt-tag">opcional</span></mat-label>
982
+ <input matInput type="number" formControlName="baseBackoffMs" />
983
+ </mat-form-field>
984
+ </form>
985
+ </mat-tab>
986
+ <mat-tab label="Mensagens">
987
+ <form [formGroup]="messagesGroup">
988
+ <h4 class="section-subtitle">
989
+ <mat-icon aria-hidden="true">edit</mat-icon>
990
+ Opções configuráveis — Mensagens (UI)
991
+ </h4>
992
+ <mat-form-field appearance="fill">
993
+ <mat-label>Sucesso (individual) <span class="opt-tag">opcional</span></mat-label>
994
+ <input
995
+ matInput
996
+ formControlName="successSingle"
997
+ placeholder="ex.: Arquivo enviado com sucesso"
998
+ />
999
+ </mat-form-field>
1000
+ <mat-form-field appearance="fill">
1001
+ <mat-label>Sucesso (em lote) <span class="opt-tag">opcional</span></mat-label>
1002
+ <input
1003
+ matInput
1004
+ formControlName="successBulk"
1005
+ placeholder="ex.: Upload concluído"
1006
+ />
1007
+ </mat-form-field>
1008
+ <div [formGroup]="errorsGroup">
1009
+ <ng-container *ngFor="let e of errorEntries">
1010
+ <mat-form-field appearance="fill">
1011
+ <mat-label>{{ e.label }} <span class="opt-tag">opcional</span></mat-label>
1012
+ <input matInput [formControlName]="e.code" />
1013
+ <mat-hint class="code-hint">{{ e.code }}</mat-hint>
1014
+ </mat-form-field>
1015
+ </ng-container>
1016
+ </div>
1017
+ </form>
1018
+ </mat-tab>
1019
+ <mat-tab label="Cabeçalhos">
1020
+ <form [formGroup]="headersGroup">
1021
+ <h4 class="section-subtitle">
1022
+ <mat-icon aria-hidden="true">edit</mat-icon>
1023
+ Opções configuráveis — Cabeçalhos (consulta)
1024
+ </h4>
1025
+ <mat-form-field appearance="fill">
1026
+ <mat-label>Cabeçalho de tenant <span class="opt-tag">opcional</span></mat-label>
1027
+ <input
1028
+ matInput
1029
+ formControlName="tenantHeader"
1030
+ placeholder="X-Tenant-Id"
1031
+ />
1032
+ </mat-form-field>
1033
+ <mat-form-field appearance="fill">
1034
+ <mat-label>Valor do tenant <span class="opt-tag">opcional</span></mat-label>
1035
+ <input
1036
+ matInput
1037
+ formControlName="tenantValue"
1038
+ placeholder="ex.: demo-tenant"
1039
+ />
1040
+ </mat-form-field>
1041
+ <mat-form-field appearance="fill">
1042
+ <mat-label>Cabeçalho de usuário <span class="opt-tag">opcional</span></mat-label>
1043
+ <input
1044
+ matInput
1045
+ formControlName="userHeader"
1046
+ placeholder="X-User-Id"
1047
+ />
1048
+ </mat-form-field>
1049
+ <mat-form-field appearance="fill">
1050
+ <mat-label>Valor do usuário <span class="opt-tag">opcional</span></mat-label>
1051
+ <input matInput formControlName="userValue" placeholder="ex.: 42" />
1052
+ </mat-form-field>
1053
+ </form>
1054
+ </mat-tab>
1055
+ <mat-tab label="Servidor">
1056
+ <div class="server-tab">
1057
+ <div class="toolbar">
1058
+ <h4 class="section-subtitle ro">
1059
+ <mat-icon aria-hidden="true">info</mat-icon>
1060
+ Servidor (somente leitura)
1061
+ <span class="badge">read-only</span>
1062
+ </h4>
1063
+ <button type="button" (click)="refetchServerConfig()">
1064
+ Recarregar do servidor
1065
+ </button>
1066
+ <span class="hint" *ngIf="!baseUrl"
1067
+ >Defina a baseUrl no componente pai para consultar
1068
+ /api/files/config.</span
1069
+ >
1070
+ </div>
1071
+ <div *ngIf="serverLoading(); else serverLoaded">
1072
+ Carregando configuração do servidor…
1073
+ </div>
1074
+ <ng-template #serverLoaded>
1075
+ <div *ngIf="serverError(); else serverOk" class="error">
1076
+ Falha ao carregar: {{ serverError() | json }}
1077
+ </div>
1078
+ <ng-template #serverOk>
1079
+ <section *ngIf="serverData() as _">
1080
+ <h3>Resumo da configuração ativa</h3>
1081
+ <ul class="summary">
1082
+ <li>
1083
+ <strong>Max por arquivo (MB):</strong>
1084
+ {{ serverData()?.options?.maxUploadSizeMb }}
1085
+ </li>
1086
+ <li>
1087
+ <strong>Validação rigorosa:</strong>
1088
+ {{ serverData()?.options?.strictValidation }}
1089
+ </li>
1090
+ <li>
1091
+ <strong>Antivírus:</strong>
1092
+ {{ serverData()?.options?.enableVirusScanning }}
1093
+ </li>
1094
+ <li>
1095
+ <strong>Conflito de nome (padrão):</strong>
1096
+ {{ serverData()?.options?.nameConflictPolicy }}
1097
+ </li>
1098
+ <li>
1099
+ <strong>MIME aceitos:</strong>
1100
+ {{
1101
+ (serverData()?.options?.acceptMimeTypes || []).join(', ')
1102
+ }}
1103
+ </li>
1104
+ <li>
1105
+ <strong>Bulk - fail-fast padrão:</strong>
1106
+ {{ serverData()?.bulk?.failFastModeDefault }}
1107
+ </li>
1108
+ <li>
1109
+ <strong>Rate limit:</strong>
1110
+ {{ serverData()?.rateLimit?.enabled }} ({{
1111
+ serverData()?.rateLimit?.perMinute
1112
+ }}/min, {{ serverData()?.rateLimit?.perHour }}/h)
1113
+ </li>
1114
+ <li>
1115
+ <strong>Quotas:</strong> {{ serverData()?.quotas?.enabled }}
1116
+ </li>
1117
+ <li>
1118
+ <strong>Servidor:</strong> v{{
1119
+ serverData()?.metadata?.version
1120
+ }}
1121
+ • {{ serverData()?.metadata?.locale }}
1122
+ </li>
1123
+ </ul>
1124
+ <details>
1125
+ <summary>Ver JSON</summary>
1126
+ <button
1127
+ mat-icon-button
1128
+ aria-label="Copiar JSON"
1129
+ (click)="copyServerConfig()"
1130
+ type="button"
1131
+ title="Copiar JSON"
1132
+ >
1133
+ <mat-icon>content_copy</mat-icon>
1134
+ </button>
1135
+ <pre>{{ serverData() | json }}</pre>
1136
+ </details>
1137
+ <p class="note">
1138
+ As opções acima que podem ser alteradas via payload são:
1139
+ conflito de nome, validação rigorosa, tamanho máximo (MB),
1140
+ extensões/MIME aceitos, diretório destino, antivírus,
1141
+ metadados personalizados e fail-fast (no bulk).
1142
+ </p>
1143
+ </section>
1144
+ </ng-template>
1145
+ </ng-template>
1146
+ </div>
1147
+ </mat-tab>
1148
+ <mat-tab label="JSON">
1149
+ <textarea
1150
+ rows="10"
1151
+ [ngModel]="form.value | json"
1152
+ (ngModelChange)="onJsonChange($event)"
1153
+ ></textarea>
1154
+ <div class="error" *ngIf="jsonError">{{ jsonError }}</div>
1155
+ </mat-tab>
1156
+ </mat-tab-group>
1157
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i7.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i7.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: "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$1.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: i9.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: 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: i7.JsonPipe, name: "json" }] });
1158
+ }
1159
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisFilesUploadConfigEditor, decorators: [{
1160
+ type: Component,
1161
+ args: [{
1162
+ selector: 'praxis-files-upload-config-editor',
1163
+ standalone: true,
1164
+ imports: [
1165
+ CommonModule,
1166
+ ReactiveFormsModule,
1167
+ MatTabsModule,
1168
+ MatFormFieldModule,
1169
+ MatInputModule,
1170
+ MatCheckboxModule,
1171
+ MatSelectModule,
1172
+ MatButtonModule,
1173
+ MatIconModule,
1174
+ MatSnackBarModule,
1175
+ FormsModule,
1176
+ ],
1177
+ template: `
1178
+ <mat-tab-group>
1179
+ <mat-tab label="Comportamento">
1180
+ <form [formGroup]="form">
1181
+ <mat-form-field appearance="fill">
1182
+ <mat-label>Estratégia de envio <span class="opt-tag">opcional</span></mat-label>
1183
+ <mat-select formControlName="strategy">
1184
+ <mat-option value="direct">Direto (HTTP padrão)</mat-option>
1185
+ <mat-option value="presign">URL pré-assinada (S3/GCS)</mat-option>
1186
+ <mat-option value="auto"
1187
+ >Automático (tenta pré-assinada e volta ao direto)</mat-option
1188
+ >
1189
+ </mat-select>
1190
+ <mat-hint>Como os arquivos serão enviados ao servidor.</mat-hint>
1191
+ </mat-form-field>
1192
+ </form>
1193
+ <form [formGroup]="bulkGroup">
1194
+ <h4 class="section-subtitle">
1195
+ <mat-icon aria-hidden="true">build</mat-icon>
1196
+ Opções configuráveis — Lote
1197
+ </h4>
1198
+ <mat-form-field appearance="fill">
1199
+ <mat-label>Uploads paralelos <span class="opt-tag">opcional</span></mat-label>
1200
+ <input matInput type="number" formControlName="parallelUploads" />
1201
+ <mat-hint>Quantos arquivos enviar ao mesmo tempo.</mat-hint>
1202
+ </mat-form-field>
1203
+ <mat-form-field appearance="fill">
1204
+ <mat-label>Número de tentativas <span class="opt-tag">opcional</span></mat-label>
1205
+ <input matInput type="number" formControlName="retryCount" />
1206
+ <mat-hint>Tentativas automáticas em caso de falha.</mat-hint>
1207
+ </mat-form-field>
1208
+ <mat-form-field appearance="fill">
1209
+ <mat-label>Intervalo entre tentativas (ms) <span class="opt-tag">opcional</span></mat-label>
1210
+ <input matInput type="number" formControlName="retryBackoffMs" />
1211
+ <mat-hint>Tempo de espera entre tentativas.</mat-hint>
1212
+ </mat-form-field>
1213
+ </form>
1214
+ </mat-tab>
1215
+ <mat-tab label="Interface">
1216
+ <form [formGroup]="uiGroup">
1217
+ <h4 class="section-subtitle">
1218
+ <mat-icon aria-hidden="true">edit</mat-icon>
1219
+ Opções configuráveis — Interface
1220
+ </h4>
1221
+ <mat-checkbox formControlName="showDropzone"
1222
+ >Exibir área de soltar</mat-checkbox
1223
+ >
1224
+ <mat-checkbox formControlName="showProgress"
1225
+ >Exibir barra de progresso</mat-checkbox
1226
+ >
1227
+ <mat-checkbox formControlName="showConflictPolicySelector"
1228
+ >Permitir escolher a política de conflito</mat-checkbox
1229
+ >
1230
+ <mat-checkbox formControlName="manualUpload"
1231
+ >Exigir clique em “Enviar” (modo manual)</mat-checkbox
1232
+ >
1233
+ <mat-checkbox formControlName="dense">Layout compacto</mat-checkbox>
1234
+ <mat-form-field appearance="fill">
1235
+ <mat-label>Tipos permitidos (accept) <span class="opt-tag">opcional</span></mat-label>
1236
+ <input
1237
+ matInput
1238
+ formControlName="accept"
1239
+ placeholder="ex.: pdf,jpg,png"
1240
+ />
1241
+ <mat-hint>Lista separada por vírgula (opcional).</mat-hint>
1242
+ </mat-form-field>
1243
+ <mat-checkbox formControlName="showMetadataForm"
1244
+ >Exibir formulário de metadados (JSON)</mat-checkbox
1245
+ >
1246
+
1247
+ <!-- NOVO: Grupo Dropzone -->
1248
+ <fieldset [formGroup]="dropzoneGroup" class="subgroup">
1249
+ <legend>
1250
+ <mat-icon aria-hidden="true">download</mat-icon>
1251
+ Dropzone (expansão por proximidade)
1252
+ </legend>
1253
+ <mat-checkbox formControlName="expandOnDragProximity">
1254
+ Expandir ao aproximar arquivo durante arraste
1255
+ </mat-checkbox>
1256
+ <mat-form-field appearance="fill">
1257
+ <mat-label>Raio de proximidade (px) <span class="opt-tag">opcional</span></mat-label>
1258
+ <input matInput type="number" formControlName="proximityPx" />
1259
+ </mat-form-field>
1260
+ <mat-form-field appearance="fill">
1261
+ <mat-label>Modo de expansão</mat-label>
1262
+ <mat-select formControlName="expandMode">
1263
+ <mat-option value="overlay">Overlay (recomendado)</mat-option>
1264
+ <mat-option value="inline">Inline</mat-option>
1265
+ </mat-select>
1266
+ </mat-form-field>
1267
+ <mat-form-field appearance="fill">
1268
+ <mat-label>Altura do overlay (px) <span class="opt-tag">opcional</span></mat-label>
1269
+ <input matInput type="number" formControlName="expandHeight" />
1270
+ </mat-form-field>
1271
+ <mat-form-field appearance="fill">
1272
+ <mat-label>Debounce de arraste (ms) <span class="opt-tag">opcional</span></mat-label>
1273
+ <input matInput type="number" formControlName="expandDebounceMs" />
1274
+ </mat-form-field>
1275
+ </fieldset>
1276
+
1277
+ <!-- NOVO: Grupo Lista/Detalhes -->
1278
+ <fieldset [formGroup]="listGroup" class="subgroup">
1279
+ <legend>
1280
+ <mat-icon aria-hidden="true">view_list</mat-icon>
1281
+ Lista e detalhes
1282
+ </legend>
1283
+ <mat-form-field appearance="fill">
1284
+ <mat-label>Colapsar após (itens) <span class="opt-tag">opcional</span></mat-label>
1285
+ <input matInput type="number" formControlName="collapseAfter" />
1286
+ </mat-form-field>
1287
+ <mat-form-field appearance="fill">
1288
+ <mat-label>Modo de detalhes</mat-label>
1289
+ <mat-select formControlName="detailsMode">
1290
+ <mat-option value="auto">Automático</mat-option>
1291
+ <mat-option value="card">Card (overlay)</mat-option>
1292
+ <mat-option value="sidesheet">Side-sheet</mat-option>
1293
+ </mat-select>
1294
+ </mat-form-field>
1295
+ <mat-form-field appearance="fill">
1296
+ <mat-label>Largura máxima do card (px) <span class="opt-tag">opcional</span></mat-label>
1297
+ <input matInput type="number" formControlName="detailsMaxWidth" />
1298
+ </mat-form-field>
1299
+ <mat-checkbox formControlName="detailsShowTechnical">
1300
+ Mostrar detalhes técnicos por padrão
1301
+ </mat-checkbox>
1302
+ <mat-form-field appearance="fill">
1303
+ <mat-label>Campos de metadados (whitelist) <span class="opt-tag">opcional</span></mat-label>
1304
+ <input matInput formControlName="detailsFields" placeholder="ex.: id,fileName,contentType" />
1305
+ <mat-hint>Lista separada por vírgula; vazio = todos.</mat-hint>
1306
+ </mat-form-field>
1307
+ <mat-form-field appearance="fill">
1308
+ <mat-label>Âncora do overlay</mat-label>
1309
+ <mat-select formControlName="detailsAnchor">
1310
+ <mat-option value="item">Item</mat-option>
1311
+ <mat-option value="field">Campo</mat-option>
1312
+ </mat-select>
1313
+ </mat-form-field>
1314
+ </fieldset>
1315
+ </form>
1316
+ </mat-tab>
1317
+ <mat-tab label="Validações">
1318
+ <form [formGroup]="limitsGroup">
1319
+ <h4 class="section-subtitle">
1320
+ <mat-icon aria-hidden="true">build</mat-icon>
1321
+ Opções configuráveis — Validações
1322
+ </h4>
1323
+ <mat-form-field appearance="fill">
1324
+ <mat-label>Tamanho máximo do arquivo (bytes) <span class="opt-tag">opcional</span></mat-label>
1325
+ <input matInput type="number" formControlName="maxFileSizeBytes" />
1326
+ <mat-hint>Limite de validação no cliente (opcional).</mat-hint>
1327
+ </mat-form-field>
1328
+ <mat-form-field appearance="fill">
1329
+ <mat-label>Máx. arquivos por lote <span class="opt-tag">opcional</span></mat-label>
1330
+ <input matInput type="number" formControlName="maxFilesPerBulk" />
1331
+ </mat-form-field>
1332
+ <mat-form-field appearance="fill">
1333
+ <mat-label>Tamanho máximo do lote (bytes) <span class="opt-tag">opcional</span></mat-label>
1334
+ <input matInput type="number" formControlName="maxBulkSizeBytes" />
1335
+ </mat-form-field>
1336
+ <mat-form-field appearance="fill">
1337
+ <mat-label>Política de conflito (padrão) <span class="opt-tag">opcional</span></mat-label>
1338
+ <mat-select formControlName="defaultConflictPolicy">
1339
+ <mat-option value="RENAME">Renomear automaticamente</mat-option>
1340
+ <mat-option value="MAKE_UNIQUE">Gerar nome único</mat-option>
1341
+ <mat-option value="OVERWRITE">Sobrescrever arquivo existente</mat-option>
1342
+ <mat-option value="SKIP">Pular se já existir</mat-option>
1343
+ <mat-option value="ERROR">Falhar (erro)</mat-option>
1344
+ </mat-select>
1345
+ <mat-hint>O que fazer quando o nome do arquivo já existe.</mat-hint>
1346
+ <div class="warn" *ngIf="limitsGroup.get('defaultConflictPolicy')?.value === 'OVERWRITE'">
1347
+ <mat-icon color="warn" aria-hidden="true">warning</mat-icon>
1348
+ Atenção: OVERWRITE pode sobrescrever arquivos existentes.
1349
+ </div>
1350
+ </mat-form-field>
1351
+ <mat-checkbox formControlName="failFast"
1352
+ >Parar no primeiro erro (fail-fast)</mat-checkbox
1353
+ >
1354
+ <mat-checkbox formControlName="strictValidation"
1355
+ >Validação rigorosa (backend)</mat-checkbox
1356
+ >
1357
+ <mat-form-field appearance="fill">
1358
+ <mat-label>Tamanho máx. por arquivo (MB) <span class="req-tag">mandatório</span></mat-label>
1359
+ <input matInput type="number" formControlName="maxUploadSizeMb" required />
1360
+ <mat-hint>Validado pelo backend (1–500 MB).</mat-hint>
1361
+ </mat-form-field>
1362
+ </form>
1363
+ <form [formGroup]="optionsGroup">
1364
+ <h4 class="section-subtitle">
1365
+ <mat-icon aria-hidden="true">tune</mat-icon>
1366
+ Opções configuráveis — Avançado
1367
+ </h4>
1368
+ <mat-form-field appearance="fill">
1369
+ <mat-label>Extensões permitidas <span class="opt-tag">opcional</span></mat-label>
1370
+ <input
1371
+ matInput
1372
+ formControlName="allowedExtensions"
1373
+ placeholder="ex.: pdf,docx,xlsx"
1374
+ />
1375
+ <mat-hint>Lista separada por vírgula (opcional).</mat-hint>
1376
+ </mat-form-field>
1377
+ <mat-form-field appearance="fill">
1378
+ <mat-label>MIME types aceitos <span class="opt-tag">opcional</span></mat-label>
1379
+ <input
1380
+ matInput
1381
+ formControlName="acceptMimeTypes"
1382
+ placeholder="ex.: application/pdf,image/png"
1383
+ />
1384
+ <mat-hint>Lista separada por vírgula (opcional).</mat-hint>
1385
+ </mat-form-field>
1386
+ <mat-form-field appearance="fill">
1387
+ <mat-label>Diretório destino <span class="opt-tag">opcional</span></mat-label>
1388
+ <input
1389
+ matInput
1390
+ formControlName="targetDirectory"
1391
+ placeholder="ex.: documentos/notas"
1392
+ />
1393
+ </mat-form-field>
1394
+ <mat-checkbox formControlName="enableVirusScanning">
1395
+ Forçar antivírus (quando disponível)
1396
+ </mat-checkbox>
1397
+ <div class="warn" *ngIf="optionsGroup.get('enableVirusScanning')?.value === true">
1398
+ <mat-icon color="warn" aria-hidden="true">warning</mat-icon>
1399
+ Pode impactar desempenho e latência de upload.
1400
+ </div>
1401
+ </form>
1402
+ <form [formGroup]="quotasGroup">
1403
+ <h4 class="section-subtitle">
1404
+ <mat-icon aria-hidden="true">edit</mat-icon>
1405
+ Opções configuráveis — Quotas (UI)
1406
+ </h4>
1407
+ <mat-checkbox formControlName="showQuotaWarnings"
1408
+ >Exibir avisos de cota</mat-checkbox
1409
+ >
1410
+ <mat-checkbox formControlName="blockOnExceed"
1411
+ >Bloquear ao exceder cota</mat-checkbox
1412
+ >
1413
+ </form>
1414
+ <form [formGroup]="rateLimitGroup">
1415
+ <h4 class="section-subtitle">
1416
+ <mat-icon aria-hidden="true">edit</mat-icon>
1417
+ Opções configuráveis — Rate Limit (UI)
1418
+ </h4>
1419
+ <mat-checkbox formControlName="showBannerOn429"
1420
+ >Exibir banner quando atingir o limite</mat-checkbox
1421
+ >
1422
+ <mat-checkbox formControlName="autoRetryOn429"
1423
+ >Tentar novamente automaticamente</mat-checkbox
1424
+ >
1425
+ <mat-form-field appearance="fill">
1426
+ <mat-label>Máximo de tentativas automáticas <span class="opt-tag">opcional</span></mat-label>
1427
+ <input matInput type="number" formControlName="maxAutoRetry" />
1428
+ </mat-form-field>
1429
+ <mat-form-field appearance="fill">
1430
+ <mat-label>Intervalo base entre tentativas (ms) <span class="opt-tag">opcional</span></mat-label>
1431
+ <input matInput type="number" formControlName="baseBackoffMs" />
1432
+ </mat-form-field>
1433
+ </form>
1434
+ </mat-tab>
1435
+ <mat-tab label="Mensagens">
1436
+ <form [formGroup]="messagesGroup">
1437
+ <h4 class="section-subtitle">
1438
+ <mat-icon aria-hidden="true">edit</mat-icon>
1439
+ Opções configuráveis — Mensagens (UI)
1440
+ </h4>
1441
+ <mat-form-field appearance="fill">
1442
+ <mat-label>Sucesso (individual) <span class="opt-tag">opcional</span></mat-label>
1443
+ <input
1444
+ matInput
1445
+ formControlName="successSingle"
1446
+ placeholder="ex.: Arquivo enviado com sucesso"
1447
+ />
1448
+ </mat-form-field>
1449
+ <mat-form-field appearance="fill">
1450
+ <mat-label>Sucesso (em lote) <span class="opt-tag">opcional</span></mat-label>
1451
+ <input
1452
+ matInput
1453
+ formControlName="successBulk"
1454
+ placeholder="ex.: Upload concluído"
1455
+ />
1456
+ </mat-form-field>
1457
+ <div [formGroup]="errorsGroup">
1458
+ <ng-container *ngFor="let e of errorEntries">
1459
+ <mat-form-field appearance="fill">
1460
+ <mat-label>{{ e.label }} <span class="opt-tag">opcional</span></mat-label>
1461
+ <input matInput [formControlName]="e.code" />
1462
+ <mat-hint class="code-hint">{{ e.code }}</mat-hint>
1463
+ </mat-form-field>
1464
+ </ng-container>
1465
+ </div>
1466
+ </form>
1467
+ </mat-tab>
1468
+ <mat-tab label="Cabeçalhos">
1469
+ <form [formGroup]="headersGroup">
1470
+ <h4 class="section-subtitle">
1471
+ <mat-icon aria-hidden="true">edit</mat-icon>
1472
+ Opções configuráveis — Cabeçalhos (consulta)
1473
+ </h4>
1474
+ <mat-form-field appearance="fill">
1475
+ <mat-label>Cabeçalho de tenant <span class="opt-tag">opcional</span></mat-label>
1476
+ <input
1477
+ matInput
1478
+ formControlName="tenantHeader"
1479
+ placeholder="X-Tenant-Id"
1480
+ />
1481
+ </mat-form-field>
1482
+ <mat-form-field appearance="fill">
1483
+ <mat-label>Valor do tenant <span class="opt-tag">opcional</span></mat-label>
1484
+ <input
1485
+ matInput
1486
+ formControlName="tenantValue"
1487
+ placeholder="ex.: demo-tenant"
1488
+ />
1489
+ </mat-form-field>
1490
+ <mat-form-field appearance="fill">
1491
+ <mat-label>Cabeçalho de usuário <span class="opt-tag">opcional</span></mat-label>
1492
+ <input
1493
+ matInput
1494
+ formControlName="userHeader"
1495
+ placeholder="X-User-Id"
1496
+ />
1497
+ </mat-form-field>
1498
+ <mat-form-field appearance="fill">
1499
+ <mat-label>Valor do usuário <span class="opt-tag">opcional</span></mat-label>
1500
+ <input matInput formControlName="userValue" placeholder="ex.: 42" />
1501
+ </mat-form-field>
1502
+ </form>
1503
+ </mat-tab>
1504
+ <mat-tab label="Servidor">
1505
+ <div class="server-tab">
1506
+ <div class="toolbar">
1507
+ <h4 class="section-subtitle ro">
1508
+ <mat-icon aria-hidden="true">info</mat-icon>
1509
+ Servidor (somente leitura)
1510
+ <span class="badge">read-only</span>
1511
+ </h4>
1512
+ <button type="button" (click)="refetchServerConfig()">
1513
+ Recarregar do servidor
1514
+ </button>
1515
+ <span class="hint" *ngIf="!baseUrl"
1516
+ >Defina a baseUrl no componente pai para consultar
1517
+ /api/files/config.</span
1518
+ >
1519
+ </div>
1520
+ <div *ngIf="serverLoading(); else serverLoaded">
1521
+ Carregando configuração do servidor…
1522
+ </div>
1523
+ <ng-template #serverLoaded>
1524
+ <div *ngIf="serverError(); else serverOk" class="error">
1525
+ Falha ao carregar: {{ serverError() | json }}
1526
+ </div>
1527
+ <ng-template #serverOk>
1528
+ <section *ngIf="serverData() as _">
1529
+ <h3>Resumo da configuração ativa</h3>
1530
+ <ul class="summary">
1531
+ <li>
1532
+ <strong>Max por arquivo (MB):</strong>
1533
+ {{ serverData()?.options?.maxUploadSizeMb }}
1534
+ </li>
1535
+ <li>
1536
+ <strong>Validação rigorosa:</strong>
1537
+ {{ serverData()?.options?.strictValidation }}
1538
+ </li>
1539
+ <li>
1540
+ <strong>Antivírus:</strong>
1541
+ {{ serverData()?.options?.enableVirusScanning }}
1542
+ </li>
1543
+ <li>
1544
+ <strong>Conflito de nome (padrão):</strong>
1545
+ {{ serverData()?.options?.nameConflictPolicy }}
1546
+ </li>
1547
+ <li>
1548
+ <strong>MIME aceitos:</strong>
1549
+ {{
1550
+ (serverData()?.options?.acceptMimeTypes || []).join(', ')
1551
+ }}
1552
+ </li>
1553
+ <li>
1554
+ <strong>Bulk - fail-fast padrão:</strong>
1555
+ {{ serverData()?.bulk?.failFastModeDefault }}
1556
+ </li>
1557
+ <li>
1558
+ <strong>Rate limit:</strong>
1559
+ {{ serverData()?.rateLimit?.enabled }} ({{
1560
+ serverData()?.rateLimit?.perMinute
1561
+ }}/min, {{ serverData()?.rateLimit?.perHour }}/h)
1562
+ </li>
1563
+ <li>
1564
+ <strong>Quotas:</strong> {{ serverData()?.quotas?.enabled }}
1565
+ </li>
1566
+ <li>
1567
+ <strong>Servidor:</strong> v{{
1568
+ serverData()?.metadata?.version
1569
+ }}
1570
+ • {{ serverData()?.metadata?.locale }}
1571
+ </li>
1572
+ </ul>
1573
+ <details>
1574
+ <summary>Ver JSON</summary>
1575
+ <button
1576
+ mat-icon-button
1577
+ aria-label="Copiar JSON"
1578
+ (click)="copyServerConfig()"
1579
+ type="button"
1580
+ title="Copiar JSON"
1581
+ >
1582
+ <mat-icon>content_copy</mat-icon>
1583
+ </button>
1584
+ <pre>{{ serverData() | json }}</pre>
1585
+ </details>
1586
+ <p class="note">
1587
+ As opções acima que podem ser alteradas via payload são:
1588
+ conflito de nome, validação rigorosa, tamanho máximo (MB),
1589
+ extensões/MIME aceitos, diretório destino, antivírus,
1590
+ metadados personalizados e fail-fast (no bulk).
1591
+ </p>
1592
+ </section>
1593
+ </ng-template>
1594
+ </ng-template>
1595
+ </div>
1596
+ </mat-tab>
1597
+ <mat-tab label="JSON">
1598
+ <textarea
1599
+ rows="10"
1600
+ [ngModel]="form.value | json"
1601
+ (ngModelChange)="onJsonChange($event)"
1602
+ ></textarea>
1603
+ <div class="error" *ngIf="jsonError">{{ jsonError }}</div>
1604
+ </mat-tab>
1605
+ </mat-tab-group>
1606
+ `,
1607
+ }]
1608
+ }], ctorParameters: () => [{ type: i1$1.FormBuilder }, { type: undefined, decorators: [{
1609
+ type: Inject,
1610
+ args: [SETTINGS_PANEL_DATA]
1611
+ }] }, { type: i2.MatSnackBar }, { type: i0.DestroyRef }] });
1612
+
1613
+ const FILES_UPLOAD_TEXTS = new InjectionToken('FILES_UPLOAD_TEXTS', {
1614
+ providedIn: 'root',
1615
+ factory: () => ({
1616
+ settingsAriaLabel: 'Abrir configurações',
1617
+ dropzoneLabel: 'Arraste arquivos ou',
1618
+ dropzoneButton: 'selecionar',
1619
+ conflictPolicyLabel: 'Política de conflito',
1620
+ metadataLabel: 'Metadados (JSON)',
1621
+ progressAriaLabel: 'Progresso do upload',
1622
+ rateLimitBanner: 'Limite de requisições excedido. Tente novamente às',
1623
+ }),
1624
+ });
1625
+
1626
+ const TRANSLATE_LIKE = new InjectionToken('TRANSLATE_LIKE');
1627
+ const FILES_UPLOAD_ERROR_MESSAGES = new InjectionToken('FILES_UPLOAD_ERROR_MESSAGES');
1628
+ const DEFAULT_MESSAGES = {
1629
+ INVALID_FILE_TYPE: 'Tipo de arquivo inválido.',
1630
+ FILE_TOO_LARGE: 'Arquivo muito grande.',
1631
+ NOT_FOUND: 'Arquivo não encontrado.',
1632
+ UNAUTHORIZED: 'Requisição não autorizada.',
1633
+ RATE_LIMIT_EXCEEDED: 'Limite de requisições excedido.',
1634
+ INTERNAL_ERROR: 'Erro interno do servidor.',
1635
+ QUOTA_EXCEEDED: 'Cota excedida.',
1636
+ SEC_VIRUS_DETECTED: 'Vírus detectado no arquivo.',
1637
+ SEC_MALICIOUS_CONTENT: 'Conteúdo malicioso detectado.',
1638
+ SEC_DANGEROUS_TYPE: 'Tipo de arquivo perigoso.',
1639
+ FMT_MAGIC_MISMATCH: 'Conteúdo do arquivo incompatível.',
1640
+ FMT_CORRUPTED: 'Arquivo corrompido.',
1641
+ FMT_UNSUPPORTED: 'Formato de arquivo não suportado.',
1642
+ SYS_STORAGE_ERROR: 'Erro de armazenamento.',
1643
+ SYS_SERVICE_DOWN: 'Serviço indisponível.',
1644
+ SYS_RATE_LIMIT: 'Limite de requisições excedido.',
1645
+ // aliases em português vindos do backend
1646
+ ARQUIVO_MUITO_GRANDE: 'Arquivo muito grande.',
1647
+ TIPO_ARQUIVO_INVALIDO: 'Tipo de arquivo inválido.',
1648
+ TIPO_MIDIA_NAO_SUPORTADO: 'Tipo de mídia não suportado.',
1649
+ CAMPO_OBRIGATORIO_AUSENTE: 'Campo obrigatório ausente.',
1650
+ OPCOES_JSON_INVALIDAS: 'JSON de opções inválido.',
1651
+ ARGUMENTO_INVALIDO: 'Argumento inválido.',
1652
+ NAO_AUTORIZADO: 'Autenticação necessária.',
1653
+ ERRO_INTERNO: 'Erro interno do servidor.',
1654
+ LIMITE_TAXA_EXCEDIDO: 'Limite de taxa excedido.',
1655
+ COTA_EXCEDIDA: 'Cota excedida.',
1656
+ ARQUIVO_JA_EXISTE: 'Arquivo já existe.',
1657
+ };
1658
+ class ErrorMapperService {
1659
+ customMessages;
1660
+ translate;
1661
+ constructor(customMessages, translate) {
1662
+ this.customMessages = customMessages;
1663
+ this.translate = translate;
1664
+ }
1665
+ map(error, headers) {
1666
+ const code = String(error.code || 'UNKNOWN_ERROR');
1667
+ const key = `praxis.filesUpload.errors.${code}`;
1668
+ const translated = this.translate?.instant(key);
1669
+ const message = (translated && translated !== key ? translated : undefined) ??
1670
+ this.customMessages?.[code] ??
1671
+ DEFAULT_MESSAGES[code] ??
1672
+ error.message ??
1673
+ 'Erro desconhecido.';
1674
+ // metadados do catálogo
1675
+ const meta = getErrorMeta(code);
1676
+ const mapped = {
1677
+ message,
1678
+ title: meta.title,
1679
+ severity: meta.severity,
1680
+ category: meta.category,
1681
+ phase: meta.phase,
1682
+ userAction: meta.userAction,
1683
+ };
1684
+ const isRateLimit = code === 'LIMITE_TAXA_EXCEDIDO' ||
1685
+ code === 'RATE_LIMIT_EXCEEDED' ||
1686
+ code === 'SYS_RATE_LIMIT';
1687
+ if (isRateLimit) {
1688
+ const limit = Number(headers?.get('X-RateLimit-Limit'));
1689
+ const remaining = Number(headers?.get('X-RateLimit-Remaining'));
1690
+ const resetEpochSeconds = Number(headers?.get('X-RateLimit-Reset'));
1691
+ if (!Number.isNaN(limit) &&
1692
+ !Number.isNaN(remaining) &&
1693
+ !Number.isNaN(resetEpochSeconds)) {
1694
+ mapped.rateLimit = { limit, remaining, resetEpochSeconds };
1695
+ }
1696
+ }
1697
+ return mapped;
1698
+ }
1699
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: ErrorMapperService, deps: [{ token: FILES_UPLOAD_ERROR_MESSAGES, optional: true }, { token: TRANSLATE_LIKE, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
1700
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: ErrorMapperService, providedIn: 'root' });
1701
+ }
1702
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: ErrorMapperService, decorators: [{
1703
+ type: Injectable,
1704
+ args: [{ providedIn: 'root' }]
1705
+ }], ctorParameters: () => [{ type: undefined, decorators: [{
1706
+ type: Optional
1707
+ }, {
1708
+ type: Inject,
1709
+ args: [FILES_UPLOAD_ERROR_MESSAGES]
1710
+ }] }, { type: undefined, decorators: [{
1711
+ type: Optional
1712
+ }, {
1713
+ type: Inject,
1714
+ args: [TRANSLATE_LIKE]
1715
+ }] }] });
1716
+
1717
+ function toError(key, info) {
1718
+ return { [key]: info };
1719
+ }
1720
+ function ensureFiles(value) {
1721
+ if (!value)
1722
+ return [];
1723
+ return Array.isArray(value) ? value : [value];
1724
+ }
1725
+ function acceptValidator(allowed) {
1726
+ const normalized = (allowed || []).map((a) => a.toLowerCase());
1727
+ const acceptAll = normalized.includes('*');
1728
+ return (control) => {
1729
+ if (acceptAll)
1730
+ return null;
1731
+ const files = ensureFiles(control.value);
1732
+ if (!files.length)
1733
+ return null;
1734
+ const invalid = files.find((f) => {
1735
+ const type = f.type ? String(f.type).toLowerCase() : '';
1736
+ const name = f.name ? String(f.name).toLowerCase() : '';
1737
+ const ext = name.includes('.')
1738
+ ? name.substring(name.lastIndexOf('.'))
1739
+ : '';
1740
+ return !normalized.some((a) => {
1741
+ if (a.endsWith('/*')) {
1742
+ return type.startsWith(a.slice(0, -1));
1743
+ }
1744
+ if (a.startsWith('.')) {
1745
+ return ext === a;
1746
+ }
1747
+ return type === a;
1748
+ });
1749
+ });
1750
+ return invalid ? toError('accept', { allowed }) : null;
1751
+ };
1752
+ }
1753
+ function maxFileSizeValidator(maxBytes) {
1754
+ return (control) => {
1755
+ const files = ensureFiles(control.value);
1756
+ if (!files.length)
1757
+ return null;
1758
+ const tooBig = files.find((f) => f.size > maxBytes);
1759
+ return tooBig ? toError('maxFileSize', { max: maxBytes }) : null;
1760
+ };
1761
+ }
1762
+ function maxFilesPerBulkValidator(maxFiles) {
1763
+ return (control) => {
1764
+ const files = ensureFiles(control.value);
1765
+ if (!files.length)
1766
+ return null;
1767
+ return files.length > maxFiles
1768
+ ? toError('maxFilesPerBulk', { max: maxFiles, actual: files.length })
1769
+ : null;
1770
+ };
1771
+ }
1772
+ function maxBulkSizeValidator(maxBytes) {
1773
+ return (control) => {
1774
+ const files = ensureFiles(control.value);
1775
+ if (!files.length)
1776
+ return null;
1777
+ const total = files.reduce((sum, f) => sum + (f.size || 0), 0);
1778
+ return total > maxBytes
1779
+ ? toError('maxBulkSize', { max: maxBytes, actual: total })
1780
+ : null;
1781
+ };
1782
+ }
1783
+
1784
+ class DragProximityService {
1785
+ zone;
1786
+ draggingWithFiles$ = new BehaviorSubject(false);
1787
+ pointer$ = new BehaviorSubject({ x: -9999, y: -9999 });
1788
+ dragCounter = 0;
1789
+ constructor(zone) {
1790
+ this.zone = zone;
1791
+ this.zone.runOutsideAngular(() => {
1792
+ window.addEventListener('dragenter', this.onDragEnter, { passive: true });
1793
+ window.addEventListener('dragleave', this.onDragLeave, { passive: true });
1794
+ window.addEventListener('dragover', this.onDragOver, { passive: true });
1795
+ window.addEventListener('drop', this.onDrop, { passive: true });
1796
+ });
1797
+ }
1798
+ ngOnDestroy() {
1799
+ window.removeEventListener('dragenter', this.onDragEnter);
1800
+ window.removeEventListener('dragleave', this.onDragLeave);
1801
+ window.removeEventListener('dragover', this.onDragOver);
1802
+ window.removeEventListener('drop', this.onDrop);
1803
+ }
1804
+ isDraggingWithFiles() {
1805
+ return this.draggingWithFiles$.asObservable();
1806
+ }
1807
+ pointer() {
1808
+ return this.pointer$.asObservable();
1809
+ }
1810
+ withinProximity(rect, proximityPx, pt = null) {
1811
+ const p = pt ?? this.pointer$.value;
1812
+ const left = rect.left - proximityPx;
1813
+ const top = rect.top - proximityPx;
1814
+ const right = rect.right + proximityPx;
1815
+ const bottom = rect.bottom + proximityPx;
1816
+ return p.x >= left && p.x <= right && p.y >= top && p.y <= bottom;
1817
+ }
1818
+ onDragEnter = (e) => {
1819
+ this.dragCounter++;
1820
+ const hasFiles = !!e.dataTransfer && Array.from(e.dataTransfer.types || []).includes('Files');
1821
+ if (hasFiles)
1822
+ this.draggingWithFiles$.next(true);
1823
+ };
1824
+ onDragLeave = (_e) => {
1825
+ this.dragCounter = Math.max(0, this.dragCounter - 1);
1826
+ if (this.dragCounter === 0) {
1827
+ this.draggingWithFiles$.next(false);
1828
+ this.pointer$.next({ x: -9999, y: -9999 });
1829
+ }
1830
+ };
1831
+ onDragOver = (e) => {
1832
+ this.pointer$.next({ x: e.clientX, y: e.clientY });
1833
+ };
1834
+ onDrop = (_e) => {
1835
+ this.dragCounter = 0;
1836
+ this.draggingWithFiles$.next(false);
1837
+ this.pointer$.next({ x: -9999, y: -9999 });
1838
+ };
1839
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: DragProximityService, deps: [{ token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Injectable });
1840
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: DragProximityService, providedIn: 'root' });
1841
+ }
1842
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: DragProximityService, decorators: [{
1843
+ type: Injectable,
1844
+ args: [{ providedIn: 'root' }]
1845
+ }], ctorParameters: () => [{ type: i0.NgZone }] });
1846
+
1847
+ class FilesApiClient {
1848
+ http;
1849
+ constructor(http) {
1850
+ this.http = http;
1851
+ }
1852
+ /**
1853
+ * Upload a single file com metadata/conflictPolicy (compat) e/ou options JSON.
1854
+ */
1855
+ upload(baseUrl, file, options = {}) {
1856
+ const formData = new FormData();
1857
+ formData.append('file', file);
1858
+ if (options.optionsJson)
1859
+ formData.append('options', options.optionsJson);
1860
+ if (options.metadata)
1861
+ formData.append('metadata', JSON.stringify(options.metadata));
1862
+ if (options.conflictPolicy)
1863
+ formData.append('conflictPolicy', options.conflictPolicy);
1864
+ return this.http
1865
+ .post(`${baseUrl}/upload`, formData, {
1866
+ reportProgress: true,
1867
+ observe: 'events',
1868
+ })
1869
+ .pipe(retry(options.retryCount ?? 0));
1870
+ }
1871
+ /**
1872
+ * Upload múltiplo com arrays de metadata/conflictPolicy (compat) e/ou options JSON.
1873
+ */
1874
+ bulkUpload(baseUrl, files, options = {}) {
1875
+ const formData = new FormData();
1876
+ files.forEach((f) => {
1877
+ formData.append('files', f.file);
1878
+ });
1879
+ if (options.optionsJson)
1880
+ formData.append('options', options.optionsJson);
1881
+ // Suporte a failFast/failFastMode
1882
+ const ff = options.failFast ?? options.failFastMode;
1883
+ if (ff !== undefined)
1884
+ formData.append('failFast', String(ff));
1885
+ // Arrays de metadata/políticas quando presentes
1886
+ const metas = files.map((f) => f.metadata ?? null);
1887
+ if (metas.some((m) => m !== null)) {
1888
+ formData.append('metadata', JSON.stringify(metas));
1889
+ }
1890
+ const policies = files.map((f) => f.conflictPolicy ?? null);
1891
+ if (policies.some((p) => p !== null)) {
1892
+ formData.append('conflictPolicy', JSON.stringify(policies));
1893
+ }
1894
+ return this.http
1895
+ .post(`${baseUrl}/bulk`, formData, {
1896
+ reportProgress: true,
1897
+ observe: 'events',
1898
+ })
1899
+ .pipe(retry(options.retryCount ?? 0));
1900
+ }
1901
+ presign(baseUrl, fileName) {
1902
+ // Backend: POST /upload/presign?filename=...
1903
+ const url = `${baseUrl}/upload/presign?filename=${encodeURIComponent(fileName)}`;
1904
+ return this.http.post(url, null, { observe: 'body' });
1905
+ }
1906
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: FilesApiClient, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
1907
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: FilesApiClient, providedIn: 'root' });
1908
+ }
1909
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: FilesApiClient, decorators: [{
1910
+ type: Injectable,
1911
+ args: [{ providedIn: 'root' }]
1912
+ }], ctorParameters: () => [{ type: i1.HttpClient }] });
1913
+
1914
+ class PresignedUploaderService {
1915
+ http;
1916
+ constructor(http) {
1917
+ this.http = http;
1918
+ }
1919
+ /**
1920
+ * Uploads a file to a presigned URL and retries when configured.
1921
+ */
1922
+ upload(target, file, retryCount = 0) {
1923
+ const formData = new FormData();
1924
+ if (target.fields) {
1925
+ Object.entries(target.fields).forEach(([key, value]) => {
1926
+ formData.append(key, value);
1927
+ });
1928
+ }
1929
+ formData.append('file', file);
1930
+ return this.http
1931
+ .post(target.uploadUrl, formData, {
1932
+ headers: target.headers,
1933
+ observe: 'events',
1934
+ reportProgress: true,
1935
+ withCredentials: false,
1936
+ })
1937
+ .pipe(retry(retryCount));
1938
+ }
1939
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PresignedUploaderService, deps: [{ token: i1.HttpClient }], target: i0.ɵɵFactoryTarget.Injectable });
1940
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PresignedUploaderService, providedIn: 'root' });
1941
+ }
1942
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PresignedUploaderService, decorators: [{
1943
+ type: Injectable,
1944
+ args: [{ providedIn: 'root' }]
1945
+ }], ctorParameters: () => [{ type: i1.HttpClient }] });
1946
+
1947
+ class PraxisFilesUpload {
1948
+ settingsPanel;
1949
+ dragProx;
1950
+ defaultTexts;
1951
+ overlay;
1952
+ viewContainerRef;
1953
+ cdr;
1954
+ api;
1955
+ presign;
1956
+ errors;
1957
+ translate;
1958
+ configStorage;
1959
+ config = null;
1960
+ componentId = 'default';
1961
+ baseUrl;
1962
+ displayMode = 'full';
1963
+ uploadSuccess = new EventEmitter();
1964
+ bulkComplete = new EventEmitter();
1965
+ error = new EventEmitter();
1966
+ rateLimited = new EventEmitter();
1967
+ // Novos outputs Sub-issue 4
1968
+ retry = new EventEmitter();
1969
+ download = new EventEmitter();
1970
+ copyLink = new EventEmitter();
1971
+ detailsOpened = new EventEmitter();
1972
+ detailsClosed = new EventEmitter();
1973
+ pendingFilesChange = new EventEmitter();
1974
+ proximityChange = new EventEmitter();
1975
+ fileInput;
1976
+ hostRef;
1977
+ overlayTmpl;
1978
+ // Novo: template do overlay de seleção
1979
+ selectedOverlayTmpl;
1980
+ conflictPolicies = ['RENAME', 'ERROR', 'OVERWRITE', 'SKIP', 'MAKE_UNIQUE'];
1981
+ conflictPolicy = new FormControl('RENAME');
1982
+ uploadProgress = 0;
1983
+ allowMultiple = false;
1984
+ rateLimit = null;
1985
+ quotaExceeded = false;
1986
+ errorMessage = '';
1987
+ acceptString = '';
1988
+ metadata = new FormControl('');
1989
+ uploadedFiles = [];
1990
+ texts;
1991
+ // Estados para lista rica de resultados
1992
+ lastResults = null;
1993
+ lastStats = null;
1994
+ // Expor enum no template
1995
+ BulkUploadResultStatus = BulkUploadResultStatus;
1996
+ uploading = false;
1997
+ isDragging = false;
1998
+ currentFiles = [];
1999
+ // Config efetiva do servidor para fallbacks
2000
+ serverEffective;
2001
+ // Proximidade/overlay
2002
+ showProximityOverlay = false;
2003
+ proxSub;
2004
+ proxPtrSub;
2005
+ hostRect;
2006
+ draggingFiles = false;
2007
+ // CDK Overlay state
2008
+ overlayRef;
2009
+ escListener = (e) => {
2010
+ if (e.key === 'Escape') {
2011
+ this.hideProximityOverlay();
2012
+ this.hideSelectedOverlay();
2013
+ }
2014
+ };
2015
+ windowDropListener = (_e) => {
2016
+ this.hideProximityOverlay();
2017
+ };
2018
+ windowDragEndListener = (_e) => {
2019
+ this.hideProximityOverlay();
2020
+ };
2021
+ // Novo: overlay para seleção
2022
+ selectedOverlayRef;
2023
+ // Lista: colapso
2024
+ showAllResults = false;
2025
+ constructor(settingsPanel, dragProx, defaultTexts, overlay, viewContainerRef, cdr, api, presign, errors, translate, configStorage) {
2026
+ this.settingsPanel = settingsPanel;
2027
+ this.dragProx = dragProx;
2028
+ this.defaultTexts = defaultTexts;
2029
+ this.overlay = overlay;
2030
+ this.viewContainerRef = viewContainerRef;
2031
+ this.cdr = cdr;
2032
+ this.api = api;
2033
+ this.presign = presign;
2034
+ this.errors = errors;
2035
+ this.translate = translate;
2036
+ this.configStorage = configStorage;
2037
+ this.texts = defaultTexts;
2038
+ }
2039
+ // Helpers de UI
2040
+ get showProgress() {
2041
+ return this.uploading;
2042
+ }
2043
+ // Getter público para binding existente
2044
+ get isUploading() {
2045
+ return this.uploading;
2046
+ }
2047
+ get hasUploadedFiles() {
2048
+ return this.uploadedFiles.length > 0;
2049
+ }
2050
+ get pendingFiles() {
2051
+ return this.currentFiles;
2052
+ }
2053
+ get pendingCount() {
2054
+ return this.currentFiles.length;
2055
+ }
2056
+ get hasLastResults() {
2057
+ return Array.isArray(this.lastResults) && this.lastResults.length > 0;
2058
+ }
2059
+ // Abre o seletor de arquivo
2060
+ openFile(input) {
2061
+ if (!this.uploading) {
2062
+ input.click();
2063
+ }
2064
+ }
2065
+ // Expor método público para pais (wrapper) abrirem o seletor
2066
+ triggerSelect() {
2067
+ const el = this.fileInput?.nativeElement;
2068
+ if (el)
2069
+ this.openFile(el);
2070
+ }
2071
+ // Exibe nome de arquivo de forma segura
2072
+ displayFileName(f) {
2073
+ return f && typeof f === 'object' && 'name' in f ? String(f.name) : '';
2074
+ }
2075
+ formatBytes(bytes) {
2076
+ const b = typeof bytes === 'number' ? bytes : 0;
2077
+ if (b < 1024)
2078
+ return `${b} B`;
2079
+ const units = ['KB', 'MB', 'GB', 'TB'];
2080
+ let v = b / 1024;
2081
+ let i = 0;
2082
+ while (v >= 1024 && i < units.length - 1) {
2083
+ v /= 1024;
2084
+ i++;
2085
+ }
2086
+ return `${v.toFixed(1)} ${units[i]}`;
2087
+ }
2088
+ // Novo helper com metadados
2089
+ mapErrorFull(err) {
2090
+ if (!err)
2091
+ return { message: '' };
2092
+ return this.errors ? this.errors.map(err) : err;
2093
+ }
2094
+ get metadataError() {
2095
+ if (!this.config?.ui?.showMetadataForm)
2096
+ return null;
2097
+ const val = this.metadata.value;
2098
+ if (!val)
2099
+ return null;
2100
+ try {
2101
+ JSON.parse(val);
2102
+ return null;
2103
+ }
2104
+ catch {
2105
+ return this.t('invalidMetadata', 'Metadados inválidos.');
2106
+ }
2107
+ }
2108
+ ngOnInit() {
2109
+ const stored = this.configStorage?.loadConfig(`files-upload-config:${this.componentId}`) ?? this.loadLocalConfig();
2110
+ if (stored) {
2111
+ this.config = stored;
2112
+ }
2113
+ this.applyConfig(this.config ?? {});
2114
+ this.resolveTexts();
2115
+ // Nota: carregamento da configuração efetiva do servidor foi desabilitado aqui
2116
+ // para evitar dependência de HttpClient em testes. O fallback permanece opcional.
2117
+ }
2118
+ ngAfterViewInit() {
2119
+ // Calcular retângulo do host
2120
+ this.hostRect = this.hostRef?.nativeElement.getBoundingClientRect();
2121
+ // Atualizar em resize/scroll simples (MVP)
2122
+ window.addEventListener('resize', this.updateHostRect, { passive: true });
2123
+ window.addEventListener('scroll', this.updateHostRect, { passive: true });
2124
+ const debounceMs = this.config?.ui?.dropzone?.expandDebounceMs ?? 120;
2125
+ this.proxSub = this.dragProx
2126
+ .isDraggingWithFiles()
2127
+ .pipe(debounceTime(debounceMs))
2128
+ .subscribe((dragging) => {
2129
+ this.draggingFiles = dragging;
2130
+ this.evalProximity();
2131
+ });
2132
+ this.proxPtrSub = this.dragProx
2133
+ .pointer()
2134
+ .pipe(debounceTime(debounceMs))
2135
+ .subscribe(() => this.evalProximity());
2136
+ // Listeners globais para fechar overlay
2137
+ window.addEventListener('keydown', this.escListener, { passive: true });
2138
+ window.addEventListener('drop', this.windowDropListener, { passive: true });
2139
+ window.addEventListener('dragend', this.windowDragEndListener, {
2140
+ passive: true,
2141
+ });
2142
+ // Garantir que overlay de seleção sincronize se já houver arquivos pendentes
2143
+ this.manageSelectedOverlay();
2144
+ }
2145
+ ngOnDestroy() {
2146
+ window.removeEventListener('resize', this.updateHostRect);
2147
+ window.removeEventListener('scroll', this.updateHostRect);
2148
+ this.proxSub?.unsubscribe();
2149
+ this.proxPtrSub?.unsubscribe();
2150
+ window.removeEventListener('keydown', this.escListener);
2151
+ window.removeEventListener('drop', this.windowDropListener);
2152
+ window.removeEventListener('dragend', this.windowDragEndListener);
2153
+ this.disposeOverlay();
2154
+ this.disposeSelectedOverlay();
2155
+ }
2156
+ updateHostRect = () => {
2157
+ this.hostRect = this.hostRef?.nativeElement.getBoundingClientRect();
2158
+ this.syncOverlayGeometry();
2159
+ };
2160
+ evalProximity() {
2161
+ const oldValue = this.showProximityOverlay;
2162
+ let newValue = false;
2163
+ const cfg = this.config?.ui?.dropzone;
2164
+ const enabled = cfg?.expandOnDragProximity !== false; // default true
2165
+ const blocked = !this.baseUrl ||
2166
+ (this.quotaExceeded && this.config?.quotas?.blockOnExceed) ||
2167
+ this.isUploading;
2168
+ if (enabled && this.hostRect && !blocked) {
2169
+ const proxPx = cfg?.proximityPx ?? 64;
2170
+ const near = this.dragProx.withinProximity(this.hostRect, proxPx);
2171
+ newValue = this.draggingFiles && near;
2172
+ }
2173
+ this.showProximityOverlay = newValue;
2174
+ this.manageOverlay();
2175
+ if (oldValue !== this.showProximityOverlay) {
2176
+ this.proximityChange.emit(this.showProximityOverlay);
2177
+ }
2178
+ }
2179
+ manageOverlay() {
2180
+ const mode = this.config?.ui?.dropzone?.expandMode ?? 'overlay';
2181
+ if (mode !== 'overlay') {
2182
+ this.disposeOverlay();
2183
+ return;
2184
+ }
2185
+ if (this.showProximityOverlay) {
2186
+ this.ensureOverlay();
2187
+ }
2188
+ else {
2189
+ this.disposeOverlay();
2190
+ }
2191
+ }
2192
+ ensureOverlay() {
2193
+ if (this.overlayRef || !this.hostRef || !this.overlayTmpl) {
2194
+ this.syncOverlayGeometry();
2195
+ return;
2196
+ }
2197
+ const hostEl = this.hostRef.nativeElement;
2198
+ const positionStrategy = this.overlay
2199
+ .position()
2200
+ .flexibleConnectedTo(hostEl)
2201
+ .withLockedPosition(true)
2202
+ .withPositions([
2203
+ {
2204
+ originX: 'start',
2205
+ originY: 'top',
2206
+ overlayX: 'start',
2207
+ overlayY: 'top',
2208
+ },
2209
+ ]);
2210
+ this.overlayRef = this.overlay.create({
2211
+ positionStrategy,
2212
+ scrollStrategy: this.overlay.scrollStrategies.reposition(),
2213
+ hasBackdrop: false,
2214
+ panelClass: 'praxis-files-upload-overlay-panel',
2215
+ });
2216
+ this.syncOverlayGeometry();
2217
+ const portal = new TemplatePortal(this.overlayTmpl, this.viewContainerRef);
2218
+ this.overlayRef.attach(portal);
2219
+ }
2220
+ syncOverlayGeometry() {
2221
+ if (!this.overlayRef || !this.hostRef)
2222
+ return;
2223
+ const rect = this.hostRef.nativeElement.getBoundingClientRect();
2224
+ const height = this.config?.ui?.dropzone?.expandHeight ?? 200;
2225
+ this.overlayRef.updateSize({ width: rect.width, height });
2226
+ try {
2227
+ this.overlayRef.updatePosition();
2228
+ }
2229
+ catch { }
2230
+ }
2231
+ disposeOverlay() {
2232
+ if (this.overlayRef) {
2233
+ try {
2234
+ this.overlayRef.detach();
2235
+ }
2236
+ catch { }
2237
+ try {
2238
+ this.overlayRef.dispose();
2239
+ }
2240
+ catch { }
2241
+ this.overlayRef = undefined;
2242
+ }
2243
+ }
2244
+ // Novo: gerenciamento do overlay de seleção
2245
+ manageSelectedOverlay() {
2246
+ if (!this.hostRef || !this.selectedOverlayTmpl)
2247
+ return;
2248
+ if (this.pendingCount > 0 && !this.isUploading) {
2249
+ this.ensureSelectedOverlay();
2250
+ }
2251
+ else {
2252
+ this.hideSelectedOverlay();
2253
+ }
2254
+ this.cdr.markForCheck();
2255
+ }
2256
+ ensureSelectedOverlay() {
2257
+ if (!this.hostRef || !this.selectedOverlayTmpl)
2258
+ return;
2259
+ if (!this.selectedOverlayRef) {
2260
+ const hostEl = this.hostRef.nativeElement;
2261
+ const positionStrategy = this.overlay
2262
+ .position()
2263
+ .flexibleConnectedTo(hostEl)
2264
+ .withLockedPosition(true)
2265
+ .withPositions([
2266
+ {
2267
+ originX: 'start',
2268
+ originY: 'top',
2269
+ overlayX: 'start',
2270
+ overlayY: 'top',
2271
+ },
2272
+ ]);
2273
+ this.selectedOverlayRef = this.overlay.create({
2274
+ positionStrategy,
2275
+ scrollStrategy: this.overlay.scrollStrategies.reposition(),
2276
+ hasBackdrop: false,
2277
+ panelClass: 'praxis-files-selected-overlay-panel',
2278
+ });
2279
+ // Tamanho acompanha o host, altura auto com limite via CSS
2280
+ const rect = hostEl.getBoundingClientRect();
2281
+ this.selectedOverlayRef.updateSize({ width: rect.width });
2282
+ try {
2283
+ this.selectedOverlayRef.updatePosition();
2284
+ }
2285
+ catch { }
2286
+ const portal = new TemplatePortal(this.selectedOverlayTmpl, this.viewContainerRef);
2287
+ this.selectedOverlayRef.attach(portal);
2288
+ }
2289
+ else {
2290
+ this.syncSelectedOverlayGeometry();
2291
+ }
2292
+ }
2293
+ syncSelectedOverlayGeometry() {
2294
+ if (!this.selectedOverlayRef || !this.hostRef)
2295
+ return;
2296
+ const rect = this.hostRef.nativeElement.getBoundingClientRect();
2297
+ this.selectedOverlayRef.updateSize({ width: rect.width });
2298
+ try {
2299
+ this.selectedOverlayRef.updatePosition();
2300
+ }
2301
+ catch { }
2302
+ }
2303
+ hideSelectedOverlay() {
2304
+ if (this.selectedOverlayRef) {
2305
+ try {
2306
+ this.selectedOverlayRef.detach();
2307
+ }
2308
+ catch { }
2309
+ try {
2310
+ this.selectedOverlayRef.dispose();
2311
+ }
2312
+ catch { }
2313
+ this.selectedOverlayRef = undefined;
2314
+ }
2315
+ }
2316
+ disposeSelectedOverlay() {
2317
+ this.hideSelectedOverlay();
2318
+ }
2319
+ hideProximityOverlay() {
2320
+ if (this.showProximityOverlay) {
2321
+ this.showProximityOverlay = false;
2322
+ this.manageOverlay();
2323
+ }
2324
+ }
2325
+ // Helpers de configuração e armazenamento
2326
+ resolveTexts() {
2327
+ this.texts = {
2328
+ settingsAriaLabel: this.t('settingsAriaLabel', this.defaultTexts.settingsAriaLabel),
2329
+ dropzoneLabel: this.t('dropzoneLabel', this.defaultTexts.dropzoneLabel),
2330
+ dropzoneButton: this.t('dropzoneButton', this.defaultTexts.dropzoneButton),
2331
+ conflictPolicyLabel: this.t('conflictPolicyLabel', this.defaultTexts.conflictPolicyLabel),
2332
+ metadataLabel: this.t('metadataLabel', this.defaultTexts.metadataLabel),
2333
+ progressAriaLabel: this.t('progressAriaLabel', this.defaultTexts.progressAriaLabel),
2334
+ rateLimitBanner: this.t('rateLimitBanner', this.defaultTexts.rateLimitBanner),
2335
+ };
2336
+ }
2337
+ t(key, fallback) {
2338
+ const k = `praxis.filesUpload.${key}`;
2339
+ const v = this.translate?.instant(k);
2340
+ return v && v !== k ? v : fallback;
2341
+ }
2342
+ openSettings() {
2343
+ const configCopy = JSON.parse(JSON.stringify(this.config ?? {}));
2344
+ const ref = this.settingsPanel.open({
2345
+ id: `files-upload.${this.componentId}`,
2346
+ title: this.t('settingsTitle', 'Configuração de Upload de Arquivos'),
2347
+ content: {
2348
+ component: PraxisFilesUploadConfigEditor,
2349
+ inputs: { ...configCopy, baseUrl: this.baseUrl },
2350
+ },
2351
+ });
2352
+ ref.applied$.subscribe((cfg) => this.applyConfig(cfg));
2353
+ ref.saved$.subscribe((cfg) => {
2354
+ if (this.configStorage) {
2355
+ this.configStorage.saveConfig(`files-upload-config:${this.componentId}`, cfg);
2356
+ }
2357
+ else {
2358
+ this.saveLocalConfig(cfg);
2359
+ }
2360
+ this.applyConfig(cfg);
2361
+ });
2362
+ ref.reset$.subscribe(() => {
2363
+ this.applyConfig(this.config ?? {});
2364
+ });
2365
+ }
2366
+ applyConfig(cfg) {
2367
+ this.config = { ...cfg };
2368
+ this.conflictPolicy.setValue((cfg.limits?.defaultConflictPolicy ?? 'RENAME'));
2369
+ this.allowMultiple = (cfg.limits?.maxFilesPerBulk ?? 1) > 1;
2370
+ const acc = cfg.ui?.accept ?? [];
2371
+ this.acceptString = acc.includes('*') ? '' : acc.join(',');
2372
+ }
2373
+ loadLocalConfig() {
2374
+ try {
2375
+ const raw = localStorage.getItem(`files-upload-config:${this.componentId}`);
2376
+ return raw ? JSON.parse(raw) : null;
2377
+ }
2378
+ catch {
2379
+ return null;
2380
+ }
2381
+ }
2382
+ saveLocalConfig(cfg) {
2383
+ try {
2384
+ localStorage.setItem(`files-upload-config:${this.componentId}`, JSON.stringify(cfg));
2385
+ }
2386
+ catch { }
2387
+ }
2388
+ onDragOver(evt) {
2389
+ evt.preventDefault();
2390
+ this.isDragging = true;
2391
+ }
2392
+ onDragLeave(_evt) {
2393
+ this.isDragging = false;
2394
+ }
2395
+ onDrop(evt) {
2396
+ evt.preventDefault();
2397
+ this.isDragging = false;
2398
+ this.hideProximityOverlay();
2399
+ if (evt.dataTransfer?.files?.length) {
2400
+ const files = Array.from(evt.dataTransfer.files);
2401
+ if (this.config?.ui?.manualUpload) {
2402
+ this.currentFiles = files;
2403
+ this.manageSelectedOverlay();
2404
+ this.pendingFilesChange.emit(this.currentFiles);
2405
+ }
2406
+ else {
2407
+ this.startUpload(files);
2408
+ }
2409
+ }
2410
+ }
2411
+ onFilesSelected(evt) {
2412
+ const input = evt.target;
2413
+ if (input.files?.length) {
2414
+ const files = Array.from(input.files);
2415
+ if (this.config?.ui?.manualUpload) {
2416
+ this.currentFiles = files;
2417
+ this.manageSelectedOverlay();
2418
+ this.pendingFilesChange.emit(this.currentFiles);
2419
+ }
2420
+ else {
2421
+ this.startUpload(files);
2422
+ }
2423
+ input.value = '';
2424
+ }
2425
+ }
2426
+ triggerUpload() {
2427
+ if (this.currentFiles.length) {
2428
+ this.hideSelectedOverlay();
2429
+ this.startUpload(this.currentFiles);
2430
+ }
2431
+ }
2432
+ clearSelection() {
2433
+ this.currentFiles = [];
2434
+ this.pendingFilesChange.emit(this.currentFiles);
2435
+ this.errorMessage = '';
2436
+ this.hideSelectedOverlay();
2437
+ }
2438
+ // Remoção individual dentro do overlay de seleção
2439
+ removePendingFile(file) {
2440
+ this.currentFiles = this.currentFiles.filter((f) => f !== file);
2441
+ this.pendingFilesChange.emit(this.currentFiles);
2442
+ this.manageSelectedOverlay();
2443
+ }
2444
+ startUpload(files) {
2445
+ if (!this.baseUrl ||
2446
+ (this.quotaExceeded && this.config?.quotas?.blockOnExceed)) {
2447
+ return;
2448
+ }
2449
+ if (!this.api) {
2450
+ this.errorMessage = this.t('serviceUnavailable', 'Serviço de upload indisponível.');
2451
+ return;
2452
+ }
2453
+ this.errorMessage = '';
2454
+ // Limpa resultados anteriores
2455
+ this.lastResults = null;
2456
+ this.lastStats = null;
2457
+ if (!this.validateFiles(files)) {
2458
+ return;
2459
+ }
2460
+ this.uploading = true;
2461
+ this.uploadProgress = 0;
2462
+ this.rateLimit = null;
2463
+ this.uploadedFiles = [];
2464
+ this.currentFiles = files;
2465
+ this.hideSelectedOverlay();
2466
+ let metadataObj;
2467
+ if (this.config?.ui?.showMetadataForm && this.metadata.value) {
2468
+ try {
2469
+ metadataObj = JSON.parse(this.metadata.value);
2470
+ }
2471
+ catch {
2472
+ const map = this.config?.messages?.errors || {};
2473
+ this.errorMessage =
2474
+ map['metadata'] || this.t('invalidMetadata', 'Metadados inválidos.');
2475
+ this.error.emit({});
2476
+ this.uploading = false;
2477
+ return;
2478
+ }
2479
+ }
2480
+ const optionsJson = this.buildOptionsJson(metadataObj);
2481
+ const strategy = this.config?.strategy ?? 'direct';
2482
+ if (files.length > 1 && this.allowMultiple) {
2483
+ const policy = this.conflictPolicy.value || 'RENAME';
2484
+ const bulkFiles = files.map((f) => ({
2485
+ file: f,
2486
+ metadata: metadataObj,
2487
+ conflictPolicy: policy,
2488
+ }));
2489
+ this.api.bulkUpload(this.baseUrl, bulkFiles, {
2490
+ optionsJson,
2491
+ failFastMode: this.config?.limits?.failFast,
2492
+ retryCount: this.config?.bulk?.retryCount,
2493
+ }).subscribe({
2494
+ next: (event) => {
2495
+ if (event.type === HttpEventType.Response) {
2496
+ const resp = event.body;
2497
+ this.bulkComplete.emit(resp);
2498
+ // Preenche estados ricos
2499
+ this.lastResults = resp?.results ?? null;
2500
+ this.lastStats = resp?.stats ?? null;
2501
+ this.finishUpload(true, undefined, resp.results);
2502
+ }
2503
+ else {
2504
+ this.handleProgress(event);
2505
+ }
2506
+ },
2507
+ error: (err) => this.handleError(err),
2508
+ });
2509
+ }
2510
+ else if (strategy === 'presign' || strategy === 'auto') {
2511
+ const file = files[0];
2512
+ if (!this.presign) {
2513
+ // Fallback para envio direto
2514
+ this.directUpload(file, {
2515
+ optionsJson,
2516
+ retryCount: this.config?.bulk?.retryCount,
2517
+ });
2518
+ return;
2519
+ }
2520
+ this.api.presign(this.baseUrl, file.name).subscribe({
2521
+ next: (target) => {
2522
+ this.presign.upload(target, file, this.config?.bulk?.retryCount ?? 0).subscribe({
2523
+ next: (event) => {
2524
+ if (event.type === HttpEventType.Response) {
2525
+ this.finishUpload(true, {
2526
+ id: '',
2527
+ fileName: file.name,
2528
+ contentType: file.type,
2529
+ fileSize: file.size,
2530
+ uploadedAt: new Date().toISOString(),
2531
+ tenantId: '',
2532
+ scanStatus: ScanStatus.PENDING,
2533
+ });
2534
+ }
2535
+ else {
2536
+ this.handleProgress(event);
2537
+ }
2538
+ },
2539
+ error: (err) => {
2540
+ if (strategy === 'auto') {
2541
+ this.directUpload(file, {
2542
+ optionsJson,
2543
+ retryCount: this.config?.bulk?.retryCount,
2544
+ });
2545
+ }
2546
+ else {
2547
+ this.handleError(err);
2548
+ }
2549
+ },
2550
+ });
2551
+ },
2552
+ error: (err) => {
2553
+ if (strategy === 'auto') {
2554
+ this.directUpload(file, {
2555
+ optionsJson,
2556
+ retryCount: this.config?.bulk?.retryCount,
2557
+ });
2558
+ }
2559
+ else {
2560
+ this.handleError(err);
2561
+ }
2562
+ },
2563
+ });
2564
+ }
2565
+ else {
2566
+ const file = files[0];
2567
+ this.directUpload(file, {
2568
+ optionsJson,
2569
+ retryCount: this.config?.bulk?.retryCount,
2570
+ metadata: metadataObj,
2571
+ conflictPolicy: this.conflictPolicy.value || 'RENAME',
2572
+ });
2573
+ }
2574
+ }
2575
+ directUpload(file, options) {
2576
+ this.api.upload(this.baseUrl, file, options).subscribe({
2577
+ next: (event) => {
2578
+ if (event.type === HttpEventType.Response) {
2579
+ const resp = event.body;
2580
+ this.finishUpload(true, resp.file);
2581
+ }
2582
+ else {
2583
+ this.handleProgress(event);
2584
+ }
2585
+ },
2586
+ error: (err) => this.handleError(err),
2587
+ });
2588
+ }
2589
+ buildOptionsJson(customMeta) {
2590
+ const effective = this.serverEffective?.options;
2591
+ const nameConflictPolicy = this.conflictPolicy.value ||
2592
+ this.config?.limits?.defaultConflictPolicy ||
2593
+ effective?.nameConflictPolicy ||
2594
+ 'RENAME';
2595
+ const limits = this.config?.limits ?? {};
2596
+ const advanced = this.config?.options ?? {};
2597
+ // Garantir maxUploadSizeMb positivo usando fallback do servidor ou default 50
2598
+ const maxUploadSizeMb = typeof limits.maxUploadSizeMb === 'number'
2599
+ ? limits.maxUploadSizeMb
2600
+ : typeof effective?.maxUploadSizeMb === 'number'
2601
+ ? effective?.maxUploadSizeMb
2602
+ : 50;
2603
+ const options = { nameConflictPolicy };
2604
+ if (typeof maxUploadSizeMb === 'number' && maxUploadSizeMb > 0) {
2605
+ options.maxUploadSizeMb = maxUploadSizeMb;
2606
+ }
2607
+ // strictValidation: fallback para server
2608
+ if (typeof limits.strictValidation === 'boolean') {
2609
+ options.strictValidation = limits.strictValidation;
2610
+ }
2611
+ else if (typeof effective?.strictValidation === 'boolean') {
2612
+ options.strictValidation = effective.strictValidation;
2613
+ }
2614
+ // Arrays/strings opcionais: usar config ou fallback do servidor quando disponível
2615
+ const allowed = advanced.allowedExtensions?.length
2616
+ ? advanced.allowedExtensions
2617
+ : effective?.allowedExtensions && effective.allowedExtensions.length
2618
+ ? effective.allowedExtensions
2619
+ : undefined;
2620
+ if (allowed)
2621
+ options.allowedExtensions = allowed;
2622
+ const mimes = advanced.acceptMimeTypes?.length
2623
+ ? advanced.acceptMimeTypes
2624
+ : effective?.acceptMimeTypes && effective.acceptMimeTypes.length
2625
+ ? effective.acceptMimeTypes
2626
+ : undefined;
2627
+ if (mimes)
2628
+ options.acceptMimeTypes = mimes;
2629
+ const targetDir = advanced.targetDirectory ?? effective?.targetDirectory;
2630
+ if (targetDir)
2631
+ options.targetDirectory = targetDir;
2632
+ if (typeof advanced.enableVirusScanning === 'boolean') {
2633
+ options.enableVirusScanning = advanced.enableVirusScanning;
2634
+ }
2635
+ else if (typeof effective?.enableVirusScanning === 'boolean') {
2636
+ options.enableVirusScanning = effective.enableVirusScanning;
2637
+ }
2638
+ // Mapear metadados para Map<String,String>
2639
+ if (customMeta && typeof customMeta === 'object') {
2640
+ const mapped = {};
2641
+ for (const [k, v] of Object.entries(customMeta)) {
2642
+ if (v !== undefined && v !== null)
2643
+ mapped[k] = String(v);
2644
+ }
2645
+ if (Object.keys(mapped).length)
2646
+ options.customMetadata = mapped;
2647
+ }
2648
+ try {
2649
+ return JSON.stringify(options);
2650
+ }
2651
+ catch {
2652
+ return undefined;
2653
+ }
2654
+ }
2655
+ handleProgress(event) {
2656
+ if (event.type === HttpEventType.UploadProgress && event.total) {
2657
+ this.uploadProgress = Math.round((event.loaded / event.total) * 100);
2658
+ }
2659
+ }
2660
+ handleError(err) {
2661
+ this.uploading = false;
2662
+ if (this.currentFiles.length) {
2663
+ this.uploadedFiles = this.currentFiles.map((f) => ({
2664
+ fileName: f.name,
2665
+ success: false,
2666
+ }));
2667
+ this.currentFiles = [];
2668
+ }
2669
+ if (err instanceof HttpErrorResponse && err.error) {
2670
+ const mapped = this.errors?.map(err.error, err.headers) ||
2671
+ err.error;
2672
+ this.errorMessage =
2673
+ mapped.message ||
2674
+ this.t('genericUploadError', 'Erro no envio de arquivo.');
2675
+ if (mapped.rateLimit) {
2676
+ this.rateLimit = mapped.rateLimit;
2677
+ this.rateLimited.emit(mapped.rateLimit);
2678
+ }
2679
+ const code = String(err.error.code || '');
2680
+ if (code === 'QUOTA_EXCEEDED' || code === 'COTA_EXCEDIDA') {
2681
+ this.quotaExceeded = true;
2682
+ }
2683
+ this.error.emit(err.error);
2684
+ }
2685
+ else {
2686
+ this.errorMessage = this.t('genericUploadError', 'Erro no envio de arquivo.');
2687
+ this.error.emit(err);
2688
+ }
2689
+ }
2690
+ finishUpload(success, meta, results) {
2691
+ this.uploading = false;
2692
+ this.uploadProgress = 100;
2693
+ if (success) {
2694
+ if (meta) {
2695
+ this.uploadedFiles.push({ fileName: meta.fileName, success: true });
2696
+ this.uploadSuccess.emit({ file: meta });
2697
+ // Opcional: preencher lista rica também para single
2698
+ this.lastResults = [
2699
+ {
2700
+ fileName: meta.fileName,
2701
+ status: BulkUploadResultStatus.SUCCESS,
2702
+ file: meta,
2703
+ },
2704
+ ];
2705
+ this.lastStats = { total: 1, succeeded: 1, failed: 0 };
2706
+ }
2707
+ if (results) {
2708
+ // Mantém lista simples para compatibilidade
2709
+ for (const r of results) {
2710
+ if (r.status === BulkUploadResultStatus.SUCCESS) {
2711
+ this.uploadedFiles.push({ fileName: r.fileName, success: true });
2712
+ }
2713
+ else {
2714
+ this.uploadedFiles.push({ fileName: r.fileName, success: false });
2715
+ }
2716
+ }
2717
+ // Se stats não vieram do servidor, calcula minimamente
2718
+ if (!this.lastStats) {
2719
+ const succeeded = results.filter((r) => r.status === BulkUploadResultStatus.SUCCESS).length;
2720
+ const failed = results.length - succeeded;
2721
+ this.lastStats = { total: results.length, succeeded, failed };
2722
+ }
2723
+ if (!this.lastResults) {
2724
+ this.lastResults = results;
2725
+ }
2726
+ }
2727
+ }
2728
+ else {
2729
+ this.uploadedFiles = this.currentFiles.map((f) => ({
2730
+ fileName: f.name,
2731
+ success: false,
2732
+ }));
2733
+ }
2734
+ this.currentFiles = [];
2735
+ this.hideSelectedOverlay();
2736
+ this.cdr.markForCheck();
2737
+ }
2738
+ // Lista rica - helpers/ações
2739
+ get visibleResults() {
2740
+ if (!this.lastResults)
2741
+ return [];
2742
+ const cap = this.config?.ui?.list?.collapseAfter ?? 0;
2743
+ if (this.showAllResults || cap <= 0)
2744
+ return this.lastResults;
2745
+ return this.lastResults.slice(0, cap);
2746
+ }
2747
+ // Função utilitária para conversão
2748
+ fileToBulkUploadFileResult(file) {
2749
+ return {
2750
+ fileName: file.name,
2751
+ status: BulkUploadResultStatus.FAILED,
2752
+ file: {
2753
+ id: '',
2754
+ fileName: file.name,
2755
+ contentType: file.type,
2756
+ fileSize: file.size,
2757
+ uploadedAt: new Date().toISOString(),
2758
+ tenantId: '',
2759
+ scanStatus: ScanStatus.PENDING,
2760
+ metadata: {}
2761
+ }
2762
+ };
2763
+ }
2764
+ onRetry(file) {
2765
+ const bulkFile = 'fileName' in file && 'status' in file ? file : this.fileToBulkUploadFileResult(file);
2766
+ this.retry.emit(bulkFile);
2767
+ }
2768
+ onDownload(file) {
2769
+ const bulkFile = 'fileName' in file && 'status' in file ? file : this.fileToBulkUploadFileResult(file);
2770
+ this.download.emit(bulkFile);
2771
+ }
2772
+ onCopyLink(file) {
2773
+ const bulkFile = 'fileName' in file && 'status' in file ? file : this.fileToBulkUploadFileResult(file);
2774
+ this.copyLink.emit(bulkFile);
2775
+ }
2776
+ removeResult(item) {
2777
+ if (!this.lastResults)
2778
+ return;
2779
+ const idx = this.lastResults.indexOf(item);
2780
+ if (idx >= 0)
2781
+ this.lastResults = this.lastResults.filter((_, i) => i !== idx);
2782
+ // Atualiza stats mínimas
2783
+ if (this.lastStats) {
2784
+ this.lastStats.total = Math.max(0, this.lastStats.total - 1);
2785
+ if (item.status === BulkUploadResultStatus.SUCCESS)
2786
+ this.lastStats.succeeded = Math.max(0, this.lastStats.succeeded - 1);
2787
+ if (item.status === BulkUploadResultStatus.FAILED)
2788
+ this.lastStats.failed = Math.max(0, this.lastStats.failed - 1);
2789
+ }
2790
+ }
2791
+ toggleDetailsFor(item) {
2792
+ // Emite aberto/fechado com base em presença de seção extra
2793
+ // Como usamos <details>, delegamos ao usuário abrir via UI.
2794
+ // Aqui apenas emitimos um evento de intenção.
2795
+ this.detailsOpened.emit(item);
2796
+ }
2797
+ onDetailsToggle(ev, item) {
2798
+ const opened = ev.target?.open;
2799
+ if (opened)
2800
+ this.detailsOpened.emit(item);
2801
+ else
2802
+ this.detailsClosed.emit(item);
2803
+ }
2804
+ validateFiles(files) {
2805
+ const validators = [];
2806
+ if (this.config?.ui?.accept?.length) {
2807
+ validators.push(acceptValidator(this.config.ui.accept));
2808
+ }
2809
+ if (this.config?.limits?.maxFileSizeBytes) {
2810
+ validators.push(maxFileSizeValidator(this.config.limits.maxFileSizeBytes));
2811
+ }
2812
+ if (this.config?.limits?.maxFilesPerBulk) {
2813
+ validators.push(maxFilesPerBulkValidator(this.config.limits.maxFilesPerBulk));
2814
+ }
2815
+ if (this.config?.limits?.maxBulkSizeBytes) {
2816
+ validators.push(maxBulkSizeValidator(this.config.limits.maxBulkSizeBytes));
2817
+ }
2818
+ if (!validators.length) {
2819
+ return true;
2820
+ }
2821
+ const control = new FormControl(files, validators);
2822
+ if (control.invalid) {
2823
+ const errors = control.errors || {};
2824
+ const map = this.config?.messages?.errors || {};
2825
+ if (errors['accept']) {
2826
+ this.errorMessage =
2827
+ map['accept'] ||
2828
+ this.t('acceptError', 'Tipo de arquivo não permitido.');
2829
+ }
2830
+ else if (errors['maxFileSize']) {
2831
+ this.errorMessage =
2832
+ map['maxFileSize'] ||
2833
+ this.t('maxFileSizeError', 'Arquivo excede o tamanho máximo.');
2834
+ }
2835
+ else if (errors['maxFilesPerBulk']) {
2836
+ this.errorMessage =
2837
+ map['maxFilesPerBulk'] ||
2838
+ this.t('maxFilesPerBulkError', 'Quantidade de arquivos excedida.');
2839
+ }
2840
+ else if (errors['maxBulkSize']) {
2841
+ this.errorMessage =
2842
+ map['maxBulkSize'] ||
2843
+ this.t('maxBulkSizeError', 'Tamanho total dos arquivos excedido.');
2844
+ }
2845
+ this.error.emit(errors);
2846
+ return false;
2847
+ }
2848
+ return true;
2849
+ }
2850
+ // Resumo de políticas para tooltip do ícone de informações
2851
+ get policiesSummary() {
2852
+ const acc = this.config?.ui?.accept?.length
2853
+ ? this.config.ui.accept.join(', ')
2854
+ : this.acceptString || 'qualquer';
2855
+ const maxFile = this.config?.limits?.maxFileSizeBytes
2856
+ ? this.formatBytes(this.config.limits.maxFileSizeBytes)
2857
+ : '—';
2858
+ const maxCount = this.config?.limits?.maxFilesPerBulk ?? (this.allowMultiple ? 99 : 1);
2859
+ return `Tipos: ${acc} • Máx. por arquivo: ${maxFile} • Qtde: ${maxCount}`;
2860
+ }
2861
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisFilesUpload, deps: [{ token: i1$2.SettingsPanelService }, { token: DragProximityService }, { token: FILES_UPLOAD_TEXTS }, { token: i3.Overlay }, { token: i0.ViewContainerRef }, { token: i0.ChangeDetectorRef }, { token: FilesApiClient, optional: true }, { token: PresignedUploaderService, optional: true }, { token: ErrorMapperService, optional: true }, { token: TRANSLATE_LIKE, optional: true }, { token: CONFIG_STORAGE, optional: true }], target: i0.ɵɵFactoryTarget.Component });
2862
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: PraxisFilesUpload, isStandalone: true, selector: "praxis-files-upload", inputs: { config: "config", componentId: "componentId", baseUrl: "baseUrl", displayMode: "displayMode" }, outputs: { uploadSuccess: "uploadSuccess", bulkComplete: "bulkComplete", error: "error", rateLimited: "rateLimited", retry: "retry", download: "download", copyLink: "copyLink", detailsOpened: "detailsOpened", detailsClosed: "detailsClosed", pendingFilesChange: "pendingFilesChange", proximityChange: "proximityChange" }, viewQueries: [{ propertyName: "fileInput", first: true, predicate: ["fileInput"], descendants: true }, { propertyName: "hostRef", first: true, predicate: ["host"], descendants: true, static: true }, { propertyName: "overlayTmpl", first: true, predicate: ["proximityOverlayTmpl"], descendants: true }, { propertyName: "selectedOverlayTmpl", first: true, predicate: ["selectedOverlayTmpl"], descendants: true }], ngImport: i0, template: `
2863
+ <div class="praxis-files-upload" [class.dense]="config?.ui?.dense" #host>
2864
+ <!-- Proximity overlay inline (fallback quando expandMode === 'inline') -->
2865
+ <div
2866
+ class="proximity-overlay"
2867
+ *ngIf="
2868
+ showProximityOverlay &&
2869
+ (config?.ui?.dropzone?.expandMode ?? 'overlay') === 'inline'
2870
+ "
2871
+ [style.height.px]="config?.ui?.dropzone?.expandHeight ?? 200"
2872
+ aria-hidden="true"
2873
+ >
2874
+ <mat-icon class="dz-icon" aria-hidden="true" [praxisIcon]="'cloud_upload'"></mat-icon>
2875
+ <p class="dz-title">{{ texts.dropzoneLabel }}</p>
2876
+ <p class="dz-hint">Solte aqui para enviar</p>
2877
+ </div>
2878
+ <!-- Template do overlay de proximidade (renderizado via CDK Overlay) -->
2879
+ <ng-template #proximityOverlayTmpl>
2880
+ <div class="proximity-overlay" aria-hidden="true">
2881
+ <mat-icon class="dz-icon" aria-hidden="true" [praxisIcon]="'cloud_upload'"></mat-icon>
2882
+ <p class="dz-title">{{ texts.dropzoneLabel }}</p>
2883
+ <p class="dz-hint">Solte aqui para enviar</p>
2884
+ </div>
2885
+ </ng-template>
2886
+
2887
+ <!-- Overlay de seleção (lista de arquivos selecionados + ações) -->
2888
+ <ng-container *ngIf="displayMode === 'full'">
2889
+ <ng-template #selectedOverlayTmpl>
2890
+ <div
2891
+ class="selected-overlay"
2892
+ role="dialog"
2893
+ aria-label="Arquivos selecionados"
2894
+ >
2895
+ <header class="sel-header">
2896
+ <span class="title">Selecionados ({{ pendingCount }})</span>
2897
+ <span class="spacer"></span>
2898
+ <button
2899
+ mat-stroked-button
2900
+ color="primary"
2901
+ (click)="triggerUpload()"
2902
+ [disabled]="!baseUrl || isUploading"
2903
+ >
2904
+ Enviar
2905
+ </button>
2906
+ <button mat-button type="button" (click)="clearSelection()">
2907
+ Cancelar
2908
+ </button>
2909
+ </header>
2910
+ <ul class="sel-list">
2911
+ <li *ngFor="let f of pendingFiles" class="sel-item">
2912
+ <mat-icon class="file-icon" aria-hidden="true"
2913
+ >insert_drive_file</mat-icon
2914
+ >
2915
+ <div class="info">
2916
+ <div class="name" [matTooltip]="displayFileName(f)">
2917
+ {{ displayFileName(f) }}
2918
+ </div>
2919
+ <div class="meta">
2920
+ {{ f.type || '—' }} • {{ formatBytes(f.size) }}
2921
+ </div>
2922
+ </div>
2923
+ <button
2924
+ mat-icon-button
2925
+ class="remove-btn"
2926
+ [matTooltip]="t('remove', 'Remover')"
2927
+ (click)="removePendingFile(f)"
2928
+ [attr.aria-label]="t('remove', 'Remover')"
2929
+ >
2930
+ <mat-icon [praxisIcon]="'close'"></mat-icon>
2931
+ </button>
2932
+ <button
2933
+ mat-icon-button
2934
+ class="more-btn"
2935
+ [matMenuTriggerFor]="fileMenu"
2936
+ [matMenuTriggerData]="{ file: f }"
2937
+ aria-haspopup="menu"
2938
+ [matTooltip]="t('moreActions', 'Mais ações')"
2939
+ [attr.aria-label]="t('moreActions', 'Mais ações')"
2940
+ >
2941
+ <mat-icon [praxisIcon]="'more_horiz'"></mat-icon>
2942
+ </button>
2943
+ <mat-menu
2944
+ #fileMenu="matMenu"
2945
+ xPosition="before"
2946
+ yPosition="below"
2947
+ >
2948
+ <button mat-menu-item (click)="onRetry(f)">
2949
+ <mat-icon [praxisIcon]="'refresh'"></mat-icon>
2950
+ <span>{{ t('retry', 'Reenviar') }}</span>
2951
+ </button>
2952
+ <button mat-menu-item (click)="onDownload(f)">
2953
+ <mat-icon [praxisIcon]="'download'"></mat-icon>
2954
+ <span>{{ t('download', 'Baixar') }}</span>
2955
+ </button>
2956
+ <button mat-menu-item (click)="onCopyLink(f)">
2957
+ <mat-icon [praxisIcon]="'link'"></mat-icon>
2958
+ <span>{{ t('copyLink', 'Copiar link') }}</span>
2959
+ </button>
2960
+ </mat-menu>
2961
+ </li>
2962
+ </ul>
2963
+ </div>
2964
+ </ng-template>
2965
+ </ng-container>
2966
+
2967
+ <button
2968
+ type="button"
2969
+ mat-icon-button
2970
+ class="settings-btn"
2971
+ (click)="openSettings()"
2972
+ [attr.aria-label]="texts.settingsAriaLabel"
2973
+ >
2974
+ <mat-icon [praxisIcon]="'settings'"></mat-icon>
2975
+ </button>
2976
+ <div
2977
+ class="rate-limit-banner"
2978
+ *ngIf="rateLimit && config?.rateLimit?.showBannerOn429 !== false"
2979
+ >
2980
+ {{ texts.rateLimitBanner }}
2981
+ {{ rateLimit.resetEpochSeconds * 1000 | date: 'shortTime' }}.
2982
+ </div>
2983
+ <div
2984
+ class="quota-banner"
2985
+ *ngIf="quotaExceeded && config?.quotas?.showQuotaWarnings !== false"
2986
+ >
2987
+ {{ errorMessage }}
2988
+ </div>
2989
+ <div class="error" *ngIf="errorMessage && !quotaExceeded">
2990
+ {{ errorMessage }}
2991
+ </div>
2992
+ <div class="error" *ngIf="!baseUrl">
2993
+ Base URL não configurada. Informe [baseUrl] para habilitar o envio.
2994
+ </div>
2995
+ <div
2996
+ class="dropzone"
2997
+ *ngIf="config?.ui?.showDropzone !== false"
2998
+ [attr.role]="
2999
+ !baseUrl ||
3000
+ (quotaExceeded && config?.quotas?.blockOnExceed) ||
3001
+ isUploading
3002
+ ? null
3003
+ : 'button'
3004
+ "
3005
+ [attr.tabindex]="
3006
+ !baseUrl ||
3007
+ (quotaExceeded && config?.quotas?.blockOnExceed) ||
3008
+ isUploading
3009
+ ? -1
3010
+ : 0
3011
+ "
3012
+ [class.dragover]="isDragging"
3013
+ [class.disabled]="
3014
+ !baseUrl ||
3015
+ (quotaExceeded && config?.quotas?.blockOnExceed) ||
3016
+ isUploading
3017
+ "
3018
+ [attr.aria-disabled]="
3019
+ !baseUrl ||
3020
+ (quotaExceeded && config?.quotas?.blockOnExceed) ||
3021
+ isUploading
3022
+ ? 'true'
3023
+ : 'false'
3024
+ "
3025
+ [attr.aria-label]="texts.dropzoneLabel + ' ' + texts.dropzoneButton"
3026
+ (click)="
3027
+ !baseUrl ||
3028
+ (quotaExceeded && config?.quotas?.blockOnExceed) ||
3029
+ isUploading
3030
+ ? null
3031
+ : openFile(fileInput)
3032
+ "
3033
+ (drop)="onDrop($event)"
3034
+ (dragover)="onDragOver($event)"
3035
+ (dragleave)="onDragLeave($event)"
3036
+ (keydown.enter)="
3037
+ $event.preventDefault();
3038
+ !baseUrl ||
3039
+ (quotaExceeded && config?.quotas?.blockOnExceed) ||
3040
+ isUploading
3041
+ ? null
3042
+ : openFile(fileInput)
3043
+ "
3044
+ (keydown.space)="
3045
+ $event.preventDefault();
3046
+ !baseUrl ||
3047
+ (quotaExceeded && config?.quotas?.blockOnExceed) ||
3048
+ isUploading
3049
+ ? null
3050
+ : openFile(fileInput)
3051
+ "
3052
+ >
3053
+ <!-- Ações internas (ícones) -->
3054
+ <div class="field-actions" aria-hidden="false">
3055
+ <button
3056
+ mat-icon-button
3057
+ [matTooltip]="'Selecionar arquivo(s)'"
3058
+ (click)="$event.stopPropagation(); openFile(fileInput)"
3059
+ [disabled]="!baseUrl || isUploading"
3060
+ >
3061
+ <mat-icon [praxisIcon]="'attach_file'"></mat-icon>
3062
+ </button>
3063
+ <button
3064
+ mat-icon-button
3065
+ [matTooltip]="policiesSummary"
3066
+ (click)="$event.stopPropagation()"
3067
+ [disabled]="false"
3068
+ >
3069
+ <mat-icon [praxisIcon]="'info'"></mat-icon>
3070
+ </button>
3071
+ </div>
3072
+
3073
+ <mat-icon class="dz-icon" aria-hidden="true" [praxisIcon]="'cloud_upload'"></mat-icon>
3074
+ <p class="dz-title">{{ texts.dropzoneLabel }}</p>
3075
+ <p class="dz-hint">
3076
+ Arraste e solte arquivos aqui ou clique para selecionar
3077
+ </p>
3078
+ <button type="button" mat-stroked-button (click)="openFile(fileInput)">
3079
+ {{ texts.dropzoneButton }}
3080
+ </button>
3081
+ </div>
3082
+
3083
+ <input
3084
+ type="file"
3085
+ #fileInput
3086
+ (change)="onFilesSelected($event)"
3087
+ [attr.multiple]="allowMultiple ? '' : null"
3088
+ [accept]="acceptString"
3089
+ hidden
3090
+ />
3091
+
3092
+ <!-- Removido bloco pending-files: substituído por overlay de seleção -->
3093
+
3094
+ <div
3095
+ class="conflict-policy"
3096
+ *ngIf="config?.ui?.showConflictPolicySelector"
3097
+ >
3098
+ <label for="conflictPolicy">{{ texts.conflictPolicyLabel }}</label>
3099
+ <select id="conflictPolicy" [formControl]="conflictPolicy">
3100
+ <option *ngFor="let p of conflictPolicies" [value]="p">
3101
+ {{ p }}
3102
+ </option>
3103
+ </select>
3104
+ </div>
3105
+
3106
+ <div class="metadata" *ngIf="config?.ui?.showMetadataForm">
3107
+ <label for="metadata">{{ texts.metadataLabel }}</label>
3108
+ <textarea id="metadata" [formControl]="metadata"></textarea>
3109
+ <div class="error" *ngIf="metadataError">{{ metadataError }}</div>
3110
+ </div>
3111
+
3112
+ <div class="progress" *ngIf="showProgress">
3113
+ <mat-progress-bar
3114
+ mode="determinate"
3115
+ [value]="uploadProgress"
3116
+ [attr.aria-label]="texts.progressAriaLabel"
3117
+ ></mat-progress-bar>
3118
+ <span class="sr-only">{{ uploadProgress }}%</span>
3119
+ </div>
3120
+
3121
+ <ul class="file-feedback" *ngIf="hasUploadedFiles">
3122
+ <li *ngFor="let f of uploadedFiles">
3123
+ {{ f.fileName }} -
3124
+ {{
3125
+ f.success ? t('statusSuccess', 'Enviado') : t('statusError', 'Erro')
3126
+ }}
3127
+ </li>
3128
+ </ul>
3129
+
3130
+ <section class="bulk-results" *ngIf="hasLastResults">
3131
+ <header class="bulk-summary" *ngIf="lastStats">
3132
+ <div class="stat">
3133
+ <span class="label">Total</span>
3134
+ <span class="value">{{ lastStats.total }}</span>
3135
+ </div>
3136
+ <div class="stat success">
3137
+ <mat-icon [praxisIcon]="'check_circle'"></mat-icon>
3138
+ <span class="label">Sucesso</span>
3139
+ <span class="value">{{ lastStats.succeeded }}</span>
3140
+ </div>
3141
+ <div class="stat failed">
3142
+ <mat-icon [praxisIcon]="'error'"></mat-icon>
3143
+ <span class="label">Falhas</span>
3144
+ <span class="value">{{ lastStats.failed }}</span>
3145
+ </div>
3146
+ </header>
3147
+ <ul class="results-list">
3148
+ <li
3149
+ class="result-item"
3150
+ *ngFor="let r of visibleResults"
3151
+ [class.ok]="r.status === BulkUploadResultStatus.SUCCESS"
3152
+ [class.err]="r.status === BulkUploadResultStatus.FAILED"
3153
+ >
3154
+ <mat-icon class="status-icon">{{
3155
+ r.status === BulkUploadResultStatus.SUCCESS
3156
+ ? 'check_circle'
3157
+ : 'error'
3158
+ }}</mat-icon>
3159
+ <div class="main">
3160
+ <div class="title">
3161
+ <span class="name">{{ r.fileName }}</span>
3162
+ <span
3163
+ class="badge"
3164
+ *ngIf="r.status === BulkUploadResultStatus.SUCCESS"
3165
+ >{{ t('statusSuccess', 'Enviado') }}</span
3166
+ >
3167
+ <span
3168
+ class="badge err"
3169
+ *ngIf="r.status === BulkUploadResultStatus.FAILED"
3170
+ >{{ t('statusError', 'Erro') }}</span
3171
+ >
3172
+ </div>
3173
+ <div class="meta" *ngIf="r.file">
3174
+ <span *ngIf="r.file.id">ID: {{ r.file.id }}</span>
3175
+ <span *ngIf="r.file.contentType"
3176
+ >Tipo: {{ r.file.contentType }}</span
3177
+ >
3178
+ <span *ngIf="r.file.fileSize"
3179
+ >Tamanho: {{ formatBytes(r.file.fileSize) }}</span
3180
+ >
3181
+ <span *ngIf="r.file.uploadedAt"
3182
+ >Enviado em: {{ r.file.uploadedAt | date: 'short' }}</span
3183
+ >
3184
+ </div>
3185
+ <div class="error" *ngIf="r.error as e">
3186
+ <span class="code">{{ e.code }}</span>
3187
+ <ng-container *ngIf="mapErrorFull(e) as me">
3188
+ <span class="title" *ngIf="me.title">{{ me.title }}:</span>
3189
+ <span class="msg">{{ me.message }}</span>
3190
+ <span class="action" *ngIf="me.userAction">
3191
+ — {{ me.userAction }}</span
3192
+ >
3193
+ </ng-container>
3194
+ <span class="trace" *ngIf="e.traceId"
3195
+ >(trace {{ e.traceId }})</span
3196
+ >
3197
+ </div>
3198
+ <div class="extra" *ngIf="r.file?.metadata as m">
3199
+ <details (toggle)="onDetailsToggle($event, r)">
3200
+ <summary>Metadados</summary>
3201
+ <pre>{{ m | json }}</pre>
3202
+ </details>
3203
+ </div>
3204
+ </div>
3205
+ <div class="item-actions">
3206
+ <button
3207
+ mat-icon-button
3208
+ [matTooltip]="'Detalhes'"
3209
+ (click)="toggleDetailsFor(r)"
3210
+ >
3211
+ <mat-icon [praxisIcon]="'info'"></mat-icon>
3212
+ </button>
3213
+ <button
3214
+ mat-icon-button
3215
+ [matTooltip]="'Reenviar'"
3216
+ (click)="onRetry(r)"
3217
+ [disabled]="isUploading"
3218
+ >
3219
+ <mat-icon [praxisIcon]="'refresh'"></mat-icon>
3220
+ </button>
3221
+ <button
3222
+ mat-icon-button
3223
+ [matTooltip]="'Baixar'"
3224
+ (click)="onDownload(r)"
3225
+ [disabled]="r.status !== BulkUploadResultStatus.SUCCESS"
3226
+ >
3227
+ <mat-icon [praxisIcon]="'download'"></mat-icon>
3228
+ </button>
3229
+ <button
3230
+ mat-icon-button
3231
+ [matTooltip]="'Copiar link'"
3232
+ (click)="onCopyLink(r)"
3233
+ [disabled]="r.status !== BulkUploadResultStatus.SUCCESS"
3234
+ >
3235
+ <mat-icon [praxisIcon]="'link'"></mat-icon>
3236
+ </button>
3237
+ <button
3238
+ mat-icon-button
3239
+ [matTooltip]="'Remover'"
3240
+ (click)="removeResult(r)"
3241
+ >
3242
+ <mat-icon [praxisIcon]="'close'"></mat-icon>
3243
+ </button>
3244
+ </div>
3245
+ </li>
3246
+ </ul>
3247
+ <div
3248
+ class="list-footer"
3249
+ *ngIf="
3250
+ !showAllResults &&
3251
+ lastResults &&
3252
+ lastResults.length > (config?.ui?.list?.collapseAfter ?? 0)
3253
+ "
3254
+ >
3255
+ <button mat-button type="button" (click)="showAllResults = true">
3256
+ Ver todos
3257
+ </button>
3258
+ </div>
3259
+ <div
3260
+ class="list-footer"
3261
+ *ngIf="
3262
+ showAllResults &&
3263
+ (config?.ui?.list?.collapseAfter ?? 0) > 0 &&
3264
+ lastResults &&
3265
+ lastResults.length > (config?.ui?.list?.collapseAfter ?? 0)
3266
+ "
3267
+ >
3268
+ <button mat-button type="button" (click)="showAllResults = false">
3269
+ Ver menos
3270
+ </button>
3271
+ </div>
3272
+ </section>
3273
+
3274
+ <ng-content></ng-content>
3275
+ </div>
3276
+ `, isInline: true, styles: ["@charset \"UTF-8\";.praxis-files-upload{position:relative}.praxis-files-upload .settings-btn{position:absolute;top:.5rem;right:.5rem}.praxis-files-upload .rate-limit-banner,.praxis-files-upload .quota-banner,.praxis-files-upload .error{margin:.5rem 0;padding:.5rem 1rem;border-radius:var(--mat-sys-shape-corner-small, 4px)}.praxis-files-upload .rate-limit-banner{background:var(--mat-sys-color-error-container);color:var(--mat-sys-color-on-error-container)}.praxis-files-upload .quota-banner{background:var(--mat-sys-color-tertiary-container);color:var(--mat-sys-color-on-tertiary-container)}.praxis-files-upload .dropzone{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem;border:2px dashed var(--mat-sys-color-outline);border-radius:var(--mat-sys-shape-corner-medium, 4px);background:var(--mat-sys-color-surface-container-low);color:var(--mat-sys-color-on-surface);text-align:center;cursor:pointer;transition:background-color .2s;width:100%;min-height:160px;margin:.5rem 0;gap:.5rem;position:relative}.praxis-files-upload .dropzone:focus,.praxis-files-upload .dropzone:hover{outline:none;background:var(--mat-sys-color-surface-container-high);border-color:var(--mat-sys-color-primary)}.praxis-files-upload .dropzone.dragover{background:var(--mat-sys-color-surface-container-high);border-color:var(--mat-sys-color-primary)}.praxis-files-upload .dropzone.disabled{opacity:.6;cursor:not-allowed;pointer-events:none}.praxis-files-upload .dropzone p{margin:0 0 .5rem}.praxis-files-upload .dropzone .dz-icon{font-size:40px;color:var(--mat-sys-color-primary);margin-bottom:.25rem}.praxis-files-upload .dropzone .dz-title{font-weight:600}.praxis-files-upload .dropzone .dz-hint{margin:0;opacity:.8;font-size:.9rem}.praxis-files-upload .dropzone .field-actions{position:absolute;top:.25rem;right:.25rem;display:flex;gap:.25rem;pointer-events:auto}.praxis-files-upload .dropzone .field-actions button[mat-icon-button]{width:32px;height:32px}.praxis-files-upload .dropzone .field-actions mat-icon{font-size:20px}.praxis-files-upload .progress{margin-top:1rem}.praxis-files-upload .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);border:0}.praxis-files-upload .bulk-results{margin-top:1rem}.praxis-files-upload .bulk-results .bulk-summary{display:flex;gap:1rem;align-items:center;margin-bottom:.5rem}.praxis-files-upload .bulk-results .bulk-summary .stat{display:flex;align-items:center;gap:.35rem;padding:.25rem .5rem;border-radius:6px;background:var(--mat-sys-color-surface-container-low)}.praxis-files-upload .bulk-results .bulk-summary .stat.success{color:var(--mat-sys-color-primary)}.praxis-files-upload .bulk-results .bulk-summary .stat.failed{color:var(--mat-sys-color-error)}.praxis-files-upload .bulk-results .bulk-summary .stat .label{opacity:.9}.praxis-files-upload .bulk-results .bulk-summary .stat .value{font-weight:600}.praxis-files-upload .bulk-results .results-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.5rem}.praxis-files-upload .bulk-results .result-item{display:flex;gap:.75rem;padding:.5rem .75rem;border:1px solid var(--mat-sys-color-outline-variant);border-radius:8px;background:var(--mat-sys-color-surface-container-lowest)}.praxis-files-upload .bulk-results .result-item.ok{border-color:var(--mat-sys-color-primary)}.praxis-files-upload .bulk-results .result-item.err{border-color:var(--mat-sys-color-error)}.praxis-files-upload .bulk-results .result-item .status-icon{align-self:flex-start;color:currentColor}.praxis-files-upload .bulk-results .result-item .main{flex:1;min-width:0}.praxis-files-upload .bulk-results .result-item .title{display:flex;align-items:center;gap:.5rem}.praxis-files-upload .bulk-results .result-item .name{font-weight:600}.praxis-files-upload .bulk-results .result-item .badge{font-size:.75rem;padding:.1rem .4rem;border-radius:999px;background:color-mix(in oklab,var(--mat-sys-color-primary) 15%,transparent);color:var(--mat-sys-color-primary)}.praxis-files-upload .bulk-results .result-item .badge.err{background:color-mix(in oklab,var(--mat-sys-color-error) 15%,transparent);color:var(--mat-sys-color-error)}.praxis-files-upload .bulk-results .result-item .meta,.praxis-files-upload .bulk-results .result-item .error{display:flex;flex-wrap:wrap;gap:.5rem 1rem;margin-top:.25rem}.praxis-files-upload .bulk-results .result-item .error .code{font-weight:600}.praxis-files-upload .bulk-results .result-item .extra{margin-top:.25rem}.praxis-files-upload .bulk-results .result-item .item-actions{display:flex;align-items:center;gap:.25rem}.praxis-files-upload .bulk-results .list-footer{margin-top:.5rem;display:flex;justify-content:center}.praxis-files-upload .proximity-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:2px dashed var(--mat-sys-color-outline);border-radius:var(--mat-sys-shape-corner-medium, 4px);background:color-mix(in oklab,var(--mat-sys-color-surface-container-high) 80%,transparent);color:var(--mat-sys-color-on-surface);text-align:center;z-index:2;pointer-events:none}.praxis-files-upload .proximity-overlay .dz-icon{font-size:40px;color:var(--mat-sys-color-primary)}.praxis-files-upload .proximity-overlay .dz-title{font-weight:600}.praxis-files-upload .proximity-overlay .dz-hint{opacity:.85}.praxis-files-upload .selected-overlay{background:var(--mat-sys-color-surface);color:var(--mat-sys-color-on-surface);border:1px solid var(--mat-sys-color-outline-variant);border-radius:8px;box-shadow:var(--mat-sys-level2, 0 2px 6px rgba(0, 0, 0, .15));max-height:260px;overflow:auto;padding:.5rem}.praxis-files-upload .selected-overlay .sel-header{position:sticky;top:0;z-index:1;background:var(--mat-sys-color-surface);display:flex;align-items:center;gap:.5rem;padding-bottom:.25rem;margin-bottom:.25rem}.praxis-files-upload .selected-overlay .sel-header .title{font-weight:600}.praxis-files-upload .selected-overlay .sel-header .spacer{flex:1}.praxis-files-upload .selected-overlay .sel-list{list-style:none;margin:0;padding:0}.praxis-files-upload .selected-overlay .sel-list .sel-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;border-bottom:1px solid var(--mat-sys-color-outline-variant, #ccc);transition:background-color .12s ease}.praxis-files-upload .selected-overlay .sel-list .sel-item .file-icon{opacity:.9;color:var(--mat-sys-color-primary, #1976d2)}.praxis-files-upload .selected-overlay .sel-list .sel-item .info{flex:1;min-width:0}.praxis-files-upload .selected-overlay .sel-list .sel-item .info .name{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.praxis-files-upload .selected-overlay .sel-list .sel-item .info .meta{font-size:.8rem;opacity:.7}.praxis-files-upload .selected-overlay .sel-list .sel-item .remove-btn{color:var(--mat-sys-color-error, #b00020)}.praxis-files-upload .selected-overlay .sel-list .sel-item .more-btn{margin-left:.25rem}.praxis-files-upload .selected-overlay .sel-list .sel-item:hover{background:color-mix(in oklab,#000 8%,transparent)}:host ::ng-deep .praxis-files-upload-overlay-panel{pointer-events:none}:host ::ng-deep .praxis-files-selected-overlay-panel{pointer-events:auto;z-index:3}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i7.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i7.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i1$1.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i1$1.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { 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.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i1$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i1$1.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i9.MatButton, selector: " button[matButton], a[matButton], button[mat-button], button[mat-raised-button], button[mat-flat-button], button[mat-stroked-button], a[mat-button], a[mat-raised-button], a[mat-flat-button], a[mat-stroked-button] ", inputs: ["matButton"], exportAs: ["matButton", "matAnchor"] }, { kind: "component", type: i9.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: "directive", type: PraxisIconDirective, selector: "mat-icon[praxisIcon]", inputs: ["praxisIcon"] }, { kind: "ngmodule", type: MatProgressBarModule }, { kind: "component", type: i11.MatProgressBar, selector: "mat-progress-bar", inputs: ["color", "value", "bufferValue", "mode"], outputs: ["animationEnd"], exportAs: ["matProgressBar"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i3$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i3$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i3$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i13.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }, { kind: "pipe", type: i7.JsonPipe, name: "json" }, { kind: "pipe", type: i7.DatePipe, name: "date" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
3277
+ }
3278
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PraxisFilesUpload, decorators: [{
3279
+ type: Component,
3280
+ args: [{ selector: 'praxis-files-upload', standalone: true, imports: [
3281
+ CommonModule,
3282
+ ReactiveFormsModule,
3283
+ MatButtonModule,
3284
+ MatIconModule,
3285
+ PraxisIconDirective,
3286
+ MatProgressBarModule,
3287
+ MatMenuModule,
3288
+ MatTooltipModule,
3289
+ ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
3290
+ <div class="praxis-files-upload" [class.dense]="config?.ui?.dense" #host>
3291
+ <!-- Proximity overlay inline (fallback quando expandMode === 'inline') -->
3292
+ <div
3293
+ class="proximity-overlay"
3294
+ *ngIf="
3295
+ showProximityOverlay &&
3296
+ (config?.ui?.dropzone?.expandMode ?? 'overlay') === 'inline'
3297
+ "
3298
+ [style.height.px]="config?.ui?.dropzone?.expandHeight ?? 200"
3299
+ aria-hidden="true"
3300
+ >
3301
+ <mat-icon class="dz-icon" aria-hidden="true" [praxisIcon]="'cloud_upload'"></mat-icon>
3302
+ <p class="dz-title">{{ texts.dropzoneLabel }}</p>
3303
+ <p class="dz-hint">Solte aqui para enviar</p>
3304
+ </div>
3305
+ <!-- Template do overlay de proximidade (renderizado via CDK Overlay) -->
3306
+ <ng-template #proximityOverlayTmpl>
3307
+ <div class="proximity-overlay" aria-hidden="true">
3308
+ <mat-icon class="dz-icon" aria-hidden="true" [praxisIcon]="'cloud_upload'"></mat-icon>
3309
+ <p class="dz-title">{{ texts.dropzoneLabel }}</p>
3310
+ <p class="dz-hint">Solte aqui para enviar</p>
3311
+ </div>
3312
+ </ng-template>
3313
+
3314
+ <!-- Overlay de seleção (lista de arquivos selecionados + ações) -->
3315
+ <ng-container *ngIf="displayMode === 'full'">
3316
+ <ng-template #selectedOverlayTmpl>
3317
+ <div
3318
+ class="selected-overlay"
3319
+ role="dialog"
3320
+ aria-label="Arquivos selecionados"
3321
+ >
3322
+ <header class="sel-header">
3323
+ <span class="title">Selecionados ({{ pendingCount }})</span>
3324
+ <span class="spacer"></span>
3325
+ <button
3326
+ mat-stroked-button
3327
+ color="primary"
3328
+ (click)="triggerUpload()"
3329
+ [disabled]="!baseUrl || isUploading"
3330
+ >
3331
+ Enviar
3332
+ </button>
3333
+ <button mat-button type="button" (click)="clearSelection()">
3334
+ Cancelar
3335
+ </button>
3336
+ </header>
3337
+ <ul class="sel-list">
3338
+ <li *ngFor="let f of pendingFiles" class="sel-item">
3339
+ <mat-icon class="file-icon" aria-hidden="true"
3340
+ >insert_drive_file</mat-icon
3341
+ >
3342
+ <div class="info">
3343
+ <div class="name" [matTooltip]="displayFileName(f)">
3344
+ {{ displayFileName(f) }}
3345
+ </div>
3346
+ <div class="meta">
3347
+ {{ f.type || '—' }} • {{ formatBytes(f.size) }}
3348
+ </div>
3349
+ </div>
3350
+ <button
3351
+ mat-icon-button
3352
+ class="remove-btn"
3353
+ [matTooltip]="t('remove', 'Remover')"
3354
+ (click)="removePendingFile(f)"
3355
+ [attr.aria-label]="t('remove', 'Remover')"
3356
+ >
3357
+ <mat-icon [praxisIcon]="'close'"></mat-icon>
3358
+ </button>
3359
+ <button
3360
+ mat-icon-button
3361
+ class="more-btn"
3362
+ [matMenuTriggerFor]="fileMenu"
3363
+ [matMenuTriggerData]="{ file: f }"
3364
+ aria-haspopup="menu"
3365
+ [matTooltip]="t('moreActions', 'Mais ações')"
3366
+ [attr.aria-label]="t('moreActions', 'Mais ações')"
3367
+ >
3368
+ <mat-icon [praxisIcon]="'more_horiz'"></mat-icon>
3369
+ </button>
3370
+ <mat-menu
3371
+ #fileMenu="matMenu"
3372
+ xPosition="before"
3373
+ yPosition="below"
3374
+ >
3375
+ <button mat-menu-item (click)="onRetry(f)">
3376
+ <mat-icon [praxisIcon]="'refresh'"></mat-icon>
3377
+ <span>{{ t('retry', 'Reenviar') }}</span>
3378
+ </button>
3379
+ <button mat-menu-item (click)="onDownload(f)">
3380
+ <mat-icon [praxisIcon]="'download'"></mat-icon>
3381
+ <span>{{ t('download', 'Baixar') }}</span>
3382
+ </button>
3383
+ <button mat-menu-item (click)="onCopyLink(f)">
3384
+ <mat-icon [praxisIcon]="'link'"></mat-icon>
3385
+ <span>{{ t('copyLink', 'Copiar link') }}</span>
3386
+ </button>
3387
+ </mat-menu>
3388
+ </li>
3389
+ </ul>
3390
+ </div>
3391
+ </ng-template>
3392
+ </ng-container>
3393
+
3394
+ <button
3395
+ type="button"
3396
+ mat-icon-button
3397
+ class="settings-btn"
3398
+ (click)="openSettings()"
3399
+ [attr.aria-label]="texts.settingsAriaLabel"
3400
+ >
3401
+ <mat-icon [praxisIcon]="'settings'"></mat-icon>
3402
+ </button>
3403
+ <div
3404
+ class="rate-limit-banner"
3405
+ *ngIf="rateLimit && config?.rateLimit?.showBannerOn429 !== false"
3406
+ >
3407
+ {{ texts.rateLimitBanner }}
3408
+ {{ rateLimit.resetEpochSeconds * 1000 | date: 'shortTime' }}.
3409
+ </div>
3410
+ <div
3411
+ class="quota-banner"
3412
+ *ngIf="quotaExceeded && config?.quotas?.showQuotaWarnings !== false"
3413
+ >
3414
+ {{ errorMessage }}
3415
+ </div>
3416
+ <div class="error" *ngIf="errorMessage && !quotaExceeded">
3417
+ {{ errorMessage }}
3418
+ </div>
3419
+ <div class="error" *ngIf="!baseUrl">
3420
+ Base URL não configurada. Informe [baseUrl] para habilitar o envio.
3421
+ </div>
3422
+ <div
3423
+ class="dropzone"
3424
+ *ngIf="config?.ui?.showDropzone !== false"
3425
+ [attr.role]="
3426
+ !baseUrl ||
3427
+ (quotaExceeded && config?.quotas?.blockOnExceed) ||
3428
+ isUploading
3429
+ ? null
3430
+ : 'button'
3431
+ "
3432
+ [attr.tabindex]="
3433
+ !baseUrl ||
3434
+ (quotaExceeded && config?.quotas?.blockOnExceed) ||
3435
+ isUploading
3436
+ ? -1
3437
+ : 0
3438
+ "
3439
+ [class.dragover]="isDragging"
3440
+ [class.disabled]="
3441
+ !baseUrl ||
3442
+ (quotaExceeded && config?.quotas?.blockOnExceed) ||
3443
+ isUploading
3444
+ "
3445
+ [attr.aria-disabled]="
3446
+ !baseUrl ||
3447
+ (quotaExceeded && config?.quotas?.blockOnExceed) ||
3448
+ isUploading
3449
+ ? 'true'
3450
+ : 'false'
3451
+ "
3452
+ [attr.aria-label]="texts.dropzoneLabel + ' ' + texts.dropzoneButton"
3453
+ (click)="
3454
+ !baseUrl ||
3455
+ (quotaExceeded && config?.quotas?.blockOnExceed) ||
3456
+ isUploading
3457
+ ? null
3458
+ : openFile(fileInput)
3459
+ "
3460
+ (drop)="onDrop($event)"
3461
+ (dragover)="onDragOver($event)"
3462
+ (dragleave)="onDragLeave($event)"
3463
+ (keydown.enter)="
3464
+ $event.preventDefault();
3465
+ !baseUrl ||
3466
+ (quotaExceeded && config?.quotas?.blockOnExceed) ||
3467
+ isUploading
3468
+ ? null
3469
+ : openFile(fileInput)
3470
+ "
3471
+ (keydown.space)="
3472
+ $event.preventDefault();
3473
+ !baseUrl ||
3474
+ (quotaExceeded && config?.quotas?.blockOnExceed) ||
3475
+ isUploading
3476
+ ? null
3477
+ : openFile(fileInput)
3478
+ "
3479
+ >
3480
+ <!-- Ações internas (ícones) -->
3481
+ <div class="field-actions" aria-hidden="false">
3482
+ <button
3483
+ mat-icon-button
3484
+ [matTooltip]="'Selecionar arquivo(s)'"
3485
+ (click)="$event.stopPropagation(); openFile(fileInput)"
3486
+ [disabled]="!baseUrl || isUploading"
3487
+ >
3488
+ <mat-icon [praxisIcon]="'attach_file'"></mat-icon>
3489
+ </button>
3490
+ <button
3491
+ mat-icon-button
3492
+ [matTooltip]="policiesSummary"
3493
+ (click)="$event.stopPropagation()"
3494
+ [disabled]="false"
3495
+ >
3496
+ <mat-icon [praxisIcon]="'info'"></mat-icon>
3497
+ </button>
3498
+ </div>
3499
+
3500
+ <mat-icon class="dz-icon" aria-hidden="true" [praxisIcon]="'cloud_upload'"></mat-icon>
3501
+ <p class="dz-title">{{ texts.dropzoneLabel }}</p>
3502
+ <p class="dz-hint">
3503
+ Arraste e solte arquivos aqui ou clique para selecionar
3504
+ </p>
3505
+ <button type="button" mat-stroked-button (click)="openFile(fileInput)">
3506
+ {{ texts.dropzoneButton }}
3507
+ </button>
3508
+ </div>
3509
+
3510
+ <input
3511
+ type="file"
3512
+ #fileInput
3513
+ (change)="onFilesSelected($event)"
3514
+ [attr.multiple]="allowMultiple ? '' : null"
3515
+ [accept]="acceptString"
3516
+ hidden
3517
+ />
3518
+
3519
+ <!-- Removido bloco pending-files: substituído por overlay de seleção -->
3520
+
3521
+ <div
3522
+ class="conflict-policy"
3523
+ *ngIf="config?.ui?.showConflictPolicySelector"
3524
+ >
3525
+ <label for="conflictPolicy">{{ texts.conflictPolicyLabel }}</label>
3526
+ <select id="conflictPolicy" [formControl]="conflictPolicy">
3527
+ <option *ngFor="let p of conflictPolicies" [value]="p">
3528
+ {{ p }}
3529
+ </option>
3530
+ </select>
3531
+ </div>
3532
+
3533
+ <div class="metadata" *ngIf="config?.ui?.showMetadataForm">
3534
+ <label for="metadata">{{ texts.metadataLabel }}</label>
3535
+ <textarea id="metadata" [formControl]="metadata"></textarea>
3536
+ <div class="error" *ngIf="metadataError">{{ metadataError }}</div>
3537
+ </div>
3538
+
3539
+ <div class="progress" *ngIf="showProgress">
3540
+ <mat-progress-bar
3541
+ mode="determinate"
3542
+ [value]="uploadProgress"
3543
+ [attr.aria-label]="texts.progressAriaLabel"
3544
+ ></mat-progress-bar>
3545
+ <span class="sr-only">{{ uploadProgress }}%</span>
3546
+ </div>
3547
+
3548
+ <ul class="file-feedback" *ngIf="hasUploadedFiles">
3549
+ <li *ngFor="let f of uploadedFiles">
3550
+ {{ f.fileName }} -
3551
+ {{
3552
+ f.success ? t('statusSuccess', 'Enviado') : t('statusError', 'Erro')
3553
+ }}
3554
+ </li>
3555
+ </ul>
3556
+
3557
+ <section class="bulk-results" *ngIf="hasLastResults">
3558
+ <header class="bulk-summary" *ngIf="lastStats">
3559
+ <div class="stat">
3560
+ <span class="label">Total</span>
3561
+ <span class="value">{{ lastStats.total }}</span>
3562
+ </div>
3563
+ <div class="stat success">
3564
+ <mat-icon [praxisIcon]="'check_circle'"></mat-icon>
3565
+ <span class="label">Sucesso</span>
3566
+ <span class="value">{{ lastStats.succeeded }}</span>
3567
+ </div>
3568
+ <div class="stat failed">
3569
+ <mat-icon [praxisIcon]="'error'"></mat-icon>
3570
+ <span class="label">Falhas</span>
3571
+ <span class="value">{{ lastStats.failed }}</span>
3572
+ </div>
3573
+ </header>
3574
+ <ul class="results-list">
3575
+ <li
3576
+ class="result-item"
3577
+ *ngFor="let r of visibleResults"
3578
+ [class.ok]="r.status === BulkUploadResultStatus.SUCCESS"
3579
+ [class.err]="r.status === BulkUploadResultStatus.FAILED"
3580
+ >
3581
+ <mat-icon class="status-icon">{{
3582
+ r.status === BulkUploadResultStatus.SUCCESS
3583
+ ? 'check_circle'
3584
+ : 'error'
3585
+ }}</mat-icon>
3586
+ <div class="main">
3587
+ <div class="title">
3588
+ <span class="name">{{ r.fileName }}</span>
3589
+ <span
3590
+ class="badge"
3591
+ *ngIf="r.status === BulkUploadResultStatus.SUCCESS"
3592
+ >{{ t('statusSuccess', 'Enviado') }}</span
3593
+ >
3594
+ <span
3595
+ class="badge err"
3596
+ *ngIf="r.status === BulkUploadResultStatus.FAILED"
3597
+ >{{ t('statusError', 'Erro') }}</span
3598
+ >
3599
+ </div>
3600
+ <div class="meta" *ngIf="r.file">
3601
+ <span *ngIf="r.file.id">ID: {{ r.file.id }}</span>
3602
+ <span *ngIf="r.file.contentType"
3603
+ >Tipo: {{ r.file.contentType }}</span
3604
+ >
3605
+ <span *ngIf="r.file.fileSize"
3606
+ >Tamanho: {{ formatBytes(r.file.fileSize) }}</span
3607
+ >
3608
+ <span *ngIf="r.file.uploadedAt"
3609
+ >Enviado em: {{ r.file.uploadedAt | date: 'short' }}</span
3610
+ >
3611
+ </div>
3612
+ <div class="error" *ngIf="r.error as e">
3613
+ <span class="code">{{ e.code }}</span>
3614
+ <ng-container *ngIf="mapErrorFull(e) as me">
3615
+ <span class="title" *ngIf="me.title">{{ me.title }}:</span>
3616
+ <span class="msg">{{ me.message }}</span>
3617
+ <span class="action" *ngIf="me.userAction">
3618
+ — {{ me.userAction }}</span
3619
+ >
3620
+ </ng-container>
3621
+ <span class="trace" *ngIf="e.traceId"
3622
+ >(trace {{ e.traceId }})</span
3623
+ >
3624
+ </div>
3625
+ <div class="extra" *ngIf="r.file?.metadata as m">
3626
+ <details (toggle)="onDetailsToggle($event, r)">
3627
+ <summary>Metadados</summary>
3628
+ <pre>{{ m | json }}</pre>
3629
+ </details>
3630
+ </div>
3631
+ </div>
3632
+ <div class="item-actions">
3633
+ <button
3634
+ mat-icon-button
3635
+ [matTooltip]="'Detalhes'"
3636
+ (click)="toggleDetailsFor(r)"
3637
+ >
3638
+ <mat-icon [praxisIcon]="'info'"></mat-icon>
3639
+ </button>
3640
+ <button
3641
+ mat-icon-button
3642
+ [matTooltip]="'Reenviar'"
3643
+ (click)="onRetry(r)"
3644
+ [disabled]="isUploading"
3645
+ >
3646
+ <mat-icon [praxisIcon]="'refresh'"></mat-icon>
3647
+ </button>
3648
+ <button
3649
+ mat-icon-button
3650
+ [matTooltip]="'Baixar'"
3651
+ (click)="onDownload(r)"
3652
+ [disabled]="r.status !== BulkUploadResultStatus.SUCCESS"
3653
+ >
3654
+ <mat-icon [praxisIcon]="'download'"></mat-icon>
3655
+ </button>
3656
+ <button
3657
+ mat-icon-button
3658
+ [matTooltip]="'Copiar link'"
3659
+ (click)="onCopyLink(r)"
3660
+ [disabled]="r.status !== BulkUploadResultStatus.SUCCESS"
3661
+ >
3662
+ <mat-icon [praxisIcon]="'link'"></mat-icon>
3663
+ </button>
3664
+ <button
3665
+ mat-icon-button
3666
+ [matTooltip]="'Remover'"
3667
+ (click)="removeResult(r)"
3668
+ >
3669
+ <mat-icon [praxisIcon]="'close'"></mat-icon>
3670
+ </button>
3671
+ </div>
3672
+ </li>
3673
+ </ul>
3674
+ <div
3675
+ class="list-footer"
3676
+ *ngIf="
3677
+ !showAllResults &&
3678
+ lastResults &&
3679
+ lastResults.length > (config?.ui?.list?.collapseAfter ?? 0)
3680
+ "
3681
+ >
3682
+ <button mat-button type="button" (click)="showAllResults = true">
3683
+ Ver todos
3684
+ </button>
3685
+ </div>
3686
+ <div
3687
+ class="list-footer"
3688
+ *ngIf="
3689
+ showAllResults &&
3690
+ (config?.ui?.list?.collapseAfter ?? 0) > 0 &&
3691
+ lastResults &&
3692
+ lastResults.length > (config?.ui?.list?.collapseAfter ?? 0)
3693
+ "
3694
+ >
3695
+ <button mat-button type="button" (click)="showAllResults = false">
3696
+ Ver menos
3697
+ </button>
3698
+ </div>
3699
+ </section>
3700
+
3701
+ <ng-content></ng-content>
3702
+ </div>
3703
+ `, styles: ["@charset \"UTF-8\";.praxis-files-upload{position:relative}.praxis-files-upload .settings-btn{position:absolute;top:.5rem;right:.5rem}.praxis-files-upload .rate-limit-banner,.praxis-files-upload .quota-banner,.praxis-files-upload .error{margin:.5rem 0;padding:.5rem 1rem;border-radius:var(--mat-sys-shape-corner-small, 4px)}.praxis-files-upload .rate-limit-banner{background:var(--mat-sys-color-error-container);color:var(--mat-sys-color-on-error-container)}.praxis-files-upload .quota-banner{background:var(--mat-sys-color-tertiary-container);color:var(--mat-sys-color-on-tertiary-container)}.praxis-files-upload .dropzone{display:flex;flex-direction:column;align-items:center;justify-content:center;padding:2rem;border:2px dashed var(--mat-sys-color-outline);border-radius:var(--mat-sys-shape-corner-medium, 4px);background:var(--mat-sys-color-surface-container-low);color:var(--mat-sys-color-on-surface);text-align:center;cursor:pointer;transition:background-color .2s;width:100%;min-height:160px;margin:.5rem 0;gap:.5rem;position:relative}.praxis-files-upload .dropzone:focus,.praxis-files-upload .dropzone:hover{outline:none;background:var(--mat-sys-color-surface-container-high);border-color:var(--mat-sys-color-primary)}.praxis-files-upload .dropzone.dragover{background:var(--mat-sys-color-surface-container-high);border-color:var(--mat-sys-color-primary)}.praxis-files-upload .dropzone.disabled{opacity:.6;cursor:not-allowed;pointer-events:none}.praxis-files-upload .dropzone p{margin:0 0 .5rem}.praxis-files-upload .dropzone .dz-icon{font-size:40px;color:var(--mat-sys-color-primary);margin-bottom:.25rem}.praxis-files-upload .dropzone .dz-title{font-weight:600}.praxis-files-upload .dropzone .dz-hint{margin:0;opacity:.8;font-size:.9rem}.praxis-files-upload .dropzone .field-actions{position:absolute;top:.25rem;right:.25rem;display:flex;gap:.25rem;pointer-events:auto}.praxis-files-upload .dropzone .field-actions button[mat-icon-button]{width:32px;height:32px}.praxis-files-upload .dropzone .field-actions mat-icon{font-size:20px}.praxis-files-upload .progress{margin-top:1rem}.praxis-files-upload .sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);border:0}.praxis-files-upload .bulk-results{margin-top:1rem}.praxis-files-upload .bulk-results .bulk-summary{display:flex;gap:1rem;align-items:center;margin-bottom:.5rem}.praxis-files-upload .bulk-results .bulk-summary .stat{display:flex;align-items:center;gap:.35rem;padding:.25rem .5rem;border-radius:6px;background:var(--mat-sys-color-surface-container-low)}.praxis-files-upload .bulk-results .bulk-summary .stat.success{color:var(--mat-sys-color-primary)}.praxis-files-upload .bulk-results .bulk-summary .stat.failed{color:var(--mat-sys-color-error)}.praxis-files-upload .bulk-results .bulk-summary .stat .label{opacity:.9}.praxis-files-upload .bulk-results .bulk-summary .stat .value{font-weight:600}.praxis-files-upload .bulk-results .results-list{list-style:none;padding:0;margin:0;display:flex;flex-direction:column;gap:.5rem}.praxis-files-upload .bulk-results .result-item{display:flex;gap:.75rem;padding:.5rem .75rem;border:1px solid var(--mat-sys-color-outline-variant);border-radius:8px;background:var(--mat-sys-color-surface-container-lowest)}.praxis-files-upload .bulk-results .result-item.ok{border-color:var(--mat-sys-color-primary)}.praxis-files-upload .bulk-results .result-item.err{border-color:var(--mat-sys-color-error)}.praxis-files-upload .bulk-results .result-item .status-icon{align-self:flex-start;color:currentColor}.praxis-files-upload .bulk-results .result-item .main{flex:1;min-width:0}.praxis-files-upload .bulk-results .result-item .title{display:flex;align-items:center;gap:.5rem}.praxis-files-upload .bulk-results .result-item .name{font-weight:600}.praxis-files-upload .bulk-results .result-item .badge{font-size:.75rem;padding:.1rem .4rem;border-radius:999px;background:color-mix(in oklab,var(--mat-sys-color-primary) 15%,transparent);color:var(--mat-sys-color-primary)}.praxis-files-upload .bulk-results .result-item .badge.err{background:color-mix(in oklab,var(--mat-sys-color-error) 15%,transparent);color:var(--mat-sys-color-error)}.praxis-files-upload .bulk-results .result-item .meta,.praxis-files-upload .bulk-results .result-item .error{display:flex;flex-wrap:wrap;gap:.5rem 1rem;margin-top:.25rem}.praxis-files-upload .bulk-results .result-item .error .code{font-weight:600}.praxis-files-upload .bulk-results .result-item .extra{margin-top:.25rem}.praxis-files-upload .bulk-results .result-item .item-actions{display:flex;align-items:center;gap:.25rem}.praxis-files-upload .bulk-results .list-footer{margin-top:.5rem;display:flex;justify-content:center}.praxis-files-upload .proximity-overlay{position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:.5rem;border:2px dashed var(--mat-sys-color-outline);border-radius:var(--mat-sys-shape-corner-medium, 4px);background:color-mix(in oklab,var(--mat-sys-color-surface-container-high) 80%,transparent);color:var(--mat-sys-color-on-surface);text-align:center;z-index:2;pointer-events:none}.praxis-files-upload .proximity-overlay .dz-icon{font-size:40px;color:var(--mat-sys-color-primary)}.praxis-files-upload .proximity-overlay .dz-title{font-weight:600}.praxis-files-upload .proximity-overlay .dz-hint{opacity:.85}.praxis-files-upload .selected-overlay{background:var(--mat-sys-color-surface);color:var(--mat-sys-color-on-surface);border:1px solid var(--mat-sys-color-outline-variant);border-radius:8px;box-shadow:var(--mat-sys-level2, 0 2px 6px rgba(0, 0, 0, .15));max-height:260px;overflow:auto;padding:.5rem}.praxis-files-upload .selected-overlay .sel-header{position:sticky;top:0;z-index:1;background:var(--mat-sys-color-surface);display:flex;align-items:center;gap:.5rem;padding-bottom:.25rem;margin-bottom:.25rem}.praxis-files-upload .selected-overlay .sel-header .title{font-weight:600}.praxis-files-upload .selected-overlay .sel-header .spacer{flex:1}.praxis-files-upload .selected-overlay .sel-list{list-style:none;margin:0;padding:0}.praxis-files-upload .selected-overlay .sel-list .sel-item{display:flex;align-items:center;gap:.75rem;padding:.5rem .75rem;border-bottom:1px solid var(--mat-sys-color-outline-variant, #ccc);transition:background-color .12s ease}.praxis-files-upload .selected-overlay .sel-list .sel-item .file-icon{opacity:.9;color:var(--mat-sys-color-primary, #1976d2)}.praxis-files-upload .selected-overlay .sel-list .sel-item .info{flex:1;min-width:0}.praxis-files-upload .selected-overlay .sel-list .sel-item .info .name{font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.praxis-files-upload .selected-overlay .sel-list .sel-item .info .meta{font-size:.8rem;opacity:.7}.praxis-files-upload .selected-overlay .sel-list .sel-item .remove-btn{color:var(--mat-sys-color-error, #b00020)}.praxis-files-upload .selected-overlay .sel-list .sel-item .more-btn{margin-left:.25rem}.praxis-files-upload .selected-overlay .sel-list .sel-item:hover{background:color-mix(in oklab,#000 8%,transparent)}:host ::ng-deep .praxis-files-upload-overlay-panel{pointer-events:none}:host ::ng-deep .praxis-files-selected-overlay-panel{pointer-events:auto;z-index:3}\n"] }]
3704
+ }], ctorParameters: () => [{ type: i1$2.SettingsPanelService }, { type: DragProximityService }, { type: undefined, decorators: [{
3705
+ type: Inject,
3706
+ args: [FILES_UPLOAD_TEXTS]
3707
+ }] }, { type: i3.Overlay }, { type: i0.ViewContainerRef }, { type: i0.ChangeDetectorRef }, { type: FilesApiClient, decorators: [{
3708
+ type: Optional
3709
+ }] }, { type: PresignedUploaderService, decorators: [{
3710
+ type: Optional
3711
+ }] }, { type: ErrorMapperService, decorators: [{
3712
+ type: Optional
3713
+ }] }, { type: undefined, decorators: [{
3714
+ type: Optional
3715
+ }, {
3716
+ type: Inject,
3717
+ args: [TRANSLATE_LIKE]
3718
+ }] }, { type: undefined, decorators: [{
3719
+ type: Optional
3720
+ }, {
3721
+ type: Inject,
3722
+ args: [CONFIG_STORAGE]
3723
+ }] }], propDecorators: { config: [{
3724
+ type: Input
3725
+ }], componentId: [{
3726
+ type: Input
3727
+ }], baseUrl: [{
3728
+ type: Input
3729
+ }], displayMode: [{
3730
+ type: Input
3731
+ }], uploadSuccess: [{
3732
+ type: Output
3733
+ }], bulkComplete: [{
3734
+ type: Output
3735
+ }], error: [{
3736
+ type: Output
3737
+ }], rateLimited: [{
3738
+ type: Output
3739
+ }], retry: [{
3740
+ type: Output
3741
+ }], download: [{
3742
+ type: Output
3743
+ }], copyLink: [{
3744
+ type: Output
3745
+ }], detailsOpened: [{
3746
+ type: Output
3747
+ }], detailsClosed: [{
3748
+ type: Output
3749
+ }], pendingFilesChange: [{
3750
+ type: Output
3751
+ }], proximityChange: [{
3752
+ type: Output
3753
+ }], fileInput: [{
3754
+ type: ViewChild,
3755
+ args: ['fileInput']
3756
+ }], hostRef: [{
3757
+ type: ViewChild,
3758
+ args: ['host', { static: true }]
3759
+ }], overlayTmpl: [{
3760
+ type: ViewChild,
3761
+ args: ['proximityOverlayTmpl']
3762
+ }], selectedOverlayTmpl: [{
3763
+ type: ViewChild,
3764
+ args: ['selectedOverlayTmpl']
3765
+ }] } });
3766
+
3767
+ class PdxFilesUploadFieldComponent extends SimpleBaseInputComponent {
3768
+ config = null;
3769
+ valueMode = 'metadata';
3770
+ baseUrl;
3771
+ uploadError = null;
3772
+ uploader;
3773
+ // Estado para UI de seleção manual
3774
+ pendingFiles = [];
3775
+ // Ações para UI de seleção manual
3776
+ onTriggerUpload() {
3777
+ this.uploader?.triggerUpload();
3778
+ }
3779
+ onClearSelection() {
3780
+ this.uploader?.clearSelection();
3781
+ }
3782
+ // Recebe o estado do componente filho
3783
+ handlePendingFiles(files) {
3784
+ this.pendingFiles = files;
3785
+ if (files.length > 0) {
3786
+ this.control().markAsTouched(); // Garante que validação de 'required' seja reavaliada
3787
+ }
3788
+ }
3789
+ // Estado visual do campo compacto
3790
+ lastFileName = null;
3791
+ batchCount = 0;
3792
+ isProximityActive = false;
3793
+ errors = inject(ErrorMapperService);
3794
+ translate = inject(TRANSLATE_LIKE, { optional: true }) ?? null;
3795
+ get showUploadError() {
3796
+ const c = this.control();
3797
+ return c.touched && c.invalid;
3798
+ }
3799
+ t(key, fallback) {
3800
+ const k = `praxis.filesUpload.${key}`;
3801
+ const v = this.translate?.instant(k);
3802
+ return v && v !== k ? v : fallback;
3803
+ }
3804
+ onUploadSuccess(event) {
3805
+ const value = this.valueMode === 'id' ? event.file.id : event.file;
3806
+ this.setValue(value);
3807
+ this.control().setErrors(null);
3808
+ this.uploadError = null;
3809
+ // Atualiza visual
3810
+ this.lastFileName = event.file.fileName || null;
3811
+ this.batchCount = 1;
3812
+ }
3813
+ onBulkComplete(resp) {
3814
+ // Atualiza contador e último nome (primeiro sucesso se houver)
3815
+ const total = Array.isArray(resp?.results) ? resp.results.length : 0;
3816
+ this.batchCount = total;
3817
+ const firstOk = resp?.results?.find((r) => r?.file?.fileName)?.file?.fileName;
3818
+ if (firstOk) {
3819
+ this.lastFileName = String(firstOk);
3820
+ }
3821
+ }
3822
+ // When an error occurs, mark the control invalid and show message
3823
+ onUploadError(event) {
3824
+ this.control().setErrors({ upload: true });
3825
+ this.control().markAsTouched();
3826
+ this.control().markAsDirty();
3827
+ if (event && typeof event === 'object' && 'code' in event) {
3828
+ this.uploadError = this.errors.map(event).message;
3829
+ }
3830
+ else if (event && typeof event.message === 'string') {
3831
+ this.uploadError = event.message;
3832
+ }
3833
+ else {
3834
+ this.uploadError = this.t('genericUploadError', 'Erro no envio de arquivo.');
3835
+ }
3836
+ }
3837
+ isShellDisabled() {
3838
+ try {
3839
+ return !!(this.control()?.disabled || this.uploader?.isUploading || !this.baseUrl);
3840
+ }
3841
+ catch {
3842
+ return false;
3843
+ }
3844
+ }
3845
+ onProximityChange(isActive) {
3846
+ this.isProximityActive = isActive;
3847
+ }
3848
+ openFromShell() {
3849
+ if (this.isShellDisabled())
3850
+ return;
3851
+ this.uploader?.triggerSelect();
3852
+ }
3853
+ // Ações dos ícones (placeholders para próxima sub-issue)
3854
+ onOpenInfo() { }
3855
+ onOpenMenu() { }
3856
+ onClear() {
3857
+ this.lastFileName = null;
3858
+ this.batchCount = 0;
3859
+ this.uploadError = null;
3860
+ try {
3861
+ this.setValue(null);
3862
+ const c = this.control();
3863
+ c.markAsPristine();
3864
+ c.markAsUntouched();
3865
+ c.setErrors(null);
3866
+ }
3867
+ catch { }
3868
+ }
3869
+ get policySummary() {
3870
+ const a = this.config?.ui?.accept?.length ? this.config.ui.accept.join(', ') : '—';
3871
+ const maxB = this.config?.limits?.maxFileSizeBytes;
3872
+ const maxMb = maxB ? `${(maxB / 1024 / 1024).toFixed(1)} MB` : '—';
3873
+ const maxN = this.config?.limits?.maxFilesPerBulk ?? 1;
3874
+ return `Tipos: ${a} • Máx/arquivo: ${maxMb} • Máx/itens: ${maxN}`;
3875
+ }
3876
+ onRemovePendingFile(file, event) {
3877
+ event.stopPropagation(); // Impede que o menu feche ao clicar no botão
3878
+ this.uploader?.removePendingFile(file);
3879
+ }
3880
+ formatBytes(bytes) {
3881
+ if (bytes === 0)
3882
+ return '0 Bytes';
3883
+ const k = 1024;
3884
+ const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
3885
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
3886
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
3887
+ }
3888
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PdxFilesUploadFieldComponent, deps: null, target: i0.ɵɵFactoryTarget.Component });
3889
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.1.4", type: PdxFilesUploadFieldComponent, isStandalone: true, selector: "pdx-material-files-upload", inputs: { config: "config", valueMode: "valueMode", baseUrl: "baseUrl" }, host: { properties: { "class": "componentCssClasses()", "attr.data-field-type": "\"files-upload\"", "attr.data-field-name": "metadata()?.name", "attr.data-component-id": "componentId()" } }, viewQueries: [{ propertyName: "uploader", first: true, predicate: ["uploader"], descendants: true }], usesInheritance: true, ngImport: i0, template: `
3890
+ <!-- Casca compacta tipo input -->
3891
+ <div class="pdx-upload-field-shell"
3892
+ role="button"
3893
+ tabindex="0"
3894
+ [attr.aria-describedby]="componentId() + '-messages'"
3895
+ [class.error]="showUploadError"
3896
+ [class.disabled]="isShellDisabled()"
3897
+ [attr.aria-disabled]="isShellDisabled() ? 'true' : null"
3898
+ [class.proximity-active]="isProximityActive"
3899
+ (click)="pendingFiles.length === 0 && openFromShell()"
3900
+ (keydown.enter)="pendingFiles.length === 0 && openFromShell(); $event.preventDefault()"
3901
+ (keydown.space)="pendingFiles.length === 0 && openFromShell(); $event.preventDefault()">
3902
+ <mat-icon class="prefix" aria-hidden="true">attach_file</mat-icon>
3903
+ <div class="content">
3904
+ <!-- ESTADO 1: Sem arquivos pendentes -->
3905
+ <ng-container *ngIf="pendingFiles.length === 0; else pendingState">
3906
+ <span class="placeholder" *ngIf="!lastFileName">{{ t('placeholder','Selecione ou solte arquivos…') }}</span>
3907
+ <span class="value" *ngIf="lastFileName" [title]="lastFileName">{{ lastFileName }}</span>
3908
+ <span class="chip" *ngIf="batchCount > 1">+{{ batchCount - 1 }}</span>
3909
+ </ng-container>
3910
+
3911
+ <!-- ESTADO 2: Com arquivos pendentes -->
3912
+ <ng-template #pendingState>
3913
+ <button type="button" class="pending-text-btn" [matMenuTriggerFor]="pendingFilesMenu">
3914
+ <span>{{ pendingFiles.length }} arquivo(s) selecionado(s)</span>
3915
+ <mat-icon>arrow_drop_down</mat-icon>
3916
+ </button>
3917
+ </ng-template>
3918
+ </div>
3919
+ <div class="suffixes">
3920
+ <!-- Ações para arquivos pendentes -->
3921
+ <ng-container *ngIf="pendingFiles.length > 0">
3922
+ <button type="button" class="icon-btn" color="primary" title="Enviar" aria-label="Enviar" (click)="onTriggerUpload(); $event.stopPropagation()">
3923
+ <mat-icon>upload</mat-icon>
3924
+ </button>
3925
+ <button type="button" class="icon-btn" color="warn" title="Cancelar Seleção" aria-label="Cancelar Seleção" (click)="onClearSelection(); $event.stopPropagation()">
3926
+ <mat-icon>cancel</mat-icon>
3927
+ </button>
3928
+ </ng-container>
3929
+
3930
+ <!-- Ações padrão -->
3931
+ <button type="button" class="icon-btn" title="Informações" aria-label="Informações" (click)="$event.stopPropagation()" [disabled]="isShellDisabled()" [matTooltip]="policySummary">
3932
+ <mat-icon>info</mat-icon>
3933
+ </button>
3934
+ <button type="button" class="icon-btn" title="Mais ações" aria-label="Mais ações" (click)="$event.stopPropagation()" [disabled]="isShellDisabled()" [matMenuTriggerFor]="moreMenu">
3935
+ <mat-icon>more_horiz</mat-icon>
3936
+ </button>
3937
+ <mat-menu #moreMenu="matMenu">
3938
+ <button mat-menu-item (click)="openFromShell()">
3939
+ <mat-icon>upload_file</mat-icon>
3940
+ <span>Selecionar arquivo(s)</span>
3941
+ </button>
3942
+ <button mat-menu-item (click)="onClear()" [disabled]="!lastFileName && batchCount === 0">
3943
+ <mat-icon>clear</mat-icon>
3944
+ <span>Limpar</span>
3945
+ </button>
3946
+ </mat-menu>
3947
+ </div>
3948
+ </div>
3949
+
3950
+ <!-- Menu para exibir arquivos pendentes -->
3951
+ <mat-menu #pendingFilesMenu="matMenu" class="pending-files-menu">
3952
+ <ng-template matMenuContent>
3953
+ <button mat-menu-item *ngFor="let file of pendingFiles" (click)="$event.stopPropagation(); $event.preventDefault();" [title]="file.name">
3954
+ <div class="pending-file-item">
3955
+ <div class="file-info">
3956
+ <span class="file-name">{{ file.name }}</span>
3957
+ <span class="file-size">({{ formatBytes(file.size) }})</span>
3958
+ </div>
3959
+ <span class="spacer"></span>
3960
+ <button mat-icon-button class="remove-pending-btn" (click)="onRemovePendingFile(file, $event)" [attr.aria-label]="'Remover ' + file.name">
3961
+ <mat-icon>close</mat-icon>
3962
+ </button>
3963
+ </div>
3964
+ </button>
3965
+ </ng-template>
3966
+ </mat-menu>
3967
+
3968
+ <!-- Mensagens do campo (hint/erro) -->
3969
+ <div class="field-messages" [id]="componentId() + '-messages'">
3970
+ <div class="hint" *ngIf="metadata()?.hint && !showUploadError">{{ metadata()?.hint }}</div>
3971
+ <div class="error" *ngIf="showUploadError">{{ uploadError || t('genericUploadError','Erro no envio de arquivo.') }}</div>
3972
+ </div>
3973
+
3974
+ <!-- Componente principal de upload (agora para lógica, não para UI) -->
3975
+ <praxis-files-upload
3976
+ #uploader
3977
+ displayMode="compact"
3978
+ style="display: none;"
3979
+ [config]="config"
3980
+ [componentId]="componentId()"
3981
+ [baseUrl]="baseUrl"
3982
+ (uploadSuccess)="onUploadSuccess($event)"
3983
+ (bulkComplete)="onBulkComplete($event)"
3984
+ (error)="onUploadError($event)"
3985
+ (pendingFilesChange)="handlePendingFiles($event)"
3986
+ />
3987
+ `, isInline: true, styles: [":host{position:relative}.pdx-upload-field-shell{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border:1px solid var(--pfx-surface-border, #ccc);border-radius:8px;background:var(--pfx-surface, var(--md-sys-color-surface));color:var(--md-sys-color-on-surface);min-height:40px;cursor:pointer}.pdx-upload-field-shell:focus-within{outline:2px solid var(--md-sys-color-primary,#1976d2);outline-offset:2px}.pdx-upload-field-shell.error{border-color:var(--md-sys-color-error,#b00020)}.pdx-upload-field-shell.disabled{opacity:.6;cursor:not-allowed}.pdx-upload-field-shell .prefix{color:var(--md-sys-color-primary,#1976d2)}.pdx-upload-field-shell .content{flex:1;min-width:0;display:flex;align-items:center;gap:.5rem}.pdx-upload-field-shell .placeholder{color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell .value{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pdx-upload-field-shell .chip{font-size:.75rem;padding:0 .4rem;border-radius:999px;background:color-mix(in oklab,var(--mat-sys-color-primary,#1976d2) 15%,transparent);color:var(--mat-sys-color-primary,#1976d2)}.pdx-upload-field-shell .suffixes{display:flex;align-items:center;gap:.25rem}.pdx-upload-field-shell .icon-btn{border:0;background:transparent;padding:.25rem;border-radius:6px;cursor:pointer;color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell .icon-btn:hover{background:#ffffff0d}.pdx-upload-field-shell.disabled .icon-btn{cursor:not-allowed}.field-messages{font-size:.82rem;margin-top:.25rem}.field-messages .hint{opacity:.8}.field-messages .error{color:var(--md-sys-color-error,#b00020)}.pending-text-btn{display:flex;align-items:center;gap:.25rem;background:0;border:0;color:var(--md-sys-color-on-surface);font-family:inherit;font-size:inherit;padding:0;margin:0;cursor:pointer}.pending-text-btn span,.pending-text-btn mat-icon{color:inherit}.pending-files-menu .mat-mdc-menu-content{padding:0}.pending-file-item{display:flex;align-items:center;width:100%;gap:.5rem;padding:8px 16px}.pending-file-item .file-info{flex:1;min-width:0;text-align:left}.pending-file-item .file-name{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pending-file-item .file-size{font-size:.8rem;opacity:.7}.pending-file-item .spacer{flex:1 1 auto}.pending-file-item .remove-pending-btn{color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell.proximity-active{border-style:dashed;border-color:var(--md-sys-color-primary);transform:scale(1.02);background:color-mix(in oklab,var(--pfx-surface) 85%,var(--md-sys-color-primary) 15%);transition:all .2s ease-in-out}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i7.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i7.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: PraxisFilesUpload, selector: "praxis-files-upload", inputs: ["config", "componentId", "baseUrl", "displayMode"], outputs: ["uploadSuccess", "bulkComplete", "error", "rateLimited", "retry", "download", "copyLink", "detailsOpened", "detailsClosed", "pendingFilesChange", "proximityChange"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i10.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i3$1.MatMenu, selector: "mat-menu", inputs: ["backdropClass", "aria-label", "aria-labelledby", "aria-describedby", "xPosition", "yPosition", "overlapTrigger", "hasBackdrop", "class", "classList"], outputs: ["closed", "close"], exportAs: ["matMenu"] }, { kind: "component", type: i3$1.MatMenuItem, selector: "[mat-menu-item]", inputs: ["role", "disabled", "disableRipple"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i3$1.MatMenuContent, selector: "ng-template[matMenuContent]" }, { kind: "directive", type: i3$1.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", inputs: ["mat-menu-trigger-for", "matMenuTriggerFor", "matMenuTriggerData", "matMenuTriggerRestoreFocus"], outputs: ["menuOpened", "onMenuOpen", "menuClosed", "onMenuClose"], exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatTooltipModule }, { kind: "directive", type: i13.MatTooltip, selector: "[matTooltip]", inputs: ["matTooltipPosition", "matTooltipPositionAtOrigin", "matTooltipDisabled", "matTooltipShowDelay", "matTooltipHideDelay", "matTooltipTouchGestures", "matTooltip", "matTooltipClass"], exportAs: ["matTooltip"] }] });
3988
+ }
3989
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.1.4", ngImport: i0, type: PdxFilesUploadFieldComponent, decorators: [{
3990
+ type: Component,
3991
+ args: [{ selector: 'pdx-material-files-upload', standalone: true, imports: [CommonModule, PraxisFilesUpload, MatIconModule, MatMenuModule, MatTooltipModule], template: `
3992
+ <!-- Casca compacta tipo input -->
3993
+ <div class="pdx-upload-field-shell"
3994
+ role="button"
3995
+ tabindex="0"
3996
+ [attr.aria-describedby]="componentId() + '-messages'"
3997
+ [class.error]="showUploadError"
3998
+ [class.disabled]="isShellDisabled()"
3999
+ [attr.aria-disabled]="isShellDisabled() ? 'true' : null"
4000
+ [class.proximity-active]="isProximityActive"
4001
+ (click)="pendingFiles.length === 0 && openFromShell()"
4002
+ (keydown.enter)="pendingFiles.length === 0 && openFromShell(); $event.preventDefault()"
4003
+ (keydown.space)="pendingFiles.length === 0 && openFromShell(); $event.preventDefault()">
4004
+ <mat-icon class="prefix" aria-hidden="true">attach_file</mat-icon>
4005
+ <div class="content">
4006
+ <!-- ESTADO 1: Sem arquivos pendentes -->
4007
+ <ng-container *ngIf="pendingFiles.length === 0; else pendingState">
4008
+ <span class="placeholder" *ngIf="!lastFileName">{{ t('placeholder','Selecione ou solte arquivos…') }}</span>
4009
+ <span class="value" *ngIf="lastFileName" [title]="lastFileName">{{ lastFileName }}</span>
4010
+ <span class="chip" *ngIf="batchCount > 1">+{{ batchCount - 1 }}</span>
4011
+ </ng-container>
4012
+
4013
+ <!-- ESTADO 2: Com arquivos pendentes -->
4014
+ <ng-template #pendingState>
4015
+ <button type="button" class="pending-text-btn" [matMenuTriggerFor]="pendingFilesMenu">
4016
+ <span>{{ pendingFiles.length }} arquivo(s) selecionado(s)</span>
4017
+ <mat-icon>arrow_drop_down</mat-icon>
4018
+ </button>
4019
+ </ng-template>
4020
+ </div>
4021
+ <div class="suffixes">
4022
+ <!-- Ações para arquivos pendentes -->
4023
+ <ng-container *ngIf="pendingFiles.length > 0">
4024
+ <button type="button" class="icon-btn" color="primary" title="Enviar" aria-label="Enviar" (click)="onTriggerUpload(); $event.stopPropagation()">
4025
+ <mat-icon>upload</mat-icon>
4026
+ </button>
4027
+ <button type="button" class="icon-btn" color="warn" title="Cancelar Seleção" aria-label="Cancelar Seleção" (click)="onClearSelection(); $event.stopPropagation()">
4028
+ <mat-icon>cancel</mat-icon>
4029
+ </button>
4030
+ </ng-container>
4031
+
4032
+ <!-- Ações padrão -->
4033
+ <button type="button" class="icon-btn" title="Informações" aria-label="Informações" (click)="$event.stopPropagation()" [disabled]="isShellDisabled()" [matTooltip]="policySummary">
4034
+ <mat-icon>info</mat-icon>
4035
+ </button>
4036
+ <button type="button" class="icon-btn" title="Mais ações" aria-label="Mais ações" (click)="$event.stopPropagation()" [disabled]="isShellDisabled()" [matMenuTriggerFor]="moreMenu">
4037
+ <mat-icon>more_horiz</mat-icon>
4038
+ </button>
4039
+ <mat-menu #moreMenu="matMenu">
4040
+ <button mat-menu-item (click)="openFromShell()">
4041
+ <mat-icon>upload_file</mat-icon>
4042
+ <span>Selecionar arquivo(s)</span>
4043
+ </button>
4044
+ <button mat-menu-item (click)="onClear()" [disabled]="!lastFileName && batchCount === 0">
4045
+ <mat-icon>clear</mat-icon>
4046
+ <span>Limpar</span>
4047
+ </button>
4048
+ </mat-menu>
4049
+ </div>
4050
+ </div>
4051
+
4052
+ <!-- Menu para exibir arquivos pendentes -->
4053
+ <mat-menu #pendingFilesMenu="matMenu" class="pending-files-menu">
4054
+ <ng-template matMenuContent>
4055
+ <button mat-menu-item *ngFor="let file of pendingFiles" (click)="$event.stopPropagation(); $event.preventDefault();" [title]="file.name">
4056
+ <div class="pending-file-item">
4057
+ <div class="file-info">
4058
+ <span class="file-name">{{ file.name }}</span>
4059
+ <span class="file-size">({{ formatBytes(file.size) }})</span>
4060
+ </div>
4061
+ <span class="spacer"></span>
4062
+ <button mat-icon-button class="remove-pending-btn" (click)="onRemovePendingFile(file, $event)" [attr.aria-label]="'Remover ' + file.name">
4063
+ <mat-icon>close</mat-icon>
4064
+ </button>
4065
+ </div>
4066
+ </button>
4067
+ </ng-template>
4068
+ </mat-menu>
4069
+
4070
+ <!-- Mensagens do campo (hint/erro) -->
4071
+ <div class="field-messages" [id]="componentId() + '-messages'">
4072
+ <div class="hint" *ngIf="metadata()?.hint && !showUploadError">{{ metadata()?.hint }}</div>
4073
+ <div class="error" *ngIf="showUploadError">{{ uploadError || t('genericUploadError','Erro no envio de arquivo.') }}</div>
4074
+ </div>
4075
+
4076
+ <!-- Componente principal de upload (agora para lógica, não para UI) -->
4077
+ <praxis-files-upload
4078
+ #uploader
4079
+ displayMode="compact"
4080
+ style="display: none;"
4081
+ [config]="config"
4082
+ [componentId]="componentId()"
4083
+ [baseUrl]="baseUrl"
4084
+ (uploadSuccess)="onUploadSuccess($event)"
4085
+ (bulkComplete)="onBulkComplete($event)"
4086
+ (error)="onUploadError($event)"
4087
+ (pendingFilesChange)="handlePendingFiles($event)"
4088
+ />
4089
+ `, host: {
4090
+ '[class]': 'componentCssClasses()',
4091
+ '[attr.data-field-type]': '"files-upload"',
4092
+ '[attr.data-field-name]': 'metadata()?.name',
4093
+ '[attr.data-component-id]': 'componentId()',
4094
+ }, styles: [":host{position:relative}.pdx-upload-field-shell{display:flex;align-items:center;gap:.5rem;padding:.5rem .75rem;border:1px solid var(--pfx-surface-border, #ccc);border-radius:8px;background:var(--pfx-surface, var(--md-sys-color-surface));color:var(--md-sys-color-on-surface);min-height:40px;cursor:pointer}.pdx-upload-field-shell:focus-within{outline:2px solid var(--md-sys-color-primary,#1976d2);outline-offset:2px}.pdx-upload-field-shell.error{border-color:var(--md-sys-color-error,#b00020)}.pdx-upload-field-shell.disabled{opacity:.6;cursor:not-allowed}.pdx-upload-field-shell .prefix{color:var(--md-sys-color-primary,#1976d2)}.pdx-upload-field-shell .content{flex:1;min-width:0;display:flex;align-items:center;gap:.5rem}.pdx-upload-field-shell .placeholder{color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell .value{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pdx-upload-field-shell .chip{font-size:.75rem;padding:0 .4rem;border-radius:999px;background:color-mix(in oklab,var(--mat-sys-color-primary,#1976d2) 15%,transparent);color:var(--mat-sys-color-primary,#1976d2)}.pdx-upload-field-shell .suffixes{display:flex;align-items:center;gap:.25rem}.pdx-upload-field-shell .icon-btn{border:0;background:transparent;padding:.25rem;border-radius:6px;cursor:pointer;color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell .icon-btn:hover{background:#ffffff0d}.pdx-upload-field-shell.disabled .icon-btn{cursor:not-allowed}.field-messages{font-size:.82rem;margin-top:.25rem}.field-messages .hint{opacity:.8}.field-messages .error{color:var(--md-sys-color-error,#b00020)}.pending-text-btn{display:flex;align-items:center;gap:.25rem;background:0;border:0;color:var(--md-sys-color-on-surface);font-family:inherit;font-size:inherit;padding:0;margin:0;cursor:pointer}.pending-text-btn span,.pending-text-btn mat-icon{color:inherit}.pending-files-menu .mat-mdc-menu-content{padding:0}.pending-file-item{display:flex;align-items:center;width:100%;gap:.5rem;padding:8px 16px}.pending-file-item .file-info{flex:1;min-width:0;text-align:left}.pending-file-item .file-name{display:block;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pending-file-item .file-size{font-size:.8rem;opacity:.7}.pending-file-item .spacer{flex:1 1 auto}.pending-file-item .remove-pending-btn{color:var(--md-sys-color-on-surface-variant)}.pdx-upload-field-shell.proximity-active{border-style:dashed;border-color:var(--md-sys-color-primary);transform:scale(1.02);background:color-mix(in oklab,var(--pfx-surface) 85%,var(--md-sys-color-primary) 15%);transition:all .2s ease-in-out}\n"] }]
4095
+ }], propDecorators: { config: [{
4096
+ type: Input
4097
+ }], valueMode: [{
4098
+ type: Input
4099
+ }], baseUrl: [{
4100
+ type: Input
4101
+ }], uploader: [{
4102
+ type: ViewChild,
4103
+ args: ['uploader']
4104
+ }] } });
4105
+
4106
+ const FILES_UPLOAD_PT_BR = {
4107
+ 'praxis.filesUpload.settingsAriaLabel': 'Abrir configurações',
4108
+ 'praxis.filesUpload.dropzoneLabel': 'Arraste arquivos ou',
4109
+ 'praxis.filesUpload.dropzoneButton': 'selecionar',
4110
+ 'praxis.filesUpload.conflictPolicyLabel': 'Política de conflito',
4111
+ 'praxis.filesUpload.metadataLabel': 'Metadados (JSON)',
4112
+ 'praxis.filesUpload.progressAriaLabel': 'Progresso do upload',
4113
+ 'praxis.filesUpload.rateLimitBanner': 'Limite de requisições excedido. Tente novamente às',
4114
+ 'praxis.filesUpload.settingsTitle': 'Configuração de Upload de Arquivos',
4115
+ 'praxis.filesUpload.statusSuccess': 'Enviado',
4116
+ 'praxis.filesUpload.statusError': 'Erro',
4117
+ 'praxis.filesUpload.invalidMetadata': 'Metadados inválidos.',
4118
+ 'praxis.filesUpload.genericUploadError': 'Erro no envio de arquivo.',
4119
+ 'praxis.filesUpload.acceptError': 'Tipo de arquivo não permitido.',
4120
+ 'praxis.filesUpload.maxFileSizeError': 'Arquivo excede o tamanho máximo.',
4121
+ 'praxis.filesUpload.maxFilesPerBulkError': 'Quantidade de arquivos excedida.',
4122
+ 'praxis.filesUpload.maxBulkSizeError': 'Tamanho total dos arquivos excedido.',
4123
+ 'praxis.filesUpload.errors.INVALID_FILE_TYPE': 'Tipo de arquivo inválido.',
4124
+ 'praxis.filesUpload.errors.FILE_TOO_LARGE': 'Arquivo muito grande.',
4125
+ 'praxis.filesUpload.errors.NOT_FOUND': 'Arquivo não encontrado.',
4126
+ 'praxis.filesUpload.errors.UNAUTHORIZED': 'Requisição não autorizada.',
4127
+ 'praxis.filesUpload.errors.RATE_LIMIT_EXCEEDED': 'Limite de requisições excedido.',
4128
+ 'praxis.filesUpload.errors.INTERNAL_ERROR': 'Erro interno do servidor.',
4129
+ 'praxis.filesUpload.errors.QUOTA_EXCEEDED': 'Cota excedida.',
4130
+ 'praxis.filesUpload.errors.SEC_VIRUS_DETECTED': 'Vírus detectado no arquivo.',
4131
+ 'praxis.filesUpload.errors.SEC_MALICIOUS_CONTENT': 'Conteúdo malicioso detectado.',
4132
+ 'praxis.filesUpload.errors.SEC_DANGEROUS_TYPE': 'Tipo de arquivo perigoso.',
4133
+ 'praxis.filesUpload.errors.FMT_MAGIC_MISMATCH': 'Conteúdo do arquivo incompatível.',
4134
+ 'praxis.filesUpload.errors.FMT_CORRUPTED': 'Arquivo corrompido.',
4135
+ 'praxis.filesUpload.errors.FMT_UNSUPPORTED': 'Formato de arquivo não suportado.',
4136
+ 'praxis.filesUpload.errors.SYS_STORAGE_ERROR': 'Erro de armazenamento.',
4137
+ 'praxis.filesUpload.errors.SYS_SERVICE_DOWN': 'Serviço indisponível.',
4138
+ 'praxis.filesUpload.errors.SYS_RATE_LIMIT': 'Limite de requisições excedido.',
4139
+ // Aliases PT-BR do backend e códigos adicionais
4140
+ 'praxis.filesUpload.errors.ARQUIVO_MUITO_GRANDE': 'Arquivo muito grande.',
4141
+ 'praxis.filesUpload.errors.TIPO_ARQUIVO_INVALIDO': 'Tipo de arquivo inválido.',
4142
+ 'praxis.filesUpload.errors.TIPO_MIDIA_NAO_SUPORTADO': 'Tipo de mídia não suportado.',
4143
+ 'praxis.filesUpload.errors.CAMPO_OBRIGATORIO_AUSENTE': 'Campo obrigatório ausente.',
4144
+ 'praxis.filesUpload.errors.OPCOES_JSON_INVALIDAS': 'JSON de opções inválido.',
4145
+ 'praxis.filesUpload.errors.ARGUMENTO_INVALIDO': 'Argumento inválido.',
4146
+ 'praxis.filesUpload.errors.NAO_AUTORIZADO': 'Autenticação necessária.',
4147
+ 'praxis.filesUpload.errors.ERRO_INTERNO': 'Erro interno do servidor.',
4148
+ 'praxis.filesUpload.errors.LIMITE_TAXA_EXCEDIDO': 'Limite de taxa excedido.',
4149
+ 'praxis.filesUpload.errors.COTA_EXCEDIDA': 'Cota excedida.',
4150
+ 'praxis.filesUpload.errors.ARQUIVO_JA_EXISTE': 'Arquivo já existe.',
4151
+ // Outros possíveis códigos mapeados no catálogo
4152
+ 'praxis.filesUpload.errors.INVALID_JSON_OPTIONS': 'JSON de opções inválido.',
4153
+ 'praxis.filesUpload.errors.EMPTY_FILENAME': 'Nome do arquivo obrigatório.',
4154
+ 'praxis.filesUpload.errors.FILE_EXISTS': 'Arquivo já existe.',
4155
+ 'praxis.filesUpload.errors.PATH_TRAVERSAL': 'Path traversal detectado.',
4156
+ 'praxis.filesUpload.errors.INSUFFICIENT_STORAGE': 'Sem espaço em disco.',
4157
+ 'praxis.filesUpload.errors.UPLOAD_TIMEOUT': 'Tempo esgotado no upload.',
4158
+ 'praxis.filesUpload.errors.BULK_UPLOAD_TIMEOUT': 'Tempo esgotado no upload em lote.',
4159
+ 'praxis.filesUpload.errors.BULK_UPLOAD_CANCELLED': 'Upload em lote cancelado.',
4160
+ 'praxis.filesUpload.errors.USER_CANCELLED': 'Upload cancelado pelo usuário.',
4161
+ 'praxis.filesUpload.errors.UNKNOWN_ERROR': 'Erro desconhecido.',
4162
+ };
4163
+
4164
+ /** Metadata for PraxisFilesUpload component */
4165
+ const PRAXIS_FILES_UPLOAD_COMPONENT_METADATA = {
4166
+ id: 'praxis-files-upload',
4167
+ selector: 'praxis-files-upload',
4168
+ component: PraxisFilesUpload,
4169
+ friendlyName: 'Praxis Files Upload',
4170
+ description: 'Upload de arquivos com dropzone, envio em lote e lista de resultados rica.',
4171
+ icon: 'upload_file',
4172
+ inputs: [
4173
+ { name: 'config', type: 'FilesUploadConfig | null', description: 'Configuração de UI/limites/opções/bulk/quotas/rateLimit/messages' },
4174
+ { name: 'componentId', type: 'string', default: 'default', description: 'Identificador do componente (ex.: storage de preferências)' },
4175
+ { name: 'baseUrl', type: 'string', description: 'Base URL da API de arquivos' },
4176
+ { name: 'displayMode', type: "'full' | 'compact'", default: 'full', description: 'Modo de exibição do componente' },
4177
+ ],
4178
+ outputs: [
4179
+ { name: 'uploadSuccess', type: '{ file: FileMetadata }', description: 'Emite após upload único com sucesso' },
4180
+ { name: 'bulkComplete', type: 'BulkUploadResponse', description: 'Emite após upload em lote completar' },
4181
+ { name: 'error', type: 'ErrorResponse', description: 'Erros de upload' },
4182
+ { name: 'rateLimited', type: 'RateLimitInfo', description: 'Sinaliza rate limiting' },
4183
+ { name: 'retry', type: 'BulkUploadFileResult', description: 'Usuário acionou reenvio' },
4184
+ { name: 'download', type: 'BulkUploadFileResult', description: 'Usuário solicitou download' },
4185
+ { name: 'copyLink', type: 'BulkUploadFileResult', description: 'Usuário copiou link' },
4186
+ { name: 'detailsOpened', type: 'BulkUploadFileResult', description: 'Detalhes do item abertos' },
4187
+ { name: 'detailsClosed', type: 'BulkUploadFileResult', description: 'Detalhes do item fechados' },
4188
+ { name: 'pendingFilesChange', type: 'File[]', description: 'Mudança na lista de arquivos pendentes' },
4189
+ { name: 'proximityChange', type: 'boolean', description: 'Mudança de estado de proximidade (drag-over expand)' },
4190
+ ],
4191
+ tags: ['widget', 'upload', 'configurable', 'hasWizard', 'stable'],
4192
+ lib: '@praxisui/files-upload',
4193
+ };
4194
+ /** Provider para auto-registrar metadados do componente Files Upload. */
4195
+ function providePraxisFilesUploadMetadata() {
4196
+ return {
4197
+ provide: ENVIRONMENT_INITIALIZER,
4198
+ multi: true,
4199
+ useFactory: (registry) => () => {
4200
+ registry.register(PRAXIS_FILES_UPLOAD_COMPONENT_METADATA);
4201
+ },
4202
+ deps: [ComponentMetadataRegistry],
4203
+ };
4204
+ }
4205
+
4206
+ // Export all public API from subdirectories
4207
+
4208
+ /**
4209
+ * Generated bundle index. Do not edit.
4210
+ */
4211
+
4212
+ export { BulkUploadResultStatus, ErrorCode, ErrorMapperService, FILES_UPLOAD_ERROR_MESSAGES, FILES_UPLOAD_PT_BR, FILES_UPLOAD_TEXTS, FilesApiClient, PRAXIS_FILES_UPLOAD_COMPONENT_METADATA, PdxFilesUploadFieldComponent, PraxisFilesUpload, PraxisFilesUploadConfigEditor, PresignedUploaderService, ScanStatus, TRANSLATE_LIKE, acceptValidator, maxBulkSizeValidator, maxFileSizeValidator, maxFilesPerBulkValidator, providePraxisFilesUploadMetadata };
4213
+ //# sourceMappingURL=praxisui-files-upload.mjs.map