@jvsoft/mat-form-controls 1.0.0-alpha.13
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/README.md +184 -0
- package/base/index.d.ts +5 -0
- package/base/jvs-mat-form-control-base.d.ts +51 -0
- package/base/public-api.d.ts +1 -0
- package/fesm2022/jvsoft-mat-form-controls-base.mjs +145 -0
- package/fesm2022/jvsoft-mat-form-controls-base.mjs.map +1 -0
- package/fesm2022/jvsoft-mat-form-controls-jvs-autocomplete.mjs +101 -0
- package/fesm2022/jvsoft-mat-form-controls-jvs-autocomplete.mjs.map +1 -0
- package/fesm2022/jvsoft-mat-form-controls-jvs-file-upload.mjs +624 -0
- package/fesm2022/jvsoft-mat-form-controls-jvs-file-upload.mjs.map +1 -0
- package/fesm2022/jvsoft-mat-form-controls.mjs +145 -0
- package/fesm2022/jvsoft-mat-form-controls.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/jvs-autocomplete/index.d.ts +5 -0
- package/jvs-autocomplete/jvs-autocomplete.component.d.ts +26 -0
- package/jvs-autocomplete/jvs-autocomplete.component.scss +58 -0
- package/jvs-autocomplete/public-api.d.ts +1 -0
- package/jvs-file-upload/README.md +613 -0
- package/jvs-file-upload/index.d.ts +5 -0
- package/jvs-file-upload/jvs-file-upload-item/jvs-file-upload-item.component.d.ts +29 -0
- package/jvs-file-upload/jvs-file-upload-item/jvs-file-upload-item.component.scss +118 -0
- package/jvs-file-upload/jvs-file-upload.component.d.ts +140 -0
- package/jvs-file-upload/jvs-file-upload.component.scss +163 -0
- package/jvs-file-upload/jvs-file-upload.directive.d.ts +42 -0
- package/jvs-file-upload/jvs-file-upload.interfaces.d.ts +77 -0
- package/jvs-file-upload/public-api.d.ts +4 -0
- package/package.json +39 -0
- package/public-api.d.ts +1 -0
- package/src/lib/mat-form-controls/mat-form-controls.component.css +0 -0
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
# JvsFileUploadComponent
|
|
2
|
+
|
|
3
|
+
Componente de subida de archivos reutilizable para proyectos `@jvsoft`, construido con el estándar más moderno de Angular 19+: **signals**, `input()`, `output()`, `computed()` y control flow declarativo (`@if`, `@for`, `@switch`).
|
|
4
|
+
|
|
5
|
+
Implementa `ControlValueAccessor` para integrarse de forma nativa con formularios reactivos y template-driven. No depende de ningún servicio de negocio: el componente padre inyecta las funciones de subida y eliminación como `input()`, lo que lo hace completamente reutilizable entre proyectos.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Características
|
|
10
|
+
|
|
11
|
+
- ✅ **Angular 19+ Signals**: `input()`, `output()`, `signal()`, `computed()` — sin decoradores legacy.
|
|
12
|
+
- ✅ **Dos modos de operación**: Subida manual (pre-form) y subida automática inmediata (temporal).
|
|
13
|
+
- ✅ **Drag & Drop**: Zona de arrastrar y soltar integrada con la directiva `jvsFileUpload`.
|
|
14
|
+
- ✅ **Validación de archivos**: por extensión, tamaño máximo, parte de nombre obligatoria o nombre exclusivo.
|
|
15
|
+
- ✅ **Barra de progreso individual** por cada archivo (modo temporal).
|
|
16
|
+
- ✅ **Soporte de formularios**: `formControlName`, `formControl`, `ngModel`.
|
|
17
|
+
- ✅ **Inversión de dependencias**: las funciones de upload/remove se pasan como `input()`, sin acoplamiento a servicios concretos.
|
|
18
|
+
- ✅ **Firma electrónica delegada**: emite un `output()` para que el padre gestione la firma.
|
|
19
|
+
- ✅ **Modo readonly**: solo muestra la lista de archivos, oculta la zona de drop.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Instalación y uso
|
|
24
|
+
|
|
25
|
+
### 1. Importar el componente
|
|
26
|
+
|
|
27
|
+
El componente es standalone y se publica como **secondary entry point**:
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { JvsFileUploadComponent } from '@jvsoft/mat-form-controls/jvs-file-upload';
|
|
31
|
+
|
|
32
|
+
@Component({
|
|
33
|
+
standalone: true,
|
|
34
|
+
imports: [JvsFileUploadComponent, ReactiveFormsModule],
|
|
35
|
+
})
|
|
36
|
+
export class MiFormulario {}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Modos de operación
|
|
42
|
+
|
|
43
|
+
### Modo A — Subida manual (`temporal = false`, defecto)
|
|
44
|
+
|
|
45
|
+
Los archivos se seleccionan localmente y **no se suben hasta que el padre llame a `uploadFilesPreForm()`**.
|
|
46
|
+
Ideal para formularios donde se quiere controlar cuándo se envían los datos.
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<jvs-file-upload
|
|
50
|
+
[extensionesPermitidas]="['pdf', 'docx', 'xlsx']"
|
|
51
|
+
[tamanoMaximoMB]="10"
|
|
52
|
+
formControlName="archivos"
|
|
53
|
+
/>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
// En el método de guardado del formulario:
|
|
58
|
+
const archivosSubidos = await this.fileUpload.uploadFilesPreForm('documentos/2024');
|
|
59
|
+
// archivosSubidos: JvsArchivoServidor[]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### Modo B — Subida automática (`temporal = true`)
|
|
63
|
+
|
|
64
|
+
Cada archivo se sube **inmediatamente** al ser seleccionado o arrastrado.
|
|
65
|
+
La barra de progreso es visible por cada ítem. El valor del control se actualiza en tiempo real.
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<jvs-file-upload
|
|
69
|
+
[temporal]="true"
|
|
70
|
+
[extensionesPermitidas]="['pdf']"
|
|
71
|
+
[tamanoMaximoMB]="5"
|
|
72
|
+
[uploadFn]="uploadFn"
|
|
73
|
+
[removeFn]="removeFn"
|
|
74
|
+
(archivoDescarga)="onDescargar($event)"
|
|
75
|
+
(firmarArchivo)="onFirmar($event)"
|
|
76
|
+
formControlName="archivos"
|
|
77
|
+
/>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// Definir la función de subida (provista por el proyecto consumidor)
|
|
82
|
+
readonly uploadFn: JvsUploadFn = (formData, anonimo) =>
|
|
83
|
+
this.filesQueryService.uploadFile(formData, anonimo);
|
|
84
|
+
|
|
85
|
+
// Definir la función de eliminación
|
|
86
|
+
// servFile.key → archivo registrado en BD con key de almacenamiento externo
|
|
87
|
+
// servFile.path → archivo recién subido (solo path disponible) o archivo de BD con path local
|
|
88
|
+
readonly removeFn: JvsRemoveFn = async ({ servFile }) => {
|
|
89
|
+
if (servFile.key) {
|
|
90
|
+
// Archivo registrado en BD: eliminar via mantenimiento (limpia BD + storage)
|
|
91
|
+
await this.queryService.guardarDatos(
|
|
92
|
+
'mantenimiento#archivos',
|
|
93
|
+
{ id: servFile.id },
|
|
94
|
+
'grl', 'toastr'
|
|
95
|
+
);
|
|
96
|
+
} else if (servFile.path) {
|
|
97
|
+
// Archivo en storage local (recién subido o BD sin key)
|
|
98
|
+
// El backend espera la ruta relativa sin el prefijo "storage/"
|
|
99
|
+
const path = (servFile.path as string).replace('storage/', '');
|
|
100
|
+
await this.filesQueryService.removeFile({ f: path });
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
### Modo C — Solo lectura (`readonly = true`)
|
|
106
|
+
|
|
107
|
+
Muestra la lista de archivos existentes sin zona de drop ni controles de edición.
|
|
108
|
+
|
|
109
|
+
```html
|
|
110
|
+
<jvs-file-upload
|
|
111
|
+
[readonly]="true"
|
|
112
|
+
[temporal]="true"
|
|
113
|
+
formControlName="archivos"
|
|
114
|
+
/>
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## API del Componente
|
|
120
|
+
|
|
121
|
+
### Entry Point
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
@jvsoft/mat-form-controls/jvs-file-upload
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Selector
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
jvs-file-upload
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
### Inputs
|
|
136
|
+
|
|
137
|
+
| Propiedad | Tipo | Defecto | Descripción |
|
|
138
|
+
|:---|:---|:---|:---|
|
|
139
|
+
| `temporal` | `boolean` | `false` | Activa la subida inmediata de archivos al seleccionarlos. |
|
|
140
|
+
| `permitirEliminar` | `boolean` | `true` | Muestra el botón de eliminar en cada ítem. |
|
|
141
|
+
| `readonly` | `boolean` | `false` | Oculta la zona de drop y solo muestra la lista. |
|
|
142
|
+
| `multiple` | `boolean` | `true` | Permite seleccionar más de un archivo. |
|
|
143
|
+
| `extensionesPermitidas` | `string[]` | `[]` | Lista de extensiones válidas (sin punto, minúsculas). Array vacío = todas. |
|
|
144
|
+
| `parteDeNombre` | `string[]` | `[]` | El nombre del archivo debe contener **todos** estos fragmentos. |
|
|
145
|
+
| `parteDeNombreExclusivo` | `string[]` | `[]` | Si el nombre contiene **alguno** de estos fragmentos, el archivo se acepta sin checar extensión. Tiene prioridad sobre `parteDeNombre`. |
|
|
146
|
+
| `tamanoMaximoMB` | `number \| null` | `null` | Tamaño máximo en MB. `null` = sin límite. |
|
|
147
|
+
| `nombreArchivoFijo` | `string \| null` | `null` | Si se provee, se envía como `nombreArchivo` en el `FormData` al servidor. |
|
|
148
|
+
| `carpetaSubida` | `string \| null` | `null` | Carpeta destino en el servidor. Si es `null`, usa `temp/YYYY-MM-DD_HH`. |
|
|
149
|
+
| `diskSubida` | `string \| null` | `null` | Disk de Laravel (`local`, `s3`, etc.). `null` = usar el default del servidor. |
|
|
150
|
+
| `uploadFn` | `JvsUploadFn \| null` | `null` | **Función de subida**. Requerida en modo `temporal` o al llamar `uploadFilesPreForm()`. |
|
|
151
|
+
| `removeFn` | `JvsRemoveFn \| null` | `null` | **Función de eliminación**. Si es `null`, el archivo se elimina solo de la lista local sin llamar al servidor. |
|
|
152
|
+
| `cssContenedorAgregados` | `string` | `''` | Clase CSS adicional para el contenedor de la lista en modo temporal. |
|
|
153
|
+
| `cssContenedorAgregadosLista` | `string` | `''` | Clase CSS adicional para el contenedor de archivos válidos/inválidos en modo no-temporal. |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
### Outputs
|
|
158
|
+
|
|
159
|
+
| Evento | Emite | Descripción |
|
|
160
|
+
|:---|:---|:---|
|
|
161
|
+
| `resultadoEliminado` | `JvsArchivoServidor[]` | Emite la lista de archivos del servidor restante tras eliminar un elemento. |
|
|
162
|
+
| `archivoDescarga` | `JvsFileEntry` | El usuario hizo click en "Descargar". El padre ejecuta la descarga real. |
|
|
163
|
+
| `firmarArchivo` | `JvsArchivoServidor` | El usuario hizo click en "Firmar". El padre abre el diálogo de firma electrónica. |
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
### Métodos públicos
|
|
168
|
+
|
|
169
|
+
Accede a estos métodos vía `ViewChild`:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
@ViewChild(JvsFileUploadComponent) fileUpload!: JvsFileUploadComponent;
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
| Método | Firma | Descripción |
|
|
176
|
+
|:---|:---|:---|
|
|
177
|
+
| `uploadFilesPreForm()` | `(carpeta?, anonimo?, disk?) => Promise<JvsArchivoServidor[]>` | Sube todos los archivos pendientes. Muestra confirmación si alguno falla. Retorna los archivos subidos. |
|
|
178
|
+
| `reset()` | `() => void` | Limpia toda la lista de archivos y notifica al `FormControl`. |
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
### Propiedades de estado (signals públicos)
|
|
183
|
+
|
|
184
|
+
| Signal | Tipo | Descripción |
|
|
185
|
+
|:---|:---|:---|
|
|
186
|
+
| `fileList` | `Signal<JvsFileEntry[]>` | Lista interna de archivos (locales y del servidor). |
|
|
187
|
+
| `invalidFiles` | `Signal<File[]>` | Archivos rechazados por la última selección. |
|
|
188
|
+
| `isDisabled` | `Signal<boolean>` | Estado disabled del control. |
|
|
189
|
+
| `isEmpty` | `Signal<boolean>` | `true` cuando no hay archivos en la lista. |
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
193
|
+
## Interfaces y Tipos
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
import {
|
|
197
|
+
JvsArchivoServidor,
|
|
198
|
+
JvsFileEntry,
|
|
199
|
+
JvsUploadFn,
|
|
200
|
+
JvsRemoveFn,
|
|
201
|
+
} from '@jvsoft/mat-form-controls/jvs-file-upload';
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### `JvsArchivoServidor`
|
|
205
|
+
|
|
206
|
+
Representa un archivo ya guardado en el servidor.
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
interface JvsArchivoServidor {
|
|
210
|
+
id?: number | string; // ID en la BD
|
|
211
|
+
key?: string; // Key en S3/almacenamiento externo
|
|
212
|
+
path?: string; // Path en storage local
|
|
213
|
+
nombre?: string; // Nombre legible del archivo
|
|
214
|
+
extension?: string; // Extensión (sin punto)
|
|
215
|
+
cArchivoData?: string; // JSON string con metadata de generación PDF
|
|
216
|
+
[key: string]: any; // Campos adicionales del servidor
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### `JvsFileEntry`
|
|
221
|
+
|
|
222
|
+
Entrada interna de la lista. Encapsula tanto archivos locales como los ya subidos.
|
|
223
|
+
|
|
224
|
+
```typescript
|
|
225
|
+
interface JvsFileEntry {
|
|
226
|
+
file?: File | { name: string }; // Objeto File nativo o wrapper mínimo
|
|
227
|
+
desdeServidor: boolean; // true si proviene de writeValue()
|
|
228
|
+
inProgress: boolean; // true mientras se está subiendo
|
|
229
|
+
progress: number; // 0-100
|
|
230
|
+
errorSubida?: boolean; // true si ocurrió un error
|
|
231
|
+
servFile?: JvsArchivoServidor; // Datos del servidor tras subida exitosa
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### `JvsUploadFn`
|
|
236
|
+
|
|
237
|
+
Tipo de la función de subida que el consumidor debe proveer:
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
type JvsUploadFn = (formData: FormData, anonimo?: boolean) => Observable<HttpEvent<any>>;
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### `JvsRemoveFn`
|
|
244
|
+
|
|
245
|
+
Tipo de la función de eliminación que el consumidor debe proveer:
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
type JvsRemoveFn = (params: Record<string, any>) => Promise<any>;
|
|
249
|
+
// params.servFile contiene el JvsArchivoServidor a eliminar
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## Lógica de validación de archivos
|
|
255
|
+
|
|
256
|
+
La directiva `JvsFileUploadDirective` implementa la siguiente cadena de decisión al recibir archivos (por click o drag & drop):
|
|
257
|
+
|
|
258
|
+
```
|
|
259
|
+
Para cada archivo recibido:
|
|
260
|
+
1. ¿Excede tamanoMaximoMB?
|
|
261
|
+
→ SÍ: Rechazado. Toast de error. Siguiente archivo.
|
|
262
|
+
|
|
263
|
+
2. ¿parteDeNombreExclusivo está definido Y el nombre del archivo
|
|
264
|
+
contiene ALGUNO de sus valores?
|
|
265
|
+
→ SÍ: Aceptado sin verificar extensión. (Siguiente archivo.)
|
|
266
|
+
|
|
267
|
+
3. ¿La extensión está en extensionesPermitidas (o la lista está vacía)
|
|
268
|
+
Y el nombre contiene TODOS los valores de parteDeNombre?
|
|
269
|
+
→ SÍ: Aceptado.
|
|
270
|
+
|
|
271
|
+
4. En cualquier otro caso:
|
|
272
|
+
→ Rechazado. Toast de error.
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
---
|
|
276
|
+
|
|
277
|
+
## Flujo de datos (Diagrama)
|
|
278
|
+
|
|
279
|
+
```
|
|
280
|
+
Usuario selecciona / arrastra archivos
|
|
281
|
+
│
|
|
282
|
+
▼
|
|
283
|
+
JvsFileUploadDirective.filtrarArchivos()
|
|
284
|
+
│ │
|
|
285
|
+
válidos inválidos
|
|
286
|
+
│ │
|
|
287
|
+
│ invalidFiles.set()
|
|
288
|
+
│
|
|
289
|
+
onFilesChange()
|
|
290
|
+
│
|
|
291
|
+
├─ [temporal=false] → fileList.set(entries sin progress)
|
|
292
|
+
│ _notifyChange() → onChange(File[])
|
|
293
|
+
│
|
|
294
|
+
└─ [temporal=true] → fileList.update([...nuevos])
|
|
295
|
+
uploadFileTemporal(entry) por cada nuevo
|
|
296
|
+
│
|
|
297
|
+
▼
|
|
298
|
+
uploadFn(FormData).pipe(
|
|
299
|
+
UploadProgress → fileList.update(progress)
|
|
300
|
+
Response → fileList.update(servFile)
|
|
301
|
+
_notifyChange() → onChange(JvsArchivoServidor[])
|
|
302
|
+
)
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## Integración con formularios reactivos
|
|
308
|
+
|
|
309
|
+
### Con `formControlName`
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
form = this.fb.group({
|
|
313
|
+
titulo: ['', Validators.required],
|
|
314
|
+
archivos: [[]], // Valor inicial: array vacío
|
|
315
|
+
});
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
```html
|
|
319
|
+
<form [formGroup]="form" (ngSubmit)="guardar()">
|
|
320
|
+
<jvs-file-upload
|
|
321
|
+
formControlName="archivos"
|
|
322
|
+
[temporal]="true"
|
|
323
|
+
[uploadFn]="uploadFn"
|
|
324
|
+
/>
|
|
325
|
+
</form>
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
**Valor del control:**
|
|
329
|
+
- Modo `temporal = false`: `File[]` — objetos File del browser (solo archivos locales nuevos; los archivos precargados del servidor no se incluyen en el valor pero sí se muestran en la lista).
|
|
330
|
+
- Modo `temporal = true`: `JvsArchivoServidor[]` — objetos retornados por el servidor.
|
|
331
|
+
|
|
332
|
+
### Validación de formulario
|
|
333
|
+
|
|
334
|
+
El componente implementa la interfaz `Validator` de Angular. Registra errores en el `FormControl` automáticamente:
|
|
335
|
+
|
|
336
|
+
| Error | Cuándo aparece |
|
|
337
|
+
|:---|:---|
|
|
338
|
+
| `uploadEnProgreso: true` | Hay al menos un archivo siendo subido en este momento. |
|
|
339
|
+
| `errorSubida: true` | Al menos un archivo terminó con error de subida. |
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
// Leer los errores desde el componente padre
|
|
343
|
+
const ctrl = this.form.get('archivos');
|
|
344
|
+
|
|
345
|
+
if (ctrl?.hasError('uploadEnProgreso')) {
|
|
346
|
+
// Subida en curso — no guardar aún
|
|
347
|
+
}
|
|
348
|
+
if (ctrl?.hasError('errorSubida')) {
|
|
349
|
+
// Hubo un fallo — mostrar mensaje al usuario
|
|
350
|
+
}
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
```html
|
|
354
|
+
<!-- Mostrar mensajes de error en el template -->
|
|
355
|
+
<jvs-file-upload formControlName="archivos" [temporal]="true" [uploadFn]="uploadFn">
|
|
356
|
+
<span validator>
|
|
357
|
+
@if (form.get('archivos')?.hasError('uploadEnProgreso')) {
|
|
358
|
+
Espere a que terminen de subir los archivos.
|
|
359
|
+
}
|
|
360
|
+
@if (form.get('archivos')?.hasError('errorSubida')) {
|
|
361
|
+
Algunos archivos no se pudieron subir. Elimínalos e inténtalo de nuevo.
|
|
362
|
+
}
|
|
363
|
+
</span>
|
|
364
|
+
</jvs-file-upload>
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
> Los errores se actualizan automáticamente: Angular re-ejecuta `validate()` cada vez que cambia el estado de progreso o error en la lista.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
### Cargar archivos existentes (`writeValue`)
|
|
372
|
+
|
|
373
|
+
Cuando el formulario se carga con datos del servidor (ej: editando un registro):
|
|
374
|
+
|
|
375
|
+
```typescript
|
|
376
|
+
this.form.patchValue({
|
|
377
|
+
archivos: registro.archivos, // JvsArchivoServidor[]
|
|
378
|
+
});
|
|
379
|
+
// El componente reconstruye la lista mostrando los archivos "En servidor".
|
|
380
|
+
// Funciona en ambos modos (temporal = true y temporal = false).
|
|
381
|
+
//
|
|
382
|
+
// En temporal = false: los archivos del servidor se muestran en la lista pero NO se
|
|
383
|
+
// re-suben al llamar uploadFilesPreForm() (ya están en el servidor). El valor que
|
|
384
|
+
// emite onChange solo incluye los File locales nuevos que el usuario agregue.
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
---
|
|
388
|
+
|
|
389
|
+
## Ejemplos completos
|
|
390
|
+
|
|
391
|
+
### Formulario de registro de documentos
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
@Component({
|
|
395
|
+
template: `
|
|
396
|
+
<jvs-file-upload
|
|
397
|
+
#fileUpload
|
|
398
|
+
[extensionesPermitidas]="['pdf', 'docx']"
|
|
399
|
+
[tamanoMaximoMB]="10"
|
|
400
|
+
[multiple]="false"
|
|
401
|
+
[uploadFn]="uploadFn"
|
|
402
|
+
[carpetaSubida]="'contratos/2024'"
|
|
403
|
+
(resultadoEliminado)="onEliminado($event)"
|
|
404
|
+
formControlName="contrato"
|
|
405
|
+
/>
|
|
406
|
+
`
|
|
407
|
+
})
|
|
408
|
+
export class RegistroContratoComponent {
|
|
409
|
+
@ViewChild('fileUpload') fileUpload!: JvsFileUploadComponent;
|
|
410
|
+
|
|
411
|
+
readonly uploadFn: JvsUploadFn = (fd) =>
|
|
412
|
+
inject(FilesQueryService).uploadFile(fd);
|
|
413
|
+
|
|
414
|
+
async guardar() {
|
|
415
|
+
const archivos = await this.fileUpload.uploadFilesPreForm();
|
|
416
|
+
if (!archivos.length) return; // El usuario canceló o hubo error
|
|
417
|
+
|
|
418
|
+
this.form.patchValue({ contrato: archivos[0] });
|
|
419
|
+
await this.registroService.guardar(this.form.value);
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
```
|
|
423
|
+
|
|
424
|
+
### Vista de detalle con descarga y firma
|
|
425
|
+
|
|
426
|
+
```typescript
|
|
427
|
+
@Component({
|
|
428
|
+
template: `
|
|
429
|
+
<jvs-file-upload
|
|
430
|
+
[temporal]="true"
|
|
431
|
+
[readonly]="!puedeEditar"
|
|
432
|
+
[uploadFn]="uploadFn"
|
|
433
|
+
[removeFn]="removeFn"
|
|
434
|
+
(archivoDescarga)="descargar($event)"
|
|
435
|
+
(firmarArchivo)="firmar($event)"
|
|
436
|
+
formControlName="adjuntos"
|
|
437
|
+
/>
|
|
438
|
+
`
|
|
439
|
+
})
|
|
440
|
+
export class DetalleExpedienteComponent {
|
|
441
|
+
|
|
442
|
+
descargar(entry: JvsFileEntry) {
|
|
443
|
+
if (entry.servFile?.key) {
|
|
444
|
+
this.filesService.downloadFile({ cArchivoKey: entry.servFile.key });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
firmar(archivo: JvsArchivoServidor) {
|
|
449
|
+
this.dlgFirmaService.firmarPorTipo(this.tipoFirma, archivo);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## Comparativa con la versión legada (`fc-file-upload`)
|
|
457
|
+
|
|
458
|
+
| Característica | Versión legada | `jvs-file-upload` |
|
|
459
|
+
|:---|:---|:---|
|
|
460
|
+
| Inputs | `@Input()` decorador | `input()` signal |
|
|
461
|
+
| Outputs | `@Output() EventEmitter` | `output()` signal |
|
|
462
|
+
| Estado | `any[]` mutable | `signal<JvsFileEntry[]>()` |
|
|
463
|
+
| Derivados | Lógica en template | `computed()` signals |
|
|
464
|
+
| HTTP | `toPromise()` (deprecado) | `lastValueFrom()` |
|
|
465
|
+
| Control flow | `*ngIf`, `*ngFor` | `@if`, `@for`, `@switch` |
|
|
466
|
+
| Servicios | Inyectados (acoplados) | `input<JvsUploadFn>()` (inversión de dependencia) |
|
|
467
|
+
| Firma | Dependencia directa a `FirmaService` | `output()` — el padre gestiona |
|
|
468
|
+
| `@Output() resultado` | Declarado, nunca emitía | Eliminado |
|
|
469
|
+
| `@Input() contieneEnNombre` | Declarado, sin implementar | Eliminado |
|
|
470
|
+
| `validarExtensionesV1()` | Bug en condición lógica | Eliminado |
|
|
471
|
+
| `console.log` en `mostrarDataArchivo` | Presente | Eliminado |
|
|
472
|
+
| Tipado | `fileList: any[]` | `fileList: signal<JvsFileEntry[]>` |
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
---
|
|
477
|
+
|
|
478
|
+
## Referencia de API del backend (JVSoft Laravel)
|
|
479
|
+
|
|
480
|
+
El componente consume tres endpoints del módulo `grl` del backend. Los mismos existen en versión autenticada y anónima.
|
|
481
|
+
|
|
482
|
+
### POST `/grl/archivos/cargar` · `/grl/archivos/anonimo/cargar`
|
|
483
|
+
|
|
484
|
+
Sube un archivo al disco de Laravel.
|
|
485
|
+
|
|
486
|
+
**Request** (`multipart/form-data` o JSON con base64):
|
|
487
|
+
|
|
488
|
+
| Campo | Tipo | Requerido | Descripción |
|
|
489
|
+
|:---|:---|:---|:---|
|
|
490
|
+
| `archivo` | `File` | ✓ (o `base64`) | Archivo multipart |
|
|
491
|
+
| `base64` | `string` | ✓ (o `archivo`) | Contenido en base64. Incluir prefijo `data:<mime>;base64,` o enviar `mime` por separado |
|
|
492
|
+
| `carpeta` | `string` | ✓ | Ruta destino en el disco (ej: `"temp/2024-01-15_14"`, `"tramites/adjuntos"`) |
|
|
493
|
+
| `disk` | `string` | — | Nombre del disco Laravel (`local`, `s3`, `tramite`, etc.). Default: el configurado en `filesystems.default` |
|
|
494
|
+
| `nombreArchivo` | `string` | — | Nombre fijo (sin extensión). Si se omite, se genera: `timestamp-[prefijo-]nombre_original[-sufijo].ext` |
|
|
495
|
+
| `prefijo` | `string` | — | Prefijo añadido al nombre generado automáticamente |
|
|
496
|
+
| `sufijo` | `string` | — | Sufijo añadido al nombre generado automáticamente |
|
|
497
|
+
|
|
498
|
+
**Respuesta** `200 OK`:
|
|
499
|
+
```json
|
|
500
|
+
"temp/2024-01-15_14/1705326000-documento.pdf"
|
|
501
|
+
```
|
|
502
|
+
> ⚠️ El backend devuelve un **string plano** (la ruta relativa en el disco), no un objeto JSON.
|
|
503
|
+
> El componente lo normaliza automáticamente a `{ path: "..." }` para uso uniforme.
|
|
504
|
+
|
|
505
|
+
---
|
|
506
|
+
|
|
507
|
+
### POST `/grl/archivos/eliminar` · `/grl/archivos/anonimo/eliminar`
|
|
508
|
+
|
|
509
|
+
Elimina un archivo del disco de Laravel por su ruta relativa.
|
|
510
|
+
|
|
511
|
+
**Request** (JSON):
|
|
512
|
+
|
|
513
|
+
| Campo | Tipo | Descripción |
|
|
514
|
+
|:---|:---|:---|
|
|
515
|
+
| `f` | `string` | Ruta relativa del archivo en el disco (sin prefijo `storage/`) |
|
|
516
|
+
|
|
517
|
+
**Respuesta** `200 OK`:
|
|
518
|
+
```json
|
|
519
|
+
{ "msg": "Se eliminó correctamente" }
|
|
520
|
+
```
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
### GET `/grl/archivos/descargar` · `/grl/archivos/anonimo/descargar`
|
|
525
|
+
|
|
526
|
+
Sirve un archivo almacenado. Acepta dos modos de identificación:
|
|
527
|
+
|
|
528
|
+
**Por `key` (archivos registrados en BD):**
|
|
529
|
+
|
|
530
|
+
| Parámetro | Descripción |
|
|
531
|
+
|:---|:---|
|
|
532
|
+
| `cArchivoKey` | Key del archivo en la BD |
|
|
533
|
+
| `cArchivoCVD` | Alternativa: código de verificación digital |
|
|
534
|
+
|
|
535
|
+
Si el archivo tiene firmas electrónicas, el backend genera el PDF con QR de firma incrustado antes de servirlo.
|
|
536
|
+
|
|
537
|
+
**Por ruta (`f`):**
|
|
538
|
+
|
|
539
|
+
| Parámetro | Descripción |
|
|
540
|
+
|:---|:---|
|
|
541
|
+
| `f` | Ruta relativa en el disco (ej: `"temp/2024-01-15_14/file.pdf"`) |
|
|
542
|
+
| `porTipoAutomatico` | `1` → inline si < 50 MB, descarga si es mayor |
|
|
543
|
+
| `type` | `"view_pdf"` o `"view_img"` → fuerza inline |
|
|
544
|
+
| `disk` | Disco alternativo (opcional) |
|
|
545
|
+
| `name` | Nombre de descarga personalizado (opcional) |
|
|
546
|
+
|
|
547
|
+
**Ejemplo desde el componente padre:**
|
|
548
|
+
```typescript
|
|
549
|
+
descargar(entry: JvsFileEntry) {
|
|
550
|
+
const sf = entry.servFile;
|
|
551
|
+
if (!sf) return;
|
|
552
|
+
|
|
553
|
+
if (sf.key) {
|
|
554
|
+
// Archivo en BD: el backend busca por key y aplica firmas si corresponde
|
|
555
|
+
this.filesQueryService.downloadFile({ cArchivoKey: sf.key });
|
|
556
|
+
} else if (sf.cArchivoData) {
|
|
557
|
+
// PDF generado dinámicamente con metadata
|
|
558
|
+
const meta = JSON.parse(sf.cArchivoData);
|
|
559
|
+
this.filesQueryService.downloadFile({ cArchivoKey: meta.key ?? sf.key });
|
|
560
|
+
} else if (sf.path) {
|
|
561
|
+
// Archivo recién subido (solo path disponible)
|
|
562
|
+
this.filesQueryService.downloadFile({ f: sf.path, porTipoAutomatico: 1 });
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Storybook
|
|
570
|
+
|
|
571
|
+
Para explorar el componente de forma interactiva:
|
|
572
|
+
|
|
573
|
+
```bash
|
|
574
|
+
npx nx run mat-form-controls:storybook
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
---
|
|
578
|
+
|
|
579
|
+
## Directiva `JvsFileUploadDirective`
|
|
580
|
+
|
|
581
|
+
La directiva interna que gestiona el drag-and-drop. También puede usarse independientemente:
|
|
582
|
+
|
|
583
|
+
```typescript
|
|
584
|
+
import { JvsFileUploadDirective } from '@jvsoft/mat-form-controls/jvs-file-upload';
|
|
585
|
+
```
|
|
586
|
+
|
|
587
|
+
### Inputs
|
|
588
|
+
|
|
589
|
+
| Input | Tipo | Defecto | Descripción |
|
|
590
|
+
|:---|:---|:---|:---|
|
|
591
|
+
| `controlFile` | `HTMLInputElement` | **requerido** | Referencia al `<input type="file">` para determinar si es multiple. |
|
|
592
|
+
| `extensionesPermitidas` | `string[]` | `[]` | Extensiones aceptadas al soltar archivos. |
|
|
593
|
+
| `parteDeNombre` | `string[]` | `[]` | Fragmentos requeridos en el nombre. |
|
|
594
|
+
| `parteDeNombreExclusivo` | `string[]` | `[]` | Fragmentos exclusivos (aceptan sin checar extensión). |
|
|
595
|
+
| `tamanoMaximoMB` | `number \| null` | `null` | Límite de tamaño en MB. |
|
|
596
|
+
| `isDisabled` | `boolean` | `false` | Deshabilita el drag-and-drop. |
|
|
597
|
+
| `fondoInicial` | `string` | `''` | Color de fondo del elemento en estado normal. |
|
|
598
|
+
| `fondoDragOver` | `string` | `'#e5e7eb'` | Color de fondo al arrastrar sobre el elemento. |
|
|
599
|
+
|
|
600
|
+
### Outputs
|
|
601
|
+
|
|
602
|
+
| Output | Emite | Descripción |
|
|
603
|
+
|:---|:---|:---|
|
|
604
|
+
| `filesChange` | `File[]` | Archivos válidos al soltar en la zona. |
|
|
605
|
+
| `filesInvalidChange` | `File[]` | Archivos rechazados al soltar en la zona. |
|
|
606
|
+
|
|
607
|
+
### Método estático
|
|
608
|
+
|
|
609
|
+
```typescript
|
|
610
|
+
JvsFileUploadDirective.filtrarArchivos(files, params, multiple?): { valid: File[], invalid: File[] }
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
Útil para validar archivos desde un `input[type=file]` sin necesidad del drop.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { convertirBytes } from '@jvsoft/utils';
|
|
2
|
+
import { JvsFileEntry, JvsArchivoServidor } from '../jvs-file-upload.interfaces';
|
|
3
|
+
import * as i0 from "@angular/core";
|
|
4
|
+
/**
|
|
5
|
+
* Componente de ítem individual de la lista de archivos en modo `temporal`.
|
|
6
|
+
* Muestra nombre, tamaño, barra de progreso y acciones (descargar, firmar, eliminar).
|
|
7
|
+
*/
|
|
8
|
+
export declare class JvsFileUploadItemComponent {
|
|
9
|
+
file: import("@angular/core").InputSignal<JvsFileEntry>;
|
|
10
|
+
permitirEliminar: import("@angular/core").InputSignal<boolean>;
|
|
11
|
+
isDisabled: import("@angular/core").InputSignal<boolean>;
|
|
12
|
+
eliminar: import("@angular/core").OutputEmitterRef<JvsFileEntry>;
|
|
13
|
+
descargar: import("@angular/core").OutputEmitterRef<JvsFileEntry>;
|
|
14
|
+
firmar: import("@angular/core").OutputEmitterRef<JvsArchivoServidor>;
|
|
15
|
+
readonly extension: import("@angular/core").Signal<string>;
|
|
16
|
+
readonly nombreMostrado: import("@angular/core").Signal<string>;
|
|
17
|
+
readonly estadoProgreso: import("@angular/core").Signal<"" | "cargando" | "incompleto" | "completado">;
|
|
18
|
+
readonly modoProgreso: import("@angular/core").Signal<"indeterminate" | "determinate">;
|
|
19
|
+
readonly mostrarBotonDescargar: import("@angular/core").Signal<boolean>;
|
|
20
|
+
readonly mostrarBotonFirmar: import("@angular/core").Signal<boolean>;
|
|
21
|
+
readonly mostrarBotonEliminar: import("@angular/core").Signal<boolean>;
|
|
22
|
+
readonly tamanioArchivo: import("@angular/core").Signal<number>;
|
|
23
|
+
readonly convertirBytes: typeof convertirBytes;
|
|
24
|
+
onEliminar(): void;
|
|
25
|
+
onDescargar(): void;
|
|
26
|
+
onFirmar(): void;
|
|
27
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<JvsFileUploadItemComponent, never>;
|
|
28
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<JvsFileUploadItemComponent, "jvs-file-upload-item", never, { "file": { "alias": "file"; "required": true; "isSignal": true; }; "permitirEliminar": { "alias": "permitirEliminar"; "required": false; "isSignal": true; }; "isDisabled": { "alias": "isDisabled"; "required": false; "isSignal": true; }; }, { "eliminar": "eliminar"; "descargar": "descargar"; "firmar": "firmar"; }, never, never, true, never>;
|
|
29
|
+
}
|