Просмотр исходного кода

Fix orphaned image cleanup when canceling inventory part dialog (#105)

* allow full size images

* delete orphaned part iamges
Bernt Christian Egeland 6 месяцев назад
Родитель
Сommit
f3efe289c4

+ 15 - 0
src/features/inventory/Actions/inventoryActions.ts

@@ -303,3 +303,18 @@ export async function applyMarkupToAll(input: unknown) {
     return { updated: parts.length };
   }, { requiredPermissions: [{ action: PermissionAction.UPDATE, subject: PermissionSubject.INVENTORY }] });
 }
+
+export async function deleteOrphanedUploads(fileUrls: string[]) {
+  return withAuth(async ({ organizationId }) => {
+    for (const url of fileUrls) {
+      // Only allow deleting files belonging to this org's inventory folder
+      if (!url.includes(`/${organizationId}/inventory/`)) continue;
+      try {
+        await unlink(resolveUploadPath(url));
+      } catch {
+        // File may already be gone — ignore
+      }
+    }
+    return { success: true };
+  });
+}

+ 25 - 11
src/features/inventory/Components/InventoryPartForm.tsx

@@ -1,6 +1,6 @@
 "use client";
 
-import { useState, useRef, useCallback } from "react";
+import { useState, useRef, useCallback, useEffect } from "react";
 import { useTranslations } from "next-intl";
 import { useRouter } from "next/navigation";
 import { Button } from "@/components/ui/button";
@@ -16,7 +16,7 @@ import {
 } from "@/components/ui/dialog";
 import { toast } from "sonner";
 import { useGlassModal } from "@/components/glass-modal";
-import { createInventoryPart, updateInventoryPart } from "../Actions/inventoryActions";
+import { createInventoryPart, updateInventoryPart, deleteOrphanedUploads } from "../Actions/inventoryActions";
 import { aiAnalyzePartImage } from "../Actions/aiAnalyzePartImage";
 import { Camera, ExternalLink, ImageIcon, Loader2, Plus, Sparkles, Upload, X } from "lucide-react";
 import { compressImage } from "@/lib/compress-image";
@@ -66,6 +66,21 @@ export function InventoryPartForm({ open, onOpenChange, part, markupMultiplier,
   const formRef = useRef<HTMLFormElement>(null);
   const fileInputRef = useRef<HTMLInputElement>(null);
   const cameraInputRef = useRef<HTMLInputElement>(null);
+  const uploadedUrlsRef = useRef<string[]>([]);
+
+  useEffect(() => {
+    if (open) {
+      uploadedUrlsRef.current = [];
+      setSupplierUrl(part?.supplierUrl ?? "");
+      if (part?.gallery && part.gallery.length > 0) {
+        setGallery(part.gallery);
+      } else if (part?.imageUrl) {
+        setGallery([{ url: part.imageUrl, sortOrder: 0 }]);
+      } else {
+        setGallery([]);
+      }
+    }
+  }, [open]);
 
   const uploadFile = useCallback(async (file: File) => {
     const allowedTypes = ["image/jpeg", "image/png", "image/webp", "image/avif"];
@@ -94,6 +109,7 @@ export function InventoryPartForm({ open, onOpenChange, part, markupMultiplier,
         return;
       }
       const { url } = await res.json();
+      uploadedUrlsRef.current.push(url);
       setGallery((prev) => [...prev, { url, sortOrder: prev.length }]);
       toast.success(t('form.imageUploaded'), { id: toastId });
     } catch {
@@ -279,6 +295,7 @@ export function InventoryPartForm({ open, onOpenChange, part, markupMultiplier,
       : await createInventoryPart(data);
 
     if (result.success) {
+      uploadedUrlsRef.current = [];
       onOpenChange(false);
       router.refresh();
     } else {
@@ -289,15 +306,12 @@ export function InventoryPartForm({ open, onOpenChange, part, markupMultiplier,
   };
 
   const handleOpenChange = (isOpen: boolean) => {
-    if (isOpen) {
-      setSupplierUrl(part?.supplierUrl ?? "");
-      if (part?.gallery && part.gallery.length > 0) {
-        setGallery(part.gallery);
-      } else if (part?.imageUrl) {
-        setGallery([{ url: part.imageUrl, sortOrder: 0 }]);
-      } else {
-        setGallery([]);
+    if (!isOpen) {
+      // Clean up all images uploaded during this session (they won't be saved)
+      if (uploadedUrlsRef.current.length > 0) {
+        deleteOrphanedUploads(uploadedUrlsRef.current);
       }
+      uploadedUrlsRef.current = [];
     }
     onOpenChange(isOpen);
   };
@@ -631,7 +645,7 @@ export function InventoryPartForm({ open, onOpenChange, part, markupMultiplier,
             <Button
               type="button"
               variant="outline"
-              onClick={() => onOpenChange(false)}
+              onClick={() => handleOpenChange(false)}
             >
               {t('form.cancel')}
             </Button>

+ 14 - 1
src/features/vehicles/Components/service-images-manager.tsx

@@ -12,6 +12,7 @@ import { compressImage } from "@/lib/compress-image";
 import { addServiceAttachment } from "@/features/vehicles/Actions/addServiceAttachment";
 import { updateServiceAttachment } from "@/features/vehicles/Actions/updateServiceAttachment";
 import { deleteServiceAttachment } from "@/features/vehicles/Actions/serviceActions";
+import { ImageCarousel } from "@/features/vehicles/Components/service-detail/ImageCarousel";
 
 interface Attachment {
   id: string;
@@ -39,6 +40,7 @@ export function ServiceImagesManager({
   const [images, setImages] = useState<Attachment[]>(initialImages);
   const atLimit = maxImages !== undefined && images.length >= maxImages;
   const [uploading, setUploading] = useState(false);
+  const [carouselIndex, setCarouselIndex] = useState<number | null>(null);
   const inputRef = useRef<HTMLInputElement>(null);
 
   const handleUpload = useCallback(
@@ -251,7 +253,8 @@ export function ServiceImagesManager({
                   <img
                     src={file.fileUrl}
                     alt={file.description || file.fileName}
-                    className="aspect-square w-full object-cover"
+                    className="aspect-square w-full cursor-pointer object-cover"
+                    onClick={() => setCarouselIndex(images.indexOf(file))}
                   />
                   <Button
                     type="button"
@@ -291,6 +294,16 @@ export function ServiceImagesManager({
           </div>
         )}
       </CardContent>
+
+      <ImageCarousel
+        images={images.map((img) => ({
+          ...img,
+          createdAt: new Date(),
+        }))}
+        currentIndex={carouselIndex}
+        onClose={() => setCarouselIndex(null)}
+        onChangeIndex={setCarouselIndex}
+      />
     </Card>
   );
 }