From 477f41693d524da457f560394546d52c468cc451 Mon Sep 17 00:00:00 2001 From: Maurice Date: Thu, 1 Oct 2026 13:03:46 +0200 Subject: [PATCH] feat(journey): set a journey's status by hand (#762) --- .../JourneyDetailPageSettingsDialog.test.tsx | 16 ++++++++ .../JourneyDetailPageSettingsDialog.tsx | 40 ++++++++++++++++++- client/src/pages/JourneyDetailPage.tsx | 2 +- client/src/pages/JourneyPage.tsx | 2 +- client/src/pages/journey/useJourney.ts | 4 +- client/src/store/journeyStore.ts | 2 + client/src/utils/journeyLifecycle.test.ts | 12 ++++++ client/src/utils/journeyLifecycle.ts | 5 +++ server/src/db/migrations.ts | 9 +++++ .../nest/journey/journey-domain.service.ts | 5 +++ server/src/nest/journey/journey.mcp.ts | 5 ++- .../unit/nest/journey-domain.service.test.ts | 9 +++++ shared/src/i18n/ar/journey.ts | 4 ++ shared/src/i18n/az/journey.ts | 4 ++ shared/src/i18n/br/journey.ts | 4 ++ shared/src/i18n/ca/journey.ts | 4 ++ shared/src/i18n/cs/journey.ts | 4 ++ shared/src/i18n/de/journey.ts | 4 ++ shared/src/i18n/en/journey.ts | 4 ++ shared/src/i18n/es/journey.ts | 4 ++ shared/src/i18n/et/journey.ts | 4 ++ shared/src/i18n/fr/journey.ts | 4 ++ shared/src/i18n/gr/journey.ts | 4 ++ shared/src/i18n/hu/journey.ts | 4 ++ shared/src/i18n/id/journey.ts | 4 ++ shared/src/i18n/it/journey.ts | 4 ++ shared/src/i18n/ja/journey.ts | 4 ++ shared/src/i18n/ko/journey.ts | 4 ++ shared/src/i18n/nl/journey.ts | 4 ++ shared/src/i18n/pl/journey.ts | 4 ++ shared/src/i18n/ru/journey.ts | 4 ++ shared/src/i18n/sk/journey.ts | 4 ++ shared/src/i18n/sv/journey.ts | 4 ++ shared/src/i18n/th/journey.ts | 4 ++ shared/src/i18n/tr/journey.ts | 4 ++ shared/src/i18n/uk/journey.ts | 4 ++ shared/src/i18n/vi/journey.ts | 4 ++ shared/src/i18n/zh-TW/journey.ts | 4 ++ shared/src/i18n/zh/journey.ts | 4 ++ 39 files changed, 212 insertions(+), 7 deletions(-) diff --git a/client/src/components/Journey/JourneyDetailPageSettingsDialog.test.tsx b/client/src/components/Journey/JourneyDetailPageSettingsDialog.test.tsx index 686154017..b93a146b0 100644 --- a/client/src/components/Journey/JourneyDetailPageSettingsDialog.test.tsx +++ b/client/src/components/Journey/JourneyDetailPageSettingsDialog.test.tsx @@ -199,6 +199,22 @@ describe('JourneySettingsDialog', () => { expect(onSaved).toHaveBeenCalledTimes(1) }) + it('FE-JRN-SETTINGS-762: sets the status by hand on the spot and back to automatic', async () => { + const user = userEvent.setup() + const { onRefresh } = mountDialog() + expect(screen.getByText('Follows the dates of the linked trips. Without a trip the journey stays a draft.')).toBeInTheDocument() + + await user.click(screen.getByRole('button', { name: 'Live' })) + await waitFor(() => expect(updateJourney).toHaveBeenCalledWith(3, { status_override: 'live' })) + expect(onRefresh).toHaveBeenCalled() + + updateJourney.mockClear() + mountDialog(buildJourney({ status_override: 'completed' })) + expect(screen.getAllByRole('button', { name: 'Completed' }).some(b => b.getAttribute('aria-pressed') === 'true')).toBe(true) + await user.click(screen.getAllByRole('button', { name: 'Automatic' })[1]) + await waitFor(() => expect(updateJourney).toHaveBeenCalledWith(3, { status_override: null })) + }) + it('FE-JRN-SETTINGS-011: restores an archived journey', async () => { const user = userEvent.setup() mountDialog(buildJourney({ status: 'archived' })) diff --git a/client/src/components/Journey/JourneyDetailPageSettingsDialog.tsx b/client/src/components/Journey/JourneyDetailPageSettingsDialog.tsx index d68be858b..46b6f4468 100644 --- a/client/src/components/Journey/JourneyDetailPageSettingsDialog.tsx +++ b/client/src/components/Journey/JourneyDetailPageSettingsDialog.tsx @@ -8,7 +8,7 @@ import { useToast } from '../shared/Toast' import ConfirmDialog from '../shared/ConfirmDialog' import { Tooltip } from '../shared/Tooltip' import { DeleteButton, DialogButton, DialogFooter, DialogHeader, DialogSection, DialogShell, DialogTile, FooterSpacer, NEUTRAL_TINT, fs } from '../shared/DialogShell' -import { AddRowButton, EditorField, INPUT } from '../shared/dialogParts' +import { AddRowButton, EditorField, INPUT, Segmented } from '../shared/dialogParts' import JourneyShareSection from './JourneyShareSection' import type { JourneyContributor, JourneyDetail } from '../../store/journeyStore' import { pickGradient } from '../../pages/journeyDetail/JourneyDetailPage.helpers' @@ -145,6 +145,23 @@ export function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite, } } + // The state shown for the journey, set by hand (#762). Saved on the spot like + // the switches; 'auto' stores null and hands it back to the trip dates. + const [savingStatus, setSavingStatus] = useState(false) + const statusChoice = journey.status_override ?? 'auto' + const handleStatusChange = async (next: 'auto' | 'draft' | 'live' | 'completed') => { + if (next === statusChoice || savingStatus) return + setSavingStatus(true) + try { + await updateJourney(journey.id, { status_override: next === 'auto' ? null : next }) + onRefresh() + } catch { + toast.error(t('journey.settings.saveFailed')) + } finally { + setSavingStatus(false) + } + } + const handleDelete = async () => { try { await deleteJourney(journey.id) @@ -246,6 +263,27 @@ export function JourneySettingsDialog({ journey, onClose, onSaved, onOpenInvite, /> + {/* The journey's state, by hand or from the trips (#762) */} + +
+ + label={t('journey.settings.status')} + value={statusChoice} + onChange={next => { void handleStatusChange(next) }} + fill + options={[ + { value: 'auto', label: t('journey.settings.statusAuto') }, + { value: 'draft', label: t('journey.status.draft') }, + { value: 'live', label: t('journey.frontpage.live') }, + { value: 'completed', label: t('journey.status.completed') }, + ]} + /> +
+

+ {t(statusChoice === 'auto' ? 'journey.settings.statusAutoHint' : 'journey.settings.statusManualHint')} +

+
+ {/* Trip GPX tracks on the journey map (#2194) */}
diff --git a/client/src/pages/JourneyDetailPage.tsx b/client/src/pages/JourneyDetailPage.tsx index c1d723ce4..70bbd7a20 100644 --- a/client/src/pages/JourneyDetailPage.tsx +++ b/client/src/pages/JourneyDetailPage.tsx @@ -106,7 +106,7 @@ function JourneyDetailPageDesktop() { : [] const sortedDates = [...new Set([...dayGroups.keys(), ...suggestionDates])] .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0)) - const lifecycle = computeJourneyLifecycle(current.status, tripDateMin || null, tripDateMax || null) + const lifecycle = computeJourneyLifecycle(current.status, tripDateMin || null, tripDateMax || null, current.status_override) const showMobileCombined = isMobile && view === 'timeline' const showMobileGallery = isMobile && view === 'gallery' diff --git a/client/src/pages/JourneyPage.tsx b/client/src/pages/JourneyPage.tsx index 2c7128d99..d58fc6dcd 100644 --- a/client/src/pages/JourneyPage.tsx +++ b/client/src/pages/JourneyPage.tsx @@ -419,7 +419,7 @@ function JourneyCard({ journey, onClick }: { journey: Journey & { entry_count?: const entryCount = j.entry_count ?? 0 const photoCount = j.photo_count ?? 0 const placeCount = j.place_count ?? 0 - const lifecycle = computeJourneyLifecycle(j.status, j.trip_date_min, j.trip_date_max) + const lifecycle = computeJourneyLifecycle(j.status, j.trip_date_min, j.trip_date_max, j.status_override) return (