diff --git a/client/src/components/Journey/JourneyDetailPageEntryEditor.test.tsx b/client/src/components/Journey/JourneyDetailPageEntryEditor.test.tsx index 7607acd7a..8f5fa14d5 100644 --- a/client/src/components/Journey/JourneyDetailPageEntryEditor.test.tsx +++ b/client/src/components/Journey/JourneyDetailPageEntryEditor.test.tsx @@ -836,4 +836,46 @@ describe('EntryEditor', () => { expect(amazing.className).toContain('border-zinc-200') expect(amazing.getAttribute('style')).toBe('') }) + + // #1614 — entry_time was stored, sent and displayed everywhere, but the desktop + // editor never rendered an input for it. + it('FE-JRN-EDITOR-041: shows the stored time and saves an edited one', async () => { + const user = userEvent.setup() + const { onSave } = mountEditor(buildEntry({ id: 10, title: 'Old', entry_time: '14:30:00' })) + + // The column carries HH:MM:SS; a time input only accepts HH:MM. + const timeInput = screen.getByDisplayValue('14:30') + expect(timeInput).toHaveAttribute('type', 'time') + + fireEvent.change(timeInput, { target: { value: '09:05' } }) + await user.click(screen.getByRole('button', { name: 'Save' })) + + await waitFor(() => expect(onSave).toHaveBeenCalled()) + expect(onSave.mock.calls[0][0]).toEqual(expect.objectContaining({ entry_time: '09:05' })) + }) + + it('FE-JRN-EDITOR-042: clearing the time sends null rather than an empty string', async () => { + const user = userEvent.setup() + const { onSave } = mountEditor(buildEntry({ id: 10, entry_time: '14:30:00' })) + + fireEvent.change(screen.getByDisplayValue('14:30'), { target: { value: '' } }) + await user.click(screen.getByRole('button', { name: 'Save' })) + + await waitFor(() => expect(onSave).toHaveBeenCalled()) + expect(onSave.mock.calls[0][0]).toEqual(expect.objectContaining({ entry_time: null })) + }) + + it('FE-JRN-EDITOR-043: the camera route is its own input, so the library picker survives', () => { + const { container } = mountEditor(buildEntry({ id: 10 })) + + const inputs = Array.from(container.querySelectorAll('input[type="file"]')) + const camera = inputs.filter(i => i.getAttribute('capture') === 'environment') + const library = inputs.filter(i => !i.hasAttribute('capture')) + + expect(camera).toHaveLength(1) + expect(library).toHaveLength(1) + // The library picker keeps multi-select; forcing capture onto it would drop that. + expect(library[0]).toHaveAttribute('multiple') + expect(camera[0]).not.toHaveAttribute('multiple') + }) }) diff --git a/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx b/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx index 46e2f188b..7cba4e956 100644 --- a/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx +++ b/client/src/components/Journey/JourneyDetailPageEntryEditor.tsx @@ -1,5 +1,5 @@ import { useEffect, useState, useRef } from 'react' -import { X, Plus, Image, Minus, Check, MapPin, Locate } from 'lucide-react' +import { X, Plus, Image, Minus, Check, MapPin, Locate, Camera } from 'lucide-react' import { normalizeImageFiles } from '../../utils/convertHeic' import { type ResilientResult, type UploadProgress } from '../../utils/uploadQueue' import { useTranslation } from '../../i18n' @@ -34,7 +34,7 @@ export function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, trips, const [title, setTitle] = useState(entry.title || '') const [story, setStory] = useState(entry.story || '') const [entryDate, setEntryDate] = useState(entry.entry_date || new Date().toISOString().split('T')[0]) - const [entryTime, setEntryTime] = useState(entry.entry_time || '') + const [entryTime, setEntryTime] = useState(entry.entry_time?.slice(0, 5) || '') const [locationName, setLocationName] = useState(entry.location_name || '') const [locationLat, setLocationLat] = useState(entry.location_lat ?? null) const [locationLng, setLocationLng] = useState(entry.location_lng ?? null) @@ -60,6 +60,10 @@ export function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, trips, const [externalProvider, setExternalProvider] = useState(null) const [pendingProviderGroups, setPendingProviderGroups] = useState([]) const fileRef = useRef(null) + // Own input: putting `capture` on the picker above would take the photo library + // away on a phone, which is the more common way in. This one only ever opens the + // camera, so tablets and laptops get the same route the phone sheet already has. + const cameraRef = useRef(null) const storyRef = useRef(null) const persistedEntryIdRef = useRef(entry.id > 0 ? entry.id : null) @@ -71,7 +75,7 @@ export function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, trips, title !== (entry.title || '') || story !== (entry.story || '') || entryDate !== (entry.entry_date || new Date().toISOString().split('T')[0]) || - entryTime !== (entry.entry_time || '') || + entryTime !== (entry.entry_time?.slice(0, 5) || '') || locationName !== (entry.location_name || '') || (locationLat ?? null) !== (entry.location_lat ?? null) || (locationLng ?? null) !== (entry.location_lng ?? null) || @@ -258,6 +262,7 @@ export function EntryEditor({ entry, journeyId, tripDates, galleryPhotos, trips,
{ (e.target as HTMLInputElement).value = '' }} className="hidden" /> + { (e.target as HTMLInputElement).value = '' }} className="hidden" />
)} +
-
- - + {/* Time sat in state and went to the server, it just had no input here — so a + draft's auto-stamped clock time showed up in the timeline, the map, the PDF + and the public share, and the desktop had no way to correct it (#1614). */} +
+
+ + +
+
+ + setEntryTime(e.target.value)} + className="w-full h-[42px] px-3 rounded-xl border border-zinc-200 dark:border-zinc-700 bg-transparent text-[13px] text-zinc-900 dark:text-white outline-none focus:border-zinc-400 dark:focus:border-zinc-500 [font-variant-numeric:tabular-nums]" + /> +