Skip to content

Commit e6ff0b2

Browse files
committed
Refactor: Update image handling in DetailsImage component to improve fallback logic and action button integration
1 parent dd78a76 commit e6ff0b2

File tree

1 file changed

+75
-69
lines changed

1 file changed

+75
-69
lines changed

src/frontend/src/components/details/DetailsImage.tsx

Lines changed: 75 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import { StylishText } from '../items/StylishText';
4949
/**
5050
* Props for detail image
5151
*/
52-
export type DetailImageProps = {
52+
export type ImageItemProps = {
5353
appRole?: UserRoles;
5454
primary?: boolean;
5555
src: string;
@@ -59,7 +59,7 @@ export type DetailImageProps = {
5959
pk: string;
6060
image_id?: string;
6161
content_type?: ModelType.part | ModelType.company;
62-
useGridCol?: boolean;
62+
hasAction?: boolean;
6363
};
6464

6565
/**
@@ -436,7 +436,7 @@ function ImageActionButtons({
436436
/**
437437
* Renders an image with action buttons for display on Details panels
438438
*/
439-
function ImageItem(props: Readonly<DetailImageProps>) {
439+
function ImageItem(props: Readonly<ImageItemProps>) {
440440
// Displays a group of ActionButtons on hover
441441
const { hovered, ref } = useHover();
442442
const [img, setImg] = useState<string>(props.src ?? backup_image);
@@ -502,14 +502,6 @@ function ImageItem(props: Readonly<DetailImageProps>) {
502502
props.refresh?.();
503503
};
504504

505-
const hasOverlay: boolean = useMemo(() => {
506-
return (
507-
Object.values(props.EditImageActions || {}).some(
508-
(value) => value === true
509-
) || false
510-
);
511-
}, [props.EditImageActions]);
512-
513505
const expandImage = (event: any) => {
514506
cancelEvent(event);
515507
modals.open({
@@ -535,7 +527,7 @@ function ImageItem(props: Readonly<DetailImageProps>) {
535527
/>
536528
{props.appRole &&
537529
permissions.hasChangeRole(props.appRole) &&
538-
hasOverlay && (
530+
props.hasAction && (
539531
<Overlay
540532
color='black'
541533
opacity={hovered ? 0.8 : 0}
@@ -562,15 +554,10 @@ function ImageItem(props: Readonly<DetailImageProps>) {
562554
{downloadImage.modal}
563555
{deleteUploadImage.modal}
564556

565-
{props.useGridCol !== false ? (
566-
<Grid.Col span={{ base: 12, sm: 4 }}>{imageContent}</Grid.Col>
567-
) : (
568-
imageContent
569-
)}
557+
{imageContent}
570558
</>
571559
);
572560
}
573-
574561
interface UploadImage {
575562
pk: string;
576563
image: string;
@@ -588,78 +575,97 @@ interface DetailsImageProps {
588575
}
589576

590577
/**
591-
* Carousel component to display images for a model instance
578+
* Fallback image used whenever the API returns no images for the given object.
579+
*/
580+
const FALLBACK_IMAGE: UploadImage = {
581+
pk: 'fallback',
582+
image: backup_image,
583+
primary: true
584+
};
585+
586+
/**
587+
* Carousel component that displays uploaded images for a given model instance.
588+
* Handles empty states by showing a fallback image and configures navigation /
589+
* action controls based on the available data and provided props.
592590
*/
593-
export function DetailsImage(props: Readonly<DetailsImageProps>) {
594-
const {
595-
content_model,
596-
multiple,
597-
object_id,
598-
AddImageActions,
599-
EditImageActions
600-
} = props;
601-
const img_url = ApiEndpoints.upload_image_list;
602-
603-
const imageQuery = useQuery<UploadImage[]>({
604-
queryKey: [img_url, object_id],
591+
export function DetailsImage({
592+
appRole,
593+
content_model,
594+
multiple,
595+
object_id,
596+
refresh,
597+
AddImageActions,
598+
EditImageActions
599+
}: Readonly<DetailsImageProps>) {
600+
const imgUrl = ApiEndpoints.upload_image_list;
601+
602+
/**
603+
* Fetch the list of images associated with the current object.
604+
* If the API returns an empty array, we substitute a fallback image.
605+
*/
606+
const { data: images = [], isFetching: isImagesFetching } = useQuery<
607+
UploadImage[]
608+
>({
609+
queryKey: [imgUrl, object_id],
605610
queryFn: async () => {
606-
return api
607-
.get(apiUrl(img_url), {
608-
params: {
609-
content_model: content_model,
610-
object_id: object_id
611-
}
612-
})
613-
.then((response) => {
614-
// Return the data directly, or fallback to backup image
615-
if (!response.data || response.data.length === 0) {
616-
return [
617-
{
618-
pk: 'backup',
619-
image: backup_image,
620-
primary: true
621-
}
622-
];
623-
}
624-
return response.data;
625-
});
611+
const response = await api.get(apiUrl(imgUrl), {
612+
params: {
613+
content_model,
614+
object_id
615+
}
616+
});
617+
618+
if (!response.data || response.data.length === 0) {
619+
return [FALLBACK_IMAGE];
620+
}
621+
622+
return response.data;
626623
}
627624
});
628625

629-
const config = useMemo(() => {
630-
const images = imageQuery.data || [];
631-
const backup_image = images[0]?.pk === 'backup';
632-
const hasMultipleImags = multiple && images.length > 1;
626+
/**
627+
* Pre-compute values needed by the carousel and its children.
628+
*/
629+
const carouselConfig = useMemo(() => {
630+
const isFallbackImage = images[0]?.pk === FALLBACK_IMAGE.pk;
631+
const hasMultipleImages = Boolean(multiple && images.length > 1);
632+
633+
const hasAction =
634+
Object.values(EditImageActions ?? {}).some(Boolean) ||
635+
Object.values(EditImageActions ?? {}).some(Boolean) ||
636+
false;
637+
633638
return {
639+
hasAction,
640+
hasMultipleImages,
634641
startSlide: Math.max(
635642
0,
636643
images.findIndex((img) => img.primary)
637644
),
638-
hasMultipleImags,
639645
addImageActions: {
640646
selectExisting: AddImageActions?.selectExisting,
641647
uploadNewImage: AddImageActions?.uploadNewImage
642648
},
643649
editImageActions: {
644-
deleteImage: EditImageActions?.deleteImage && !backup_image,
650+
deleteImage: EditImageActions?.deleteImage && !isFallbackImage,
645651
setAsPrimary: images.length > 1 && EditImageActions?.setAsPrimary,
646652
replaceImage: EditImageActions?.replaceImage,
647653
downloadImage: EditImageActions?.downloadImage
648654
}
649655
};
650-
}, [imageQuery.data]);
656+
}, [images]);
651657

652658
return (
653659
<Grid.Col span={{ base: 12, sm: 4 }}>
654-
{!imageQuery.isFetching && imageQuery.data && (
660+
{!isImagesFetching && images.length > 0 && (
655661
<Carousel
656662
slideSize='100%'
657663
emblaOptions={{
658-
loop: config.hasMultipleImags,
664+
loop: carouselConfig.hasMultipleImages,
659665
align: 'center'
660666
}}
661-
initialSlide={config.startSlide}
662-
withControls={config.hasMultipleImags}
667+
initialSlide={carouselConfig.startSlide}
668+
withControls={carouselConfig.hasMultipleImages}
663669
previousControlProps={{
664670
style: {
665671
transform: 'translateX(-45%)',
@@ -680,19 +686,19 @@ export function DetailsImage(props: Readonly<DetailsImageProps>) {
680686
}
681687
}}
682688
>
683-
{imageQuery.data.map((imgObj: UploadImage, index: number) => (
684-
<Carousel.Slide key={index}>
689+
{images.map((imgObj: UploadImage) => (
690+
<Carousel.Slide key={imgObj.pk}>
685691
<ImageItem
686-
appRole={props.appRole}
687-
AddImageActions={config.addImageActions}
688-
EditImageActions={config.editImageActions}
692+
appRole={appRole}
693+
AddImageActions={carouselConfig.addImageActions}
694+
EditImageActions={carouselConfig.editImageActions}
689695
src={imgObj.image}
690696
image_id={imgObj.pk}
691697
pk={object_id}
692698
primary={imgObj.primary}
693699
content_type={content_model}
694-
refresh={props.refresh}
695-
useGridCol={false}
700+
refresh={refresh}
701+
hasAction={carouselConfig.hasAction}
696702
/>
697703
</Carousel.Slide>
698704
))}

0 commit comments

Comments
 (0)