Fix header/footer placement on image upload step
This commit is contained in:
@@ -3,8 +3,6 @@ import { useRsi } from "../../hooks/useRsi";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Loader2, Upload, Trash2, AlertCircle, GripVertical, Maximize2, X } from "lucide-react";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { ScrollArea } from "@/components/ui/scroll-area";
|
||||
import { toast } from "sonner";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import config from "@/config";
|
||||
@@ -13,7 +11,6 @@ import { cn } from "@/lib/utils";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
KeyboardSensor,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
@@ -23,10 +20,7 @@ import {
|
||||
DragStartEvent,
|
||||
pointerWithin,
|
||||
rectIntersection,
|
||||
getFirstCollision,
|
||||
useDndMonitor,
|
||||
DragMoveEvent,
|
||||
closestCorners,
|
||||
CollisionDetection,
|
||||
useDroppable
|
||||
} from '@dnd-kit/core';
|
||||
@@ -35,7 +29,6 @@ import {
|
||||
SortableContext,
|
||||
sortableKeyboardCoordinates,
|
||||
useSortable,
|
||||
rectSortingStrategy,
|
||||
horizontalListSortingStrategy
|
||||
} from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
@@ -45,7 +38,7 @@ import {
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
|
||||
type Props<T extends string> = {
|
||||
type Props<T extends string = string> = {
|
||||
data: any[];
|
||||
file: File;
|
||||
onBack?: () => void;
|
||||
@@ -129,14 +122,13 @@ export const ImageUploadStep = <T extends string>({
|
||||
|
||||
// Handle drag over to track which product container is being hovered
|
||||
const handleDragOver = (event: DragMoveEvent) => {
|
||||
const { active, over } = event;
|
||||
const { over } = event;
|
||||
|
||||
if (!over) {
|
||||
setActiveDroppableId(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const activeContainer = findContainer(active.id.toString());
|
||||
let overContainer = null;
|
||||
|
||||
// Check if we're over a product container directly
|
||||
@@ -224,7 +216,7 @@ export const ImageUploadStep = <T extends string>({
|
||||
setActiveDroppableId(`product-${index}`);
|
||||
};
|
||||
|
||||
const handleNativeDragLeave = (e: DragEvent) => {
|
||||
const handleNativeDragLeave = () => {
|
||||
// Remove highlight when drag leaves
|
||||
container.classList.remove('border-primary', 'bg-primary/5');
|
||||
if (activeDroppableId === `product-${index}`) {
|
||||
@@ -414,7 +406,6 @@ export const ImageUploadStep = <T extends string>({
|
||||
newItems.push(...newFilteredItems);
|
||||
|
||||
// Update the product data with the new image order
|
||||
const updatedData = updateProductImageOrder(sourceProductIndex, newFilteredItems);
|
||||
|
||||
return newItems;
|
||||
}
|
||||
@@ -436,7 +427,6 @@ export const ImageUploadStep = <T extends string>({
|
||||
newItems.push(...newFilteredItems);
|
||||
|
||||
// Update the product data with the new image order
|
||||
const updatedData = updateProductImageOrder(sourceProductIndex, newFilteredItems);
|
||||
|
||||
return newItems;
|
||||
});
|
||||
@@ -484,24 +474,6 @@ export const ImageUploadStep = <T extends string>({
|
||||
};
|
||||
|
||||
// Function to update product data with the new image order
|
||||
const updateProductImageOrder = (productIndex: number, orderedImages: ProductImageSortable[]) => {
|
||||
// Create a copy of the data
|
||||
const newData = [...data];
|
||||
|
||||
// Get the current product
|
||||
const product = newData[productIndex];
|
||||
|
||||
// Get the ordered URLs
|
||||
const orderedUrls = orderedImages.map(img => img.imageUrl);
|
||||
|
||||
// Update the product with the ordered URLs
|
||||
product.image_url = orderedUrls.join(',');
|
||||
|
||||
// Update the data
|
||||
newData[productIndex] = product;
|
||||
|
||||
return newData;
|
||||
};
|
||||
|
||||
// Function to handle image upload
|
||||
const handleImageUpload = async (files: FileList | File[], productIndex: number) => {
|
||||
@@ -551,7 +523,6 @@ export const ImageUploadStep = <T extends string>({
|
||||
);
|
||||
|
||||
// Update the product data with the new image URL
|
||||
const updatedData = addImageToProduct(productIndex, result.imageUrl);
|
||||
|
||||
toast.success(`Image uploaded for ${data[productIndex].name || `Product #${productIndex + 1}`}`);
|
||||
} catch (error) {
|
||||
@@ -778,7 +749,6 @@ export const ImageUploadStep = <T extends string>({
|
||||
setProductImages(prev => prev.filter((_, idx) => idx !== imageIndex));
|
||||
|
||||
// Remove the image URL from the product data
|
||||
const updatedData = removeImageFromProduct(image.productIndex, image.imageUrl);
|
||||
|
||||
toast.success('Image removed successfully');
|
||||
} catch (error) {
|
||||
@@ -883,7 +853,7 @@ export const ImageUploadStep = <T extends string>({
|
||||
<div
|
||||
{...getRootProps()}
|
||||
className={cn(
|
||||
"border-2 border-dashed border-secondary-foreground/30 bg-muted/90 rounded-md h-24 w-24 flex flex-col items-center justify-center cursor-pointer hover:bg-muted/70 transition-colors shrink-0",
|
||||
"border-2 border-dashed border-secondary-foreground/30 bg-muted/90 rounded-md h-24 w-24 flex flex-col items-center justify-center self-center cursor-pointer hover:bg-muted/70 transition-colors shrink-0",
|
||||
isDragActive && "border-primary bg-muted"
|
||||
)}
|
||||
>
|
||||
@@ -909,7 +879,7 @@ export const ImageUploadStep = <T extends string>({
|
||||
<img
|
||||
src={image.previewUrl}
|
||||
alt={`Unassigned image ${index + 1}`}
|
||||
className="h-28 w-full object-cover"
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
<div className="absolute bottom-0 left-0 right-0 bg-black/60 p-2">
|
||||
<p className="text-white text-xs mb-1 truncate">{image.file.name}</p>
|
||||
@@ -943,7 +913,7 @@ export const ImageUploadStep = <T extends string>({
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
className="absolute top-1 left-1 bg-black/60 rounded-full p-1.5 text-white z-10 hover:bg-black/80"
|
||||
className="absolute top-1 left-1 bg-black/60 rounded-full p-1 text-white z-10 hover:bg-black/80"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -1018,57 +988,6 @@ export const ImageUploadStep = <T extends string>({
|
||||
};
|
||||
|
||||
// Add the ZoomedImage component to show a larger version of the image
|
||||
const ZoomedImage = ({ imageUrl, alt }: { imageUrl: string; alt: string }) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
className="absolute bottom-1 left-1 bg-black/60 rounded-full p-1.5 text-white z-10 hover:bg-black/80"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // Prevent triggering drag listeners
|
||||
setOpen(true);
|
||||
}}
|
||||
onPointerDown={(e) => {
|
||||
e.stopPropagation(); // Prevent drag from starting
|
||||
}}
|
||||
onMouseDown={(e) => {
|
||||
e.stopPropagation(); // Prevent drag from starting
|
||||
}}
|
||||
onTouchStart={(e) => {
|
||||
e.stopPropagation(); // Prevent drag from starting on touch
|
||||
}}
|
||||
>
|
||||
<Maximize2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[650px] max-h-[90vh] p-6 bg-background/95 backdrop-blur-sm border border-border rounded-lg shadow-2xl">
|
||||
<div className="relative flex flex-col items-center justify-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute -top-2 -right-2 bg-black/60 text-white hover:bg-black/80 rounded-full z-10 h-8 w-8"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="overflow-hidden rounded-md border border-border shadow-md bg-white dark:bg-black">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={alt}
|
||||
className="max-h-[70vh] max-w-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-muted-foreground text-center">
|
||||
{alt || "Product Image"}
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
// Sortable Image component with enhanced drag prevention
|
||||
const SortableImage = ({ image, productIndex, imgIndex }: { image: ProductImageSortable, productIndex: number, imgIndex: number }) => {
|
||||
@@ -1114,7 +1033,7 @@ export const ImageUploadStep = <T extends string>({
|
||||
<div
|
||||
ref={setNodeRef}
|
||||
style={style}
|
||||
className="relative border rounded-md overflow-hidden flex items-center justify-center shrink-0 cursor-grab active:cursor-grabbing select-none no-native-drag"
|
||||
className="relative border rounded-md overflow-hidden flex items-center justify-center shrink-0 cursor-grab active:cursor-grabbing select-none no-native-drag group hover:ring-2 hover:ring-primary/30 transition-all"
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
onDragStart={(e) => {
|
||||
@@ -1136,9 +1055,13 @@ export const ImageUploadStep = <T extends string>({
|
||||
className="h-full w-full object-cover select-none no-native-drag"
|
||||
draggable={false}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black/0 group-hover:bg-black/10 transition-colors duration-200"></div>
|
||||
<div className="absolute right-0 top-0 p-1 opacity-0 group-hover:opacity-90 transition-opacity">
|
||||
<GripVertical className="h-3 w-3 text-white drop-shadow-md" />
|
||||
</div>
|
||||
<button
|
||||
ref={deleteButtonRef}
|
||||
className="absolute top-1 right-1 bg-black/60 rounded-full p-1 text-white z-10 hover:bg-black/80"
|
||||
className="absolute opacity-0 group-hover:opacity-100 transition-opacity duration-200 top-1 right-1 bg-black/40 hover:bg-black/70 hover:scale-110 rounded-full p-1 text-white z-10"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // Prevent triggering drag listeners
|
||||
@@ -1154,7 +1077,7 @@ export const ImageUploadStep = <T extends string>({
|
||||
e.stopPropagation(); // Prevent drag from starting on touch
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-3 w-3" />
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
|
||||
{/* Fix zoom button with proper state management */}
|
||||
@@ -1162,7 +1085,7 @@ export const ImageUploadStep = <T extends string>({
|
||||
<DialogTrigger asChild>
|
||||
<button
|
||||
ref={zoomButtonRef}
|
||||
className="absolute bottom-1 left-1 bg-black/60 rounded-full p-1 text-white z-10 hover:bg-black/80"
|
||||
className="absolute opacity-0 group-hover:opacity-100 transition-opacity duration-200 bottom-1 left-1 bg-black/40 hover:bg-black/70 hover:scale-110 rounded-full p-1 text-white z-10"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation(); // Prevent triggering drag listeners
|
||||
@@ -1251,164 +1174,168 @@ export const ImageUploadStep = <T extends string>({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="p-4">
|
||||
<h2 className="text-2xl font-semibold mb-2">Add Product Images</h2>
|
||||
<p className="text-sm text-muted-foreground mb-2">
|
||||
<div className="flex flex-col h-[calc(100vh-9.5rem)] overflow-hidden">
|
||||
{/* Header - fixed at top */}
|
||||
<div className="px-8 py-6 bg-background shrink-0">
|
||||
<h2 className="text-2xl font-semibold">Add Product Images</h2>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Upload images for each product. Drag images to reorder them or move them between products.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="px-4 mb-4">
|
||||
<GenericDropzone />
|
||||
</div>
|
||||
|
||||
<UnassignedImagesSection />
|
||||
|
||||
<Separator />
|
||||
|
||||
<ScrollArea className="flex-1 p-4">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={customCollisionDetection}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
autoScroll={{
|
||||
threshold: {
|
||||
x: 0,
|
||||
y: 0.2, // Start scrolling when dragging near the edges
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* Add helper invisible div to handle global styles during drag */}
|
||||
{activeId && (
|
||||
<style dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
body {
|
||||
overflow-anchor: none;
|
||||
}
|
||||
img {
|
||||
-webkit-user-drag: none !important;
|
||||
}
|
||||
`
|
||||
}} />
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{data.map((product: any, index: number) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={cn(
|
||||
"p-3 transition-colors",
|
||||
activeDroppableId === `product-${index}` && activeId &&
|
||||
findContainer(activeId) !== index.toString() &&
|
||||
"ring-2 ring-primary bg-primary/5"
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col lg:flex-row lg:items-baseline gap-0.5 lg:gap-4">
|
||||
<h3 className="text-base font-medium">{product.name || `Product #${index + 1}`}</h3>
|
||||
<div className="text-xs lg:text-sm text-muted-foreground">
|
||||
<span className="font-medium">UPC:</span> {product.upc || 'N/A'} |
|
||||
<span className="font-medium"> Supplier #:</span> {product.supplier_no || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2">
|
||||
{/* Dropzone for image upload always on the left */}
|
||||
<ImageDropzone productIndex={index} />
|
||||
|
||||
{/* Images appear to the right of the dropzone in a sortable container */}
|
||||
<div
|
||||
className={getProductContainerClasses(index)}
|
||||
style={{
|
||||
pointerEvents: 'auto',
|
||||
touchAction: 'none',
|
||||
minHeight: '100px'
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
// This is a native event handler to ensure the browser recognizes the drop zone
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setActiveDroppableId(`product-${index}`);
|
||||
}}
|
||||
>
|
||||
<DroppableContainer
|
||||
id={`product-${index}`}
|
||||
isEmpty={getProductImages(index).length === 0}
|
||||
>
|
||||
{getProductImages(index).length > 0 ? (
|
||||
<SortableContext
|
||||
items={getProductImages(index).map(img => img.id)}
|
||||
strategy={horizontalListSortingStrategy}
|
||||
>
|
||||
{getProductImages(index).map((image, imgIndex) => (
|
||||
<SortableImage
|
||||
key={image.id}
|
||||
image={image}
|
||||
productIndex={index}
|
||||
imgIndex={imgIndex}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
) : (
|
||||
<div className="w-full h-full" data-empty-placeholder="true"></div>
|
||||
)}
|
||||
</DroppableContainer>
|
||||
</div>
|
||||
|
||||
{/* Hidden file input for backwards compatibility */}
|
||||
<Input
|
||||
ref={el => fileInputRefs.current[index] = el}
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept="image/*"
|
||||
multiple
|
||||
onChange={(e) => e.target.files && handleImageUpload(e.target.files, index)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
{/* Content area - only this part scrolls */}
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="h-full flex flex-col overflow-auto">
|
||||
<div className="px-8 py-4 shrink-0">
|
||||
<GenericDropzone />
|
||||
</div>
|
||||
|
||||
{/* Drag overlay for showing the dragged image */}
|
||||
<DragOverlay adjustScale={false} zIndex={1000} dropAnimation={null}>
|
||||
{activeId && activeImage && (
|
||||
<div
|
||||
className="relative border rounded-md overflow-hidden h-24 w-24 flex items-center justify-center opacity-90 cursor-grabbing shadow-xl no-native-drag"
|
||||
style={{
|
||||
width: "96px",
|
||||
height: "96px",
|
||||
transform: "none",
|
||||
willChange: "transform"
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={getFullImageUrl(activeImage.imageUrl)}
|
||||
alt="Dragging image"
|
||||
className="h-full w-full object-cover select-none no-native-drag"
|
||||
draggable={false}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-primary/10 border-2 border-primary pointer-events-none"></div>
|
||||
<div className="px-8 py-2 shrink-0">
|
||||
<UnassignedImagesSection />
|
||||
</div>
|
||||
|
||||
{/* Scrollable product cards */}
|
||||
<div className="px-8 py-2 flex-1">
|
||||
<DndContext
|
||||
sensors={sensors}
|
||||
collisionDetection={customCollisionDetection}
|
||||
onDragStart={handleDragStart}
|
||||
onDragOver={handleDragOver}
|
||||
onDragEnd={handleDragEnd}
|
||||
autoScroll={{
|
||||
threshold: {
|
||||
x: 0,
|
||||
y: 0.2,
|
||||
}
|
||||
}}
|
||||
>
|
||||
{activeId && (
|
||||
<style dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
body {
|
||||
overflow-anchor: none;
|
||||
}
|
||||
img {
|
||||
-webkit-user-drag: none !important;
|
||||
}
|
||||
`
|
||||
}} />
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
{data.map((product: any, index: number) => (
|
||||
<Card
|
||||
key={index}
|
||||
className={cn(
|
||||
"p-3 transition-colors",
|
||||
activeDroppableId === `product-${index}` && activeId &&
|
||||
findContainer(activeId) !== index.toString() &&
|
||||
"ring-2 ring-primary bg-primary/5"
|
||||
)}
|
||||
>
|
||||
<CardContent className="p-0">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex flex-col lg:flex-row lg:items-baseline gap-0.5 lg:gap-4">
|
||||
<h3 className="text-base font-medium">{product.name || `Product #${index + 1}`}</h3>
|
||||
<div className="text-xs lg:text-sm text-muted-foreground">
|
||||
<span className="font-medium">UPC:</span> {product.upc || 'N/A'} |
|
||||
<span className="font-medium"> Supplier #:</span> {product.supplier_no || 'N/A'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-2">
|
||||
<ImageDropzone productIndex={index} />
|
||||
|
||||
<div
|
||||
className={getProductContainerClasses(index)}
|
||||
style={{
|
||||
pointerEvents: 'auto',
|
||||
touchAction: 'none',
|
||||
minHeight: '100px'
|
||||
}}
|
||||
onDragOver={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setActiveDroppableId(`product-${index}`);
|
||||
}}
|
||||
>
|
||||
<DroppableContainer
|
||||
id={`product-${index}`}
|
||||
isEmpty={getProductImages(index).length === 0}
|
||||
>
|
||||
{getProductImages(index).length > 0 ? (
|
||||
<SortableContext
|
||||
items={getProductImages(index).map(img => img.id)}
|
||||
strategy={horizontalListSortingStrategy}
|
||||
>
|
||||
{getProductImages(index).map((image, imgIndex) => (
|
||||
<SortableImage
|
||||
key={image.id}
|
||||
image={image}
|
||||
productIndex={index}
|
||||
imgIndex={imgIndex}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
) : (
|
||||
<div className="w-full h-full" data-empty-placeholder="true"></div>
|
||||
)}
|
||||
</DroppableContainer>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
ref={el => fileInputRefs.current[index] = el}
|
||||
type="file"
|
||||
className="hidden"
|
||||
accept="image/*"
|
||||
multiple
|
||||
onChange={(e) => e.target.files && handleImageUpload(e.target.files, index)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</ScrollArea>
|
||||
|
||||
<DragOverlay adjustScale={false} zIndex={1000} dropAnimation={null}>
|
||||
{activeId && activeImage && (
|
||||
<div
|
||||
className="relative border rounded-md overflow-hidden h-24 w-24 flex items-center justify-center opacity-90 cursor-grabbing shadow-xl no-native-drag"
|
||||
style={{
|
||||
width: "96px",
|
||||
height: "96px",
|
||||
transform: "none",
|
||||
willChange: "transform"
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src={getFullImageUrl(activeImage.imageUrl)}
|
||||
alt="Dragging image"
|
||||
className="h-full w-full object-cover select-none no-native-drag"
|
||||
draggable={false}
|
||||
/>
|
||||
<div className="absolute inset-0 bg-primary/10 border-2 border-primary pointer-events-none"></div>
|
||||
</div>
|
||||
)}
|
||||
</DragOverlay>
|
||||
</DndContext>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="p-4 flex justify-between">
|
||||
{/* Footer - fixed at bottom */}
|
||||
<div className="flex items-center justify-between border-t bg-muted px-8 py-4 mt-1 shrink-0">
|
||||
{onBack && (
|
||||
<Button variant="outline" onClick={onBack}>
|
||||
Back
|
||||
</Button>
|
||||
)}
|
||||
<Button onClick={handleSubmit} disabled={isSubmitting || unassignedImages.length > 0}>
|
||||
<Button
|
||||
className="ml-auto"
|
||||
onClick={handleSubmit}
|
||||
disabled={isSubmitting || unassignedImages.length > 0}
|
||||
>
|
||||
{isSubmitting && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
||||
{unassignedImages.length > 0 ? 'Assign all images first' : 'Submit'}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user