@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.
- package/LICENSE +7 -0
- package/README.md +162 -0
- package/fesm2022/praxisui-files-upload.mjs +4213 -0
- package/fesm2022/praxisui-files-upload.mjs.map +1 -0
- package/index.d.ts +629 -0
- package/package.json +38 -0
|
@@ -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
|