From d418d85d024eae6974e772348d39d63f80c9e43d Mon Sep 17 00:00:00 2001 From: Marek Maslowski Date: Fri, 3 Apr 2026 03:20:45 +0200 Subject: [PATCH] fixing selection of photos from multiple sources at once --- .../src/components/Memories/MemoriesPanel.tsx | 45 +++++++++++++------ server/src/routes/memories.ts | 32 ++++++++----- 2 files changed, 52 insertions(+), 25 deletions(-) diff --git a/client/src/components/Memories/MemoriesPanel.tsx b/client/src/components/Memories/MemoriesPanel.tsx index b288487..8787dd7 100644 --- a/client/src/components/Memories/MemoriesPanel.tsx +++ b/client/src/components/Memories/MemoriesPanel.tsx @@ -33,11 +33,12 @@ interface TripPhoto { city?: string | null } -interface ImmichAsset { +interface Asset { id: string takenAt: string city: string | null country: string | null + provider: string } interface MemoriesPanelProps { @@ -63,7 +64,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa // Photo picker const [showPicker, setShowPicker] = useState(false) - const [pickerPhotos, setPickerPhotos] = useState([]) + const [pickerPhotos, setPickerPhotos] = useState([]) const [pickerLoading, setPickerLoading] = useState(false) const [selectedIds, setSelectedIds] = useState>(new Set()) @@ -238,11 +239,16 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa const loadPickerPhotos = async (useDate: boolean) => { setPickerLoading(true) try { - const res = await apiClient.post(`${pickerIntegrationBase}/search`, { + const provider = availableProviders.find(p => p.id === selectedProvider) + if (!provider) { + setPickerPhotos([]) + return + } + const res = await apiClient.post(`/integrations/${provider.id}/search`, { from: useDate && startDate ? startDate : undefined, to: useDate && endDate ? endDate : undefined, }) - setPickerPhotos(res.data.assets || []) + setPickerPhotos((res.data.assets || []).map((asset: Asset) => ({ ...asset, provider: provider.id }))) } catch { setPickerPhotos([]) toast.error(t('memories.error.loadPhotos')) @@ -268,9 +274,17 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa const executeAddPhotos = async () => { setShowConfirmShare(false) try { + const groupedByProvider = new Map() + for (const key of selectedIds) { + const [provider, assetId] = key.split('::') + if (!provider || !assetId) continue + const list = groupedByProvider.get(provider) || [] + list.push(assetId) + groupedByProvider.set(provider, list) + } + await apiClient.post(`/integrations/memories/trips/${tripId}/photos`, { - provider: selectedProvider, - asset_ids: [...selectedIds], + selections: [...groupedByProvider.entries()].map(([provider, asset_ids]) => ({ provider, asset_ids })), shared: true, }) setShowPicker(false) @@ -312,6 +326,8 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa const thumbnailBaseUrl = (photo: TripPhoto) => `/api/integrations/${photo.provider}/assets/${photo.asset_id}/thumbnail?userId=${photo.user_id}` + const makePickerKey = (provider: string, assetId: string): string => `${provider}::${assetId}` + const ownPhotos = tripPhotos.filter(p => p.user_id === currentUser?.id) const othersPhotos = tripPhotos.filter(p => p.user_id !== currentUser?.id && p.shared) const allVisibleRaw = [...ownPhotos, ...othersPhotos] @@ -461,8 +477,8 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa if (showPicker) { const alreadyAdded = new Set( tripPhotos - .filter(p => p.user_id === currentUser?.id && p.provider === selectedProvider) - .map(p => p.asset_id) + .filter(p => p.user_id === currentUser?.id) + .map(p => makePickerKey(p.provider, p.asset_id)) ) return ( @@ -537,7 +553,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa ) : (() => { // Group photos by month - const byMonth: Record = {} + const byMonth: Record = {} for (const asset of pickerPhotos) { const d = asset.takenAt ? new Date(asset.takenAt) : null const key = d ? `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}` : 'unknown' @@ -555,11 +571,12 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa
{byMonth[month].map(asset => { - const isSelected = selectedIds.has(asset.id) - const isAlready = alreadyAdded.has(asset.id) + const pickerKey = makePickerKey(asset.provider, asset.id) + const isSelected = selectedIds.has(pickerKey) + const isAlready = alreadyAdded.has(pickerKey) return ( -
!isAlready && togglePickerSelect(asset.id)} +
!isAlready && togglePickerSelect(pickerKey)} style={{ position: 'relative', aspectRatio: '1', borderRadius: 8, overflow: 'hidden', cursor: isAlready ? 'default' : 'pointer', @@ -567,7 +584,7 @@ export default function MemoriesPanel({ tripId, startDate, endDate }: MemoriesPa outline: isSelected ? '3px solid var(--text-primary)' : 'none', outlineOffset: -3, }}> - {isSelected && (
{ const authReq = req as AuthRequest; const { tripId } = req.params; - const provider = String(req.body?.provider || '').toLowerCase(); const { shared = true } = req.body; + const selectionsRaw = Array.isArray(req.body?.selections) ? req.body.selections : null; + const provider = String(req.body?.provider || '').toLowerCase(); const assetIdsRaw = req.body?.asset_ids; if (!canAccessTrip(tripId, authReq.user.id)) { return res.status(404).json({ error: 'Trip not found' }); } - if (!provider) { - return res.status(400).json({ error: 'provider is required' }); - } + const selections = selectionsRaw && selectionsRaw.length > 0 + ? selectionsRaw + .map((selection: any) => ({ + provider: String(selection?.provider || '').toLowerCase(), + asset_ids: Array.isArray(selection?.asset_ids) ? selection.asset_ids : [], + })) + .filter((selection: { provider: string; asset_ids: unknown[] }) => selection.provider && selection.asset_ids.length > 0) + : (provider && Array.isArray(assetIdsRaw) && assetIdsRaw.length > 0 + ? [{ provider, asset_ids: assetIdsRaw }] + : []); - if (!Array.isArray(assetIdsRaw) || assetIdsRaw.length === 0) { - return res.status(400).json({ error: 'asset_ids required' }); + if (selections.length === 0) { + return res.status(400).json({ error: 'selections required' }); } const insert = db.prepare( @@ -95,11 +103,13 @@ router.post('/trips/:tripId/photos', authenticate, (req: Request, res: Response) ); let added = 0; - for (const raw of assetIdsRaw) { - const assetId = String(raw || '').trim(); - if (!assetId) continue; - const result = insert.run(tripId, authReq.user.id, assetId, provider, shared ? 1 : 0); - if (result.changes > 0) added++; + for (const selection of selections) { + for (const raw of selection.asset_ids) { + const assetId = String(raw || '').trim(); + if (!assetId) continue; + const result = insert.run(tripId, authReq.user.id, assetId, selection.provider, shared ? 1 : 0); + if (result.changes > 0) added++; + } } res.json({ success: true, added });