@@ -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-
574561interface 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