Add image deletion functionality to product dialog carousel
This commit is contained in:
@@ -76,6 +76,24 @@
|
|||||||
font-size: 40px;
|
font-size: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bouton de suppression (croix rouge) */
|
||||||
|
.carousel-delete-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
right: 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 2px;
|
||||||
|
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.carousel-delete-btn mat-icon {
|
||||||
|
font-size: 20px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
color: #e53935;
|
||||||
|
}
|
||||||
|
|
||||||
/* Bandeau de vignettes */
|
/* Bandeau de vignettes */
|
||||||
|
|
||||||
.carousel-thumbs {
|
.carousel-thumbs {
|
||||||
@@ -85,15 +103,42 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.thumb-item {
|
.thumb-item {
|
||||||
|
position: relative;
|
||||||
width: 64px;
|
width: 64px;
|
||||||
height: 64px;
|
height: 64px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
overflow: hidden;
|
overflow: hidden; /* tu peux laisser comme ça */
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Bouton de suppression sur les vignettes */
|
||||||
|
.thumb-delete-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
min-width: 18px;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
line-height: 18px;
|
||||||
|
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumb-delete-btn mat-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
|
color: #e53935; /* rouge discret mais lisible */
|
||||||
|
}
|
||||||
|
|
||||||
.thumb-item.active {
|
.thumb-item.active {
|
||||||
border-color: #1976d2;
|
border-color: #1976d2;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,23 @@
|
|||||||
[disabled]="carouselItems.length <= 1">
|
[disabled]="carouselItems.length <= 1">
|
||||||
<mat-icon>chevron_right</mat-icon>
|
<mat-icon>chevron_right</mat-icon>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Bouton de suppression (croix rouge) -->
|
||||||
|
@if (carouselItems.length && !carouselItems[currentIndex].isPlaceholder) {
|
||||||
|
<button mat-icon-button
|
||||||
|
class="carousel-delete-btn"
|
||||||
|
(click)="onDeleteCurrentImage()">
|
||||||
|
<mat-icon>delete</mat-icon>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<!-- Bouton suivant -->
|
||||||
|
<button mat-icon-button
|
||||||
|
class="carousel-nav-btn right"
|
||||||
|
(click)="next()"
|
||||||
|
[disabled]="carouselItems.length <= 1">
|
||||||
|
<mat-icon>chevron_right</mat-icon>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Bandeau de vignettes -->
|
<!-- Bandeau de vignettes -->
|
||||||
@@ -40,6 +57,14 @@
|
|||||||
[class.active]="i === currentIndex"
|
[class.active]="i === currentIndex"
|
||||||
(click)="onThumbClick(i)">
|
(click)="onThumbClick(i)">
|
||||||
@if (!item.isPlaceholder) {
|
@if (!item.isPlaceholder) {
|
||||||
|
|
||||||
|
<!-- Bouton suppression vignette -->
|
||||||
|
<button mat-icon-button
|
||||||
|
class="thumb-delete-btn"
|
||||||
|
(click)="onDeleteThumb(i, $event)">
|
||||||
|
<mat-icon>close</mat-icon>
|
||||||
|
</button>
|
||||||
|
|
||||||
<img class="thumb-img" [src]="item.src" alt="Vignette produit">
|
<img class="thumb-img" [src]="item.src" alt="Vignette produit">
|
||||||
} @else {
|
} @else {
|
||||||
<div class="thumb-placeholder" (click)="fileInput.click()">
|
<div class="thumb-placeholder" (click)="fileInput.click()">
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import {Component, Inject, OnInit, inject, OnDestroy} from '@angular/core';
|
import {Component, Inject, OnInit, inject, OnDestroy} from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
import {FormBuilder, ReactiveFormsModule, Validators} from '@angular/forms';
|
||||||
import { MatFormField, MatLabel } from '@angular/material/form-field';
|
import {MatFormField, MatLabel} from '@angular/material/form-field';
|
||||||
import { MatInput } from '@angular/material/input';
|
import {MatInput} from '@angular/material/input';
|
||||||
import { MatSelectModule } from '@angular/material/select';
|
import {MatSelectModule} from '@angular/material/select';
|
||||||
import { MatCheckbox } from '@angular/material/checkbox';
|
import {MatCheckbox} from '@angular/material/checkbox';
|
||||||
import { MatButton, MatIconButton } from '@angular/material/button';
|
import {MatButton, MatIconButton} from '@angular/material/button';
|
||||||
import {
|
import {
|
||||||
MatDialogRef,
|
MatDialogRef,
|
||||||
MAT_DIALOG_DATA,
|
MAT_DIALOG_DATA,
|
||||||
@@ -13,13 +13,13 @@ import {
|
|||||||
MatDialogContent,
|
MatDialogContent,
|
||||||
MatDialogTitle
|
MatDialogTitle
|
||||||
} from '@angular/material/dialog';
|
} from '@angular/material/dialog';
|
||||||
import { MatIcon } from '@angular/material/icon';
|
import {MatIcon} from '@angular/material/icon';
|
||||||
|
|
||||||
import { catchError, forkJoin, of, Observable } from 'rxjs';
|
import {catchError, forkJoin, of, Observable} from 'rxjs';
|
||||||
|
|
||||||
import { PsItem } from '../../interfaces/ps-item';
|
import {PsItem} from '../../interfaces/ps-item';
|
||||||
import { ProductListItem } from '../../interfaces/product-list-item';
|
import {ProductListItem} from '../../interfaces/product-list-item';
|
||||||
import { PrestashopService } from '../../services/prestashop.serivce';
|
import {PrestashopService} from '../../services/prestashop.serivce';
|
||||||
|
|
||||||
export type ProductDialogData = {
|
export type ProductDialogData = {
|
||||||
mode: 'create' | 'edit';
|
mode: 'create' | 'edit';
|
||||||
@@ -48,7 +48,8 @@ export class PsProductDialogComponent implements OnInit, OnDestroy {
|
|||||||
constructor(
|
constructor(
|
||||||
@Inject(MAT_DIALOG_DATA) public data: ProductDialogData,
|
@Inject(MAT_DIALOG_DATA) public data: ProductDialogData,
|
||||||
private readonly dialogRef: MatDialogRef<PsProductDialogComponent>
|
private readonly dialogRef: MatDialogRef<PsProductDialogComponent>
|
||||||
) {}
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
mode!: 'create' | 'edit';
|
mode!: 'create' | 'edit';
|
||||||
categories: PsItem[] = [];
|
categories: PsItem[] = [];
|
||||||
@@ -167,11 +168,11 @@ export class PsProductDialogComponent implements OnInit, OnDestroy {
|
|||||||
const qty$ = this.ps.getProductQuantity(r.id).pipe(catchError(() => of(0)));
|
const qty$ = this.ps.getProductQuantity(r.id).pipe(catchError(() => of(0)));
|
||||||
const imgs$ = this.ps.getProductImageUrls(r.id).pipe(catchError(() => of<string[]>([])));
|
const imgs$ = this.ps.getProductImageUrls(r.id).pipe(catchError(() => of<string[]>([])));
|
||||||
const flags$ = this.ps.getProductFlags(r.id).pipe(
|
const flags$ = this.ps.getProductFlags(r.id).pipe(
|
||||||
catchError(() => of({ complete: false, hasManual: false, conditionLabel: undefined }))
|
catchError(() => of({complete: false, hasManual: false, conditionLabel: undefined}))
|
||||||
);
|
);
|
||||||
|
|
||||||
forkJoin({ details: details$, qty: qty$, imgs: imgs$, flags: flags$ })
|
forkJoin({details: details$, qty: qty$, imgs: imgs$, flags: flags$})
|
||||||
.subscribe(({ details, qty, imgs, flags }) => {
|
.subscribe(({details, qty, imgs, flags}) => {
|
||||||
const ttc = this.toTtc(details.priceHt ?? 0);
|
const ttc = this.toTtc(details.priceHt ?? 0);
|
||||||
const baseDesc = this.cleanForTextarea(details.description ?? '');
|
const baseDesc = this.cleanForTextarea(details.description ?? '');
|
||||||
this.lastLoadedDescription = baseDesc;
|
this.lastLoadedDescription = baseDesc;
|
||||||
@@ -203,7 +204,7 @@ export class PsProductDialogComponent implements OnInit, OnDestroy {
|
|||||||
const fl = (ev.target as HTMLInputElement).files;
|
const fl = (ev.target as HTMLInputElement).files;
|
||||||
|
|
||||||
// Nettoyage des anciens objectURL
|
// Nettoyage des anciens objectURL
|
||||||
for(let url of this.previewUrls) {
|
for (let url of this.previewUrls) {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
this.previewUrls = [];
|
this.previewUrls = [];
|
||||||
@@ -224,12 +225,12 @@ export class PsProductDialogComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
private buildCarousel() {
|
private buildCarousel() {
|
||||||
const items: CarouselItem[] = [
|
const items: CarouselItem[] = [
|
||||||
...this.existingImageUrls.map(u => ({ src: u, isPlaceholder: false })),
|
...this.existingImageUrls.map(u => ({src: u, isPlaceholder: false})),
|
||||||
...this.previewUrls.map(u => ({ src: u, isPlaceholder: false }))
|
...this.previewUrls.map(u => ({src: u, isPlaceholder: false}))
|
||||||
];
|
];
|
||||||
|
|
||||||
// placeholder en dernier
|
// placeholder en dernier
|
||||||
items.push({ src: '', isPlaceholder: true });
|
items.push({src: '', isPlaceholder: true});
|
||||||
|
|
||||||
this.carouselItems = items;
|
this.carouselItems = items;
|
||||||
if (!this.carouselItems.length) {
|
if (!this.carouselItems.length) {
|
||||||
@@ -261,7 +262,7 @@ export class PsProductDialogComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------- Save / close inchangés (à part dto.images) --------
|
// -------- Save / close --------
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
if (this.form.invalid) return;
|
if (this.form.invalid) return;
|
||||||
@@ -297,6 +298,82 @@ export class PsProductDialogComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Extrait l'id_image depuis une URL FO Presta (.../img/p/.../<id>.jpg) */
|
||||||
|
private extractImageIdFromUrl(url: string): number | null {
|
||||||
|
const m = /\/(\d+)\.(?:jpg|jpeg|png|gif)$/i.exec(url);
|
||||||
|
if (!m) return null;
|
||||||
|
const id = Number(m[1]);
|
||||||
|
return Number.isFinite(id) ? id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Suppression générique d'une image à l'index donné (carrousel + vignettes) */
|
||||||
|
private deleteImageAtIndex(idx: number) {
|
||||||
|
if (!this.carouselItems.length) return;
|
||||||
|
|
||||||
|
const item = this.carouselItems[idx];
|
||||||
|
if (!item || item.isPlaceholder) return;
|
||||||
|
|
||||||
|
const existingCount = this.existingImageUrls.length;
|
||||||
|
|
||||||
|
// --- Cas 1 : image existante (déjà chez Presta) ---
|
||||||
|
if (idx < existingCount) {
|
||||||
|
if (!this.productRow) return; // sécurité
|
||||||
|
|
||||||
|
const url = this.existingImageUrls[idx];
|
||||||
|
const imageId = this.extractImageIdFromUrl(url);
|
||||||
|
if (!imageId) {
|
||||||
|
alert('Impossible de déterminer l’ID de l’image à supprimer.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!confirm('Supprimer cette image du produit ?')) return;
|
||||||
|
|
||||||
|
this.ps.deleteProductImage(this.productRow.id, imageId).subscribe({
|
||||||
|
next: () => {
|
||||||
|
// On la retire du tableau local et on reconstruit le carrousel
|
||||||
|
this.existingImageUrls.splice(idx, 1);
|
||||||
|
this.buildCarousel();
|
||||||
|
|
||||||
|
// Repositionnement de l’index si nécessaire
|
||||||
|
if (this.currentIndex >= this.carouselItems.length - 1) {
|
||||||
|
this.currentIndex = Math.max(0, this.carouselItems.length - 2);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (e: unknown) => {
|
||||||
|
alert('Erreur lors de la suppression de l’image : ' + (e instanceof Error ? e.message : String(e)));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Cas 2 : image locale (nouvelle) ---
|
||||||
|
const localIdx = idx - existingCount;
|
||||||
|
if (localIdx >= 0 && localIdx < this.previewUrls.length) {
|
||||||
|
if (!confirm('Retirer cette image de la sélection ?')) return;
|
||||||
|
|
||||||
|
this.previewUrls.splice(localIdx, 1);
|
||||||
|
this.images.splice(localIdx, 1);
|
||||||
|
this.buildCarousel();
|
||||||
|
|
||||||
|
if (this.currentIndex >= this.carouselItems.length - 1) {
|
||||||
|
this.currentIndex = Math.max(0, this.carouselItems.length - 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// utilisée par la grande image
|
||||||
|
onDeleteCurrentImage() {
|
||||||
|
if (!this.carouselItems.length) return;
|
||||||
|
this.deleteImageAtIndex(this.currentIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
// utilisée par la croix sur une vignette
|
||||||
|
onDeleteThumb(index: number, event: MouseEvent) {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.deleteImageAtIndex(index);
|
||||||
|
}
|
||||||
|
|
||||||
close() {
|
close() {
|
||||||
this.dialogRef.close(false);
|
this.dialogRef.close(false);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user