Enhance file upload validation in add-pin and preview functionality in drag-drop component
continuous-integration/drone/push Build is passing Details

master
Alexis Feron 1 day ago
parent c5f617bd10
commit 8bca0bac22

@ -136,6 +136,12 @@
(fileRemoved)="removeFile($event)"
[errorMessage]="uploadError"
></app-drag-drop>
<div
*ngIf="form.get('files')?.invalid && form.get('files')?.touched"
class="mt-1 text-sm text-red-600 dark:text-red-500"
>
<span>Il est obligatoire d'ajouter au moins une image</span>
</div>
</div>
<div id="add-pin-modal-description" class="mb-4">

@ -61,7 +61,7 @@ export class AddPinPopupComponent implements OnInit {
location: new FormControl('', [Validators.required]),
complete_address: new FormControl('', [Validators.required]),
coordinates: new FormControl<number[]>([]),
files: new FormControl([]),
files: new FormControl([], [Validators.required]),
date: new FormControl(''),
});
}
@ -175,8 +175,8 @@ export class AddPinPopupComponent implements OnInit {
}
}
getFileNames(): string[] {
return this.files.map((file) => file.name);
getFileNames(): { name: string }[] {
return this.files.map((f) => ({ name: f.name }));
}
ngOnDestroy() {

@ -50,12 +50,24 @@
class="mt-2 text-sm text-gray-500 dark:text-gray-400"
>
<ul>
<li *ngFor="let fileName of fileNames">
{{ fileName }}
<li *ngFor="let fileObj of fileNames" class="relative">
<span
(mouseenter)="onFileNameMouseEnter(fileObj.name)"
class="cursor-pointer underline decoration-dotted max-w-[160px] truncate inline-block align-middle z-50"
[title]="fileObj.name"
>
{{
fileObj.name.length > 24
? (fileObj.name | slice : 0 : 10) +
"..." +
(fileObj.name | slice : -10)
: fileObj.name
}}
</span>
<button
type="button"
class="end-2.5 text-gray-400 bg-transparent hover:text-gray-900 rounded-lg text-sm w-6 h-6 ms-auto inline-flex justify-center items-center dark:hover:text-white"
(click)="removeFile(fileName, $event)"
(click)="removeFile(fileObj.name, $event)"
>
<svg
class="w-2 h-2"
@ -74,6 +86,22 @@
</svg>
<span class="sr-only">Delete</span>
</button>
<div
*ngIf="hoveredFileName === fileObj.name && previewUrl"
class="absolute top-8 left-20 z-20 border-2 border-gray-300 rounded shadow-lg"
>
<img
[src]="previewUrl"
alt="Preview"
class="object-cover max-w-full max-h-[120px] rounded"
style="
min-width: 120px;
min-height: 80px;
max-width: 200px;
max-height: 150px;
"
/>
</div>
</li>
</ul>
</div>

@ -2,24 +2,35 @@ import { CommonModule } from '@angular/common';
import {
Component,
EventEmitter,
HostListener,
Input,
OnChanges,
OnDestroy,
Output,
SimpleChanges,
} from '@angular/core';
import { ImageService } from '../../services/image/image.service';
@Component({
selector: 'app-drag-drop',
imports: [CommonModule],
templateUrl: './drag-drop.component.html',
})
export class DragDropComponent implements OnChanges {
@Input() initialFiles: string[] = [];
export class DragDropComponent implements OnChanges, OnDestroy {
@Input() initialFiles: { name: string; id?: string }[] = [];
@Input() errorMessage: string = '';
fileNames: string[] = [];
fileNames: { name: string; id?: string }[] = [];
@Output() filesSelected = new EventEmitter<FileList>();
@Output() fileRemoved = new EventEmitter<string>();
hoveredFileName: string | null = null;
previewUrl: string | null = null;
private objectUrl: string | null = null;
private imageUrlCache: { [id: string]: string } = {};
private lastHoveredId: string | null = null;
constructor(private imageService: ImageService) {}
ngOnChanges(changes: SimpleChanges) {
if (changes['initialFiles']) {
this.fileNames = [...this.initialFiles];
@ -58,20 +69,67 @@ export class DragDropComponent implements OnChanges {
}
updateFileNamesFromFileList(files: FileList): void {
this.fileNames = Array.from(files).map((file) => file.name);
this.fileNames = Array.from(files).map((file) => ({ name: file.name }));
}
private updateFileNames(files: FileList): void {
const newFileNames = Array.from(files).map(file => file.name);
const newFileNames = Array.from(files).map((file) => ({ name: file.name }));
this.fileNames = [...this.fileNames, ...newFileNames];
}
removeFile(fileName: string, event: Event): void {
event.stopPropagation(); // Empêcher la propagation du clic
const index = this.fileNames.indexOf(fileName);
const index = this.fileNames.findIndex((f) => f.name === fileName);
if (index > -1) {
this.fileNames.splice(index, 1);
this.fileRemoved.emit(fileName);
}
}
async onFileNameMouseEnter(fileName: string): Promise<void> {
const fileObj = this.getFileByName(fileName);
if (!fileObj) {
this.hoveredFileName = null;
this.previewUrl = null;
this.lastHoveredId = null;
return;
}
// Ne rien faire si on survole le même fichier
if (fileObj.id && this.lastHoveredId === fileObj.id) {
return;
}
this.hoveredFileName = fileName;
this.lastHoveredId = fileObj.id || null;
if (fileObj.id) {
if (this.imageUrlCache[fileObj.id]) {
this.previewUrl = this.imageUrlCache[fileObj.id];
} else {
this.imageService.getImage(fileObj.id).subscribe((blob) => {
const objectUrl = URL.createObjectURL(blob);
this.imageUrlCache[fileObj.id!] = objectUrl;
this.previewUrl = objectUrl;
});
}
} else {
this.previewUrl = null;
}
}
@HostListener('mouseleave') onFileNameMouseLeave(): void {
this.hoveredFileName = null;
this.previewUrl = null;
this.lastHoveredId = null;
}
getFileByName(fileName: string): { name: string; id?: string } | undefined {
return this.fileNames.find((f) => f.name === fileName);
}
ngOnDestroy(): void {
// Révoquer tous les objectURLs du cache
Object.values(this.imageUrlCache).forEach((url) =>
URL.revokeObjectURL(url)
);
this.imageUrlCache = {};
}
}

@ -324,7 +324,13 @@ export class EditPinPopupComponent implements OnInit, OnDestroy {
}
}
getFileNames(): string[] {
return this.files.map((file) => (file.name ? file.name.split('|')[0] : ''));
getFileNames(): { name: string; id?: string }[] {
return this.files.map((file) => {
if (file.name && file.name.includes('|')) {
const [name, id] = file.name.split('|');
return { name, id };
}
return { name: file.name || '' };
});
}
}

Loading…
Cancel
Save