diff --git a/client/src/components/Journey/JourneyDetailPageGalleryView.test.tsx b/client/src/components/Journey/JourneyDetailPageGalleryView.test.tsx index 87294107f..72cd6642a 100644 --- a/client/src/components/Journey/JourneyDetailPageGalleryView.test.tsx +++ b/client/src/components/Journey/JourneyDetailPageGalleryView.test.tsx @@ -1,7 +1,7 @@ // FE-JRN-GALLERY-001 to FE-JRN-GALLERY-016 import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' -import { act } from 'react' +import { act, useRef, useState } from 'react' import { http, HttpResponse } from 'msw' import userEvent from '@testing-library/user-event' import { render, screen, waitFor, fireEvent } from '../../../tests/helpers/render' @@ -73,20 +73,41 @@ const entries: JourneyEntry[] = [ }, ] +/** Mirrors the page wiring: the gallery reports its connected providers up, + and the host renders one button per provider next to Upload. */ +function GalleryHarness({ gallery, onPhotoClick, onRefresh, onRegisterUpload }: { + gallery: GalleryPhoto[] + onPhotoClick: (photos: GalleryPhoto[], index: number) => void + onRefresh: () => void + onRegisterUpload?: (fn: () => void) => void +}) { + const [providers, setProviders] = useState<{ id: string; name: string }[]>([]) + const browseRef = useRef<((provider: string) => void) | null>(null) + return ( + <> + {providers.map(p => ( + + ))} + { setProviders(p); browseRef.current = browse }} + /> + + ) +} + function mountGallery(gallery: GalleryPhoto[], onRegisterUpload?: (fn: () => void) => void) { const onPhotoClick = vi.fn() const onRefresh = vi.fn() const utils = render( - , + , ) return { ...utils, onPhotoClick, onRefresh } } diff --git a/client/src/components/Journey/JourneyDetailPageGalleryView.tsx b/client/src/components/Journey/JourneyDetailPageGalleryView.tsx index 883bb442b..a5783b1ea 100644 --- a/client/src/components/Journey/JourneyDetailPageGalleryView.tsx +++ b/client/src/components/Journey/JourneyDetailPageGalleryView.tsx @@ -1,5 +1,5 @@ import { useEffect, useState, useRef } from 'react' -import { RefreshCw, Camera, Image, X, Play } from 'lucide-react' +import { RefreshCw, Camera, X, Play } from 'lucide-react' import { normalizeImageFiles } from '../../utils/convertHeic' import { isVideoFile } from '../../utils/videoPoster' import { useJourneyStore } from '../../store/journeyStore' @@ -13,7 +13,7 @@ import { ProviderPicker } from './JourneyDetailPageProviderPicker' import { ScrollTrigger } from './JourneyDetailPageScrollTrigger' import EmptyState from '../shared/EmptyState' -export function GalleryView({ entries, gallery, journeyId, userId, trips, onPhotoClick, onRefresh, onRegisterUpload }: { +export function GalleryView({ entries, gallery, journeyId, userId, trips, onPhotoClick, onRefresh, onRegisterUpload, onRegisterProviders }: { entries: JourneyEntry[] gallery: GalleryPhoto[] journeyId: number @@ -22,16 +22,17 @@ export function GalleryView({ entries, gallery, journeyId, userId, trips, onPhot onPhotoClick: (photos: GalleryPhoto[], index: number) => void onRefresh: () => void onRegisterUpload?: (fn: () => void) => void + onRegisterProviders?: (providers: { id: string; name: string }[], browse: (provider: string) => void) => void }) { const { t } = useTranslation() const [showPicker, setShowPicker] = useState(false) const [pickerProvider, setPickerProvider] = useState(null) - const [availableProviders, setAvailableProviders] = useState<{ id: string; name: string }[]>([]) const [galleryProgress, setGalleryProgress] = useState<{ done: number; total: number } | null>(null) const galleryUploading = galleryProgress !== null const toast = useToast() - // check which providers are enabled AND connected for the current user + // check which providers are enabled AND connected for the current user, then + // hand them up: the page header renders the provider buttons next to Upload useEffect(() => { (async () => { try { @@ -46,7 +47,7 @@ export function GalleryView({ entries, gallery, journeyId, userId, trips, onPhot if (status.connected) connected.push({ id: p.id, name: p.name }) } catch { } } - setAvailableProviders(connected) + onRegisterProviders?.(connected, browseProvider) } catch { } })() }, []) @@ -126,23 +127,11 @@ export function GalleryView({ entries, gallery, journeyId, userId, trips, onPhot
- {/* Header */} + {/* Header — the provider buttons live in the page header next to Upload */}
{allPhotos.length} {t('journey.detail.photos')} -
- {availableProviders.map(p => ( - - ))} -
{allPhotos.length === 0 ? ( diff --git a/client/src/pages/JourneyDetailPage.tsx b/client/src/pages/JourneyDetailPage.tsx index 14a0d1a0c..6c63a28f2 100644 --- a/client/src/pages/JourneyDetailPage.tsx +++ b/client/src/pages/JourneyDetailPage.tsx @@ -10,7 +10,7 @@ import EmptyState from '../components/shared/EmptyState' import { Outlet } from 'react-router' import { ArrowLeft, MoreHorizontal, List, Grid, MapPin, - Plus, ChevronUp, ChevronDown, Eye, EyeOff, BookOpen, + Plus, ChevronUp, ChevronDown, Eye, EyeOff, BookOpen, Image, } from 'lucide-react' import MobileMapTimeline from '../components/Journey/MobileMapTimeline' import MobileEntryView from '../components/Journey/MobileEntryView' @@ -44,7 +44,8 @@ function JourneyDetailPageDesktop() { showInvite, setShowInvite, showAddTrip, setShowAddTrip, unlinkTrip, setUnlinkTrip, showSettings, setShowSettings, hideSkeletons, setHideSkeletons, - mapRef, fullMapRef, galleryUploadRef, activeLocationId, handleMarkerClick, handleLocationClick, + mapRef, fullMapRef, galleryUploadRef, galleryProviders, setGalleryProviders, galleryBrowseRef, + activeLocationId, handleMarkerClick, handleLocationClick, mapEntries, sidebarMapItems, tripDates, isMobile, tracks, feedEdge, scrollFeedTo, loadJourney, updateEntry, deleteEntry, reorderEntries, uploadPhotos, deletePhoto, @@ -346,14 +347,27 @@ function JourneyDetailPageDesktop() { )} {canEditEntries && view === 'gallery' && ( - +
+ {galleryProviders.map(p => ( + + ))} + +
)}
@@ -452,6 +466,7 @@ function JourneyDetailPageDesktop() { > { galleryUploadRef.current = fn }} + onRegisterProviders={(providers, browse) => { setGalleryProviders(providers); galleryBrowseRef.current = browse }} entries={current.entries} gallery={current.gallery || []} journeyId={current.id} diff --git a/client/src/pages/journeyDetail/useJourneyDetail.ts b/client/src/pages/journeyDetail/useJourneyDetail.ts index a41d0f6c4..0efa6bf48 100644 --- a/client/src/pages/journeyDetail/useJourneyDetail.ts +++ b/client/src/pages/journeyDetail/useJourneyDetail.ts @@ -31,6 +31,11 @@ export function useJourneyDetail() { const fullMapRef = useRef(null) /** The gallery hands its file picker up here, so the hero button can open it. */ const galleryUploadRef = useRef<(() => void) | null>(null) + /** Same deal for photo providers: the gallery probes which ones are + connected and reports them up, so the header can render an Immich/ + Synology button next to Upload. */ + const [galleryProviders, setGalleryProviders] = useState<{ id: string; name: string }[]>([]) + const galleryBrowseRef = useRef<((provider: string) => void) | null>(null) const [activeLocationId, setActiveLocationId] = useState(null) const isMobile = useIsMobile() @@ -384,7 +389,8 @@ export function useJourneyDetail() { showInvite, setShowInvite, showAddTrip, setShowAddTrip, unlinkTrip, setUnlinkTrip, showSettings, setShowSettings, hideSkeletons, setHideSkeletons, - mapRef, fullMapRef, galleryUploadRef, activeLocationId, handleMarkerClick, handleLocationClick, + mapRef, fullMapRef, galleryUploadRef, galleryProviders, setGalleryProviders, galleryBrowseRef, + activeLocationId, handleMarkerClick, handleLocationClick, mapEntries, sidebarMapItems, tripDates, isMobile, tracks, feedEdge, scrollFeedTo, loadJourney, updateEntry, deleteEntry, reorderEntries, uploadPhotos, deletePhoto,