feat(planner): edit the trip from its settings in the planner (#2811)

The trip form was mounted in the planner but nothing opened it, so title, dates,
cover and currency could only be changed from My Trips. Trip settings now lists
Edit Trip at the foot of its areas for anyone with trip_edit; it swaps the dialog
for the trip form. The phone already has Edit Trip in the More sheet.
This commit is contained in:
Maurice
2026-11-13 09:24:06 +01:00
parent 66d85729ff
commit 64b86c2403
40 changed files with 123 additions and 22 deletions
@@ -1,6 +1,6 @@
// FE-COMP-MEMBERS-001 to FE-COMP-MEMBERS-061
// FE-COMP-MEMBERS-001 to FE-COMP-MEMBERS-063
import type { Mock } from 'vitest';
import { act, render, screen, fireEvent, waitFor } from '../../../tests/helpers/render';
import { act, render, screen, fireEvent, waitFor, within } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import { delay, http, HttpResponse } from 'msw';
import { server } from '../../../tests/helpers/msw/server';
@@ -1107,4 +1107,24 @@ describe('TripMembersModal', () => {
expect(writeText).toHaveBeenCalledWith(expect.stringContaining('/join/inv9'));
view.unmount();
});
it('FE-COMP-MEMBERS-062: Edit Trip in the side column hands over to the trip form (#2811)', async () => {
const user = userEvent.setup();
const onEditTrip = vi.fn();
render(<TripMembersModal {...defaultProps} onEditTrip={onEditTrip} />);
// The column stands for it even where People is the only area.
const nav = await screen.findByRole('navigation', { name: 'Trip settings' });
await user.click(within(nav).getByRole('button', { name: 'Edit Trip' }));
expect(onEditTrip).toHaveBeenCalledTimes(1);
expect(within(nav).getByText('Title, dates, cover, description and currency.')).toBeInTheDocument();
});
it('FE-COMP-MEMBERS-063: without the right to edit the trip, the dialog offers no way to its form', async () => {
render(<TripMembersModal {...defaultProps} />);
expect(await screen.findByText('owner')).toBeInTheDocument();
expect(screen.queryByRole('button', { name: 'Edit Trip' })).toBeNull();
});
});
@@ -8,6 +8,8 @@ interface TripMembersModalProps {
/** Called after the roster changes (guest/member added, renamed or removed) so the
* planner can refresh its members for Costs participants, Collab, etc. */
onMembersChanged?: () => void
/** Opens the trip's form from the dialog, for someone who may edit the trip (#2811). */
onEditTrip?: () => void
}
/** Share: the trip's members, guests and links. The phone has its own sheet (MTripSettingsSheet) on the same hooks. */
+35 -12
View File
@@ -18,6 +18,8 @@ interface TripShareDialogProps {
tripId: number
tripTitle: string
onMembersChanged?: () => void
/** Hands over to the trip's own form; left out for someone who may not edit the trip (#2811). */
onEditTrip?: () => void
}
/** A question the dialog asks before it acts, answered in a ConfirmDialog over it. */
@@ -35,6 +37,8 @@ const BADGE = 'inline-flex flex-none items-center gap-1 rounded-full px-2 py-[2p
/** A fixed height on a wide screen, so the dialog does not jump when another area is picked. */
const BODY = 'flex min-h-0 flex-1 max-md:flex-col md:h-[min(640px,calc(100dvh-240px))]'
const NAV = 'flex flex-none gap-1 border-edge-faint bg-surface-secondary p-3 md:w-[236px] md:flex-col md:border-e max-md:overflow-x-auto max-md:border-b'
const NAV_ENTRY = 'flex flex-none items-center gap-2.5 rounded-[12px] px-2.5 py-2 text-start transition-colors'
const NAV_ENTRY_OFF = 'text-content-muted hover:bg-surface-hover hover:text-content'
const DANGER_BUTTON = 'inline-flex items-center gap-1.5 rounded-[10px] bg-surface-card px-3.5 py-2 font-medium text-danger shadow-sm ring-1 ring-edge-faint hover:bg-danger-soft disabled:cursor-default disabled:opacity-50'
/**
@@ -54,7 +58,7 @@ const AREAS: { id: Area; icon: (on: boolean) => ReactNode; labelKey: string; hin
{ id: 'journal', icon: () => <LinkedJournalIcon size={16} />, labelKey: 'trip.journal.title', hintKey: 'trip.journal.hint' },
]
function OpenDialog({ onClose, tripId, tripTitle, onMembersChanged }: TripShareDialogProps) {
function OpenDialog({ onClose, tripId, tripTitle, onMembersChanged, onEditTrip }: TripShareDialogProps) {
const { t } = useTranslation()
const labelId = useId()
const m = useTripMembers({ isOpen: true, tripId, onClose, onMembersChanged })
@@ -65,13 +69,15 @@ function OpenDialog({ onClose, tripId, tripTitle, onMembersChanged }: TripShareD
const current = areas.find(a => a.id === area) ?? areas[0]
const panelOf = (id: Area) => areas.find(a => a.id === id)
const persons = `${m.realMembers.length} ${t('members.persons', { count: m.realMembers.length })}`
// The way to the trip's form needs the side nav even where People is the only area.
const withNav = areas.length > 1 || !!onEditTrip
return (
<>
<DialogShell
onClose={onClose}
labelledBy={labelId}
width={areas.length > 1 ? 'wide' : 'detail'}
width={withNav ? 'wide' : 'detail'}
blocked={!!ask}
bodyClassName={BODY}
header={(
@@ -91,10 +97,10 @@ function OpenDialog({ onClose, tripId, tripTitle, onMembersChanged }: TripShareD
/>
)}
>
{areas.length > 1 && <AreaNav areas={areas} current={current.id} onPick={setArea} />}
{withNav && <AreaNav areas={areas} current={current.id} onPick={setArea} onEditTrip={onEditTrip} />}
<div className="min-h-0 flex-1 overflow-y-auto px-7 pb-7 pt-6 max-sm:px-5">
{/* Every area stays mounted, so its links and lists are loaded by the time it is picked. */}
<AreaPanel area={panelOf('people')} shown={current.id === 'people'} titled={areas.length > 1}>
<AreaPanel area={panelOf('people')} shown={current.id === 'people'} titled={withNav}>
{m.canManageMembers && <InviteUser m={m} />}
<MembersList m={m} onAsk={setAsk} />
{(m.isCurrentOwner || m.guests.length > 0) && <GuestsList m={m} onAsk={setAsk} />}
@@ -124,8 +130,11 @@ function OpenDialog({ onClose, tripId, tripTitle, onMembersChanged }: TripShareD
)
}
/** The areas down the side on a wide screen, a row of pills above the content on a narrow one. */
function AreaNav({ areas, current, onPick }: { areas: typeof AREAS; current: Area; onPick: (area: Area) => void }) {
/**
* The areas down the side on a wide screen, a row of pills above the content on a narrow one.
* Edit trip sits at the foot of the column: it opens the trip's form rather than an area of its own.
*/
function AreaNav({ areas, current, onPick, onEditTrip }: { areas: typeof AREAS; current: Area; onPick: (area: Area) => void; onEditTrip?: () => void }) {
const { t } = useTranslation()
return (
<nav aria-label={t('trip.settings.title')} className={NAV}>
@@ -133,19 +142,33 @@ function AreaNav({ areas, current, onPick }: { areas: typeof AREAS; current: Are
const on = a.id === current
return (
<button key={a.id} type="button" aria-current={on ? 'page' : undefined} aria-label={t(a.labelKey)} onClick={() => onPick(a.id)}
className={`flex flex-none items-center gap-2.5 rounded-[12px] px-2.5 py-2 text-start transition-colors ${on ? 'bg-surface-card text-content shadow-sm ring-1 ring-edge-faint' : 'text-content-muted hover:bg-surface-hover hover:text-content'}`}>
<span className={`grid h-8 w-8 flex-none place-items-center rounded-[9px] ${on ? 'bg-accent text-accent-text' : 'bg-surface-tertiary'}`}>{a.icon(on)}</span>
<span className="min-w-0">
<span className="block truncate font-semibold" style={fs(13, 'body')}>{t(a.labelKey)}</span>
<span className="block truncate text-content-faint max-md:hidden" style={fs(11)}>{t(a.hintKey)}</span>
</span>
className={`${NAV_ENTRY} ${on ? 'bg-surface-card text-content shadow-sm ring-1 ring-edge-faint' : NAV_ENTRY_OFF}`}>
<NavEntryText icon={a.icon(on)} on={on} label={t(a.labelKey)} hint={t(a.hintKey)} />
</button>
)
})}
{onEditTrip && (
<button type="button" aria-label={t('dashboard.editTrip')} onClick={onEditTrip} className={`${NAV_ENTRY} ${NAV_ENTRY_OFF} md:mt-auto`}>
<NavEntryText icon={<Pencil size={16} strokeWidth={2} />} on={false} label={t('dashboard.editTrip')} hint={t('trip.settings.editTripHint')} />
</button>
)}
</nav>
)
}
/** An entry's tile, its name and, on a wide screen, the line under the name. */
function NavEntryText({ icon, on, label, hint }: { icon: ReactNode; on: boolean; label: string; hint: string }) {
return (
<>
<span className={`grid h-8 w-8 flex-none place-items-center rounded-[9px] ${on ? 'bg-accent text-accent-text' : 'bg-surface-tertiary'}`}>{icon}</span>
<span className="min-w-0">
<span className="block truncate font-semibold" style={fs(13, 'body')}>{label}</span>
<span className="block truncate text-content-faint max-md:hidden" style={fs(11)}>{hint}</span>
</span>
</>
)
}
/** One area: its name and what it is for, then its parts. Hidden rather than unmounted while another one shows. */
function AreaPanel({ area, shown, titled, children }: { area: (typeof AREAS)[number] | undefined; shown: boolean; titled: boolean; children: ReactNode }) {
const { t } = useTranslation()
+2 -2
View File
@@ -92,7 +92,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
highlightedAlternative, setHighlightedAlternative,
moveRoadtripStopToDay,
dropPoiOnRoute,
showTripForm, setShowTripForm, showMembersModal, setShowMembersModal,
showTripForm, setShowTripForm, showMembersModal, setShowMembersModal, editTripFromSettings,
showReservationModal, setShowReservationModal, editingReservation, setEditingReservation,
showBookingImport, setShowBookingImport, bookingImportKind, setBookingImportKind, bookingImportAvailable,
airTrailAvailable, showAirTrailImport, setShowAirTrailImport,
@@ -830,7 +830,7 @@ function TripPlannerPageDesktop(): React.ReactElement | null {
trip={trip}
onCoverUpdate={applyTripCoverUpdate}
/>
<TripMembersModal isOpen={showMembersModal} onClose={() => setShowMembersModal(false)} tripId={tripId} tripTitle={trip?.title} onMembersChanged={refreshMembers} />
<TripMembersModal isOpen={showMembersModal} onClose={() => setShowMembersModal(false)} tripId={tripId} tripTitle={trip?.title} onMembersChanged={refreshMembers} onEditTrip={can('trip_edit', trip) ? editTripFromSettings : undefined} />
<ReservationModal isOpen={showReservationModal} onClose={() => { if (importReviewActive) { advanceImportReview() } else { setShowReservationModal(false); setEditingReservation(null); setBookingForAssignmentId(null) } }} onSave={async (data, steps) => { const r = await handleSaveReservation(data, steps); if (importReviewActive && r) advanceImportReview(); return r }} reservation={editingReservation} prefill={reservationPrefill} days={days} places={places} assignments={assignments} selectedDayId={selectedDayId} files={files} onFileUpload={canUploadFiles ? (fd) => tripActions.addFile(tripId, fd) : undefined} onFileDelete={(id) => tripActions.deleteFile(tripId, id)} accommodations={tripAccommodations} defaultAssignmentId={bookingForAssignmentId} onOpenExpense={openBookingExpense} tripMembers={tripMembers} />
{showTransportModal && (
<ErrorBoundary boundaryId="planner-panel:transport" fallback={null}>
@@ -206,6 +206,7 @@ function dialogState(): HookState {
setShowTripForm: vi.fn(),
showMembersModal: false,
setShowMembersModal: vi.fn(),
editTripFromSettings: vi.fn(),
showReservationModal: false,
setShowReservationModal: vi.fn(),
editingReservation: null,
@@ -1,4 +1,4 @@
// FE-PAGE-TPW-001 to FE-PAGE-TPW-074
// FE-PAGE-TPW-001 to FE-PAGE-TPW-075
//
// The planner page is a wiring container: everything stateful lives in
// useTripPlanner (covered in src/pages/tripPlanner/useTripPlanner.test.tsx).
@@ -1552,6 +1552,14 @@ describe('TripPlannerPage — modals', () => {
expect(hookState.setShowMembersModal).toHaveBeenCalledWith(false)
})
it('FE-PAGE-TPW-075: the members modal leads to the trip form only for someone who may edit the trip (#2811)', () => {
renderPage()
expect(props('membersModal').onEditTrip).toBe(hookState.editTripFromSettings)
renderPage({ can: vi.fn((action: string) => action !== 'trip_edit') })
expect(props('membersModal').onEditTrip).toBeUndefined()
})
it('FE-PAGE-TPW-050: closing the booking modal outside a review resets the editor', async () => {
renderPage()
@@ -1,4 +1,4 @@
// FE-TP-DIALOGS-001 to FE-TP-DIALOGS-012
// FE-TP-DIALOGS-001 to FE-TP-DIALOGS-013
//
// The planner's dialogs and the URL intents that open them, driven straight through
// usePlannerDialogs. The search params come in as options, so each test owns the URL
@@ -248,4 +248,14 @@ describe('usePlannerDialogs', () => {
act(() => { result.current.changeTransitRoute(buildReservation({ id: 15, type: 'transit' })) })
expect(result.current.transitPrefill).toEqual({ from: null, to: null })
})
it('FE-TP-DIALOGS-013: editing the trip from its settings swaps the settings for the trip form (#2811)', () => {
const { result } = renderDialogs()
act(() => { result.current.setShowMembersModal(true) })
act(() => { result.current.editTripFromSettings() })
expect(result.current.showMembersModal).toBe(false)
expect(result.current.showTripForm).toBe(true)
})
})
@@ -104,6 +104,12 @@ export function usePlannerDialogs(options: PlannerDialogsOptions) {
}, [searchParams])
const [showTripForm, setShowTripForm] = useState<boolean>(false)
const [showMembersModal, setShowMembersModal] = useState<boolean>(false)
// The trip settings hand over to the trip's form rather than stack a second dialog
// on their own: the planner's way to the title, dates, cover and currency (#2811).
const editTripFromSettings = useCallback(() => {
setShowMembersModal(false)
setShowTripForm(true)
}, [])
const [showReservationModal, setShowReservationModal] = useState<boolean>(false)
const [editingReservation, setEditingReservation] = useState<Reservation | null>(null)
const [showBookingImport, setShowBookingImport] = useState<boolean>(false)
@@ -167,7 +173,7 @@ export function usePlannerDialogs(options: PlannerDialogsOptions) {
editingAssignmentId, setEditingAssignmentId, placeFormDayId, setPlaceFormDayId,
placeFormPosition, setPlaceFormPosition, serviceStopForm, setServiceStopForm, serviceStopKind, setServiceStopKind,
stopDraft, setStopDraft, stayRelease, setStayRelease, reservationModalDayId, setReservationModalDayId,
showTripForm, setShowTripForm, showMembersModal, setShowMembersModal,
showTripForm, setShowTripForm, showMembersModal, setShowMembersModal, editTripFromSettings,
showReservationModal, setShowReservationModal, editingReservation, setEditingReservation,
showBookingImport, setShowBookingImport, bookingImportKind, setBookingImportKind,
bookingImportAvailable, setBookingImportAvailable, airTrailAvailable, showAirTrailImport, setShowAirTrailImport,
@@ -360,7 +360,7 @@ export function useTripPlanner() {
// The dialog state that only passes through to the shells.
const {
showPlaceForm, prefillCoords, reservationModalDayId, setReservationModalDayId,
showTripForm, setShowTripForm, showMembersModal, setShowMembersModal, showReservationModal,
showTripForm, setShowTripForm, showMembersModal, setShowMembersModal, editTripFromSettings, showReservationModal,
showBookingImport, setShowBookingImport, bookingImportKind, setBookingImportKind, bookingImportAvailable,
airTrailAvailable, showAirTrailImport, setShowAirTrailImport, bookingForAssignmentId, setBookingForAssignmentId,
showTransportModal, transportModalDayId, transportModalAutomated, setTransportModalAutomated,
@@ -749,7 +749,7 @@ export function useTripPlanner() {
highlightedAlternative, setHighlightedAlternative,
moveRoadtripStopToDay,
dropPoiOnRoute,
showTripForm, setShowTripForm, showMembersModal, setShowMembersModal,
showTripForm, setShowTripForm, showMembersModal, setShowMembersModal, editTripFromSettings,
showReservationModal, setShowReservationModal, editingReservation, setEditingReservation,
showBookingImport, setShowBookingImport, bookingImportKind, setBookingImportKind, bookingImportAvailable,
airTrailAvailable, showAirTrailImport, setShowAirTrailImport,
+1
View File
@@ -134,6 +134,7 @@ export function buildPlanner(overrides: Partial<TripPlanner> = {}): TripPlanner
setShowTripForm: vi.fn(),
showMembersModal: false,
setShowMembersModal: vi.fn(),
editTripFromSettings: vi.fn(),
showReservationModal: false,
setShowReservationModal: vi.fn(),
editingReservation: null,
+1
View File
@@ -127,6 +127,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'من المشاركون في هذه الرحلة، والضيوف بلا حساب.',
'trip.settings.sharing': 'المشاركة',
'trip.settings.sharingHint': 'رابط للقراءة فقط لأي شخص، ورابط دعوة للانضمام.',
'trip.settings.editTripHint': 'العنوان والتواريخ والغلاف والوصف والعملة.',
'trip.tabs.journal': 'اليوميات',
'trip.journal.title': 'اليوميات',
'trip.journal.hint': 'اربط إحدى يومياتك بهذه الرحلة. من تمنحه الوصول يفتحها هنا مباشرة في تبويب اليوميات، بشكل حي.',
+1
View File
@@ -118,6 +118,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Bu səyahətdə kimlərin olduğu və hesabı olmayan qonaqlar.',
'trip.settings.sharing': 'Paylaşma',
'trip.settings.sharingHint': 'Hər kəs üçün yalnız oxunan keçid və qoşulmaq üçün dəvət keçidi.',
'trip.settings.editTripHint': 'Ad, tarixlər, üz qabığı, təsvir və valyuta.',
'trip.tabs.journal': 'Gündəlik',
'trip.journal.title': 'Gündəlik',
'trip.journal.hint':
+1
View File
@@ -107,6 +107,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Quem está nesta viagem e os convidados sem conta.',
'trip.settings.sharing': 'Compartilhar',
'trip.settings.sharingHint': 'Um link somente leitura para qualquer pessoa e um link de convite para entrar.',
'trip.settings.editTripHint': 'Título, datas, capa, descrição e moeda.',
'trip.tabs.journal': 'Diário',
'trip.journal.title': 'Diário',
'trip.journal.hint':
+1
View File
@@ -107,6 +107,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Qui és en aquest viatge i els convidats sense compte.',
'trip.settings.sharing': 'Compartir',
'trip.settings.sharingHint': "Un enllaç de només lectura per a tothom i un enllaç d'invitació per unir-se.",
'trip.settings.editTripHint': 'Títol, dates, portada, descripció i moneda.',
'trip.tabs.journal': 'Diari',
'trip.journal.title': 'Diari',
'trip.journal.hint':
+1
View File
@@ -112,6 +112,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Kdo je na této cestě a hosté bez účtu.',
'trip.settings.sharing': 'Sdílení',
'trip.settings.sharingHint': 'Odkaz jen pro čtení pro kohokoli a pozvánka k připojení.',
'trip.settings.editTripHint': 'Název, termíny, obálka, popis a měna.',
'trip.tabs.journal': 'Deník',
'trip.journal.title': 'Deník',
'trip.journal.hint':
+1
View File
@@ -108,6 +108,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Wer auf dieser Reise ist, und Gäste ohne Konto.',
'trip.settings.sharing': 'Teilen',
'trip.settings.sharingHint': 'Ein Nur-Lesen-Link für alle und ein Einladungslink zum Beitreten.',
'trip.settings.editTripHint': 'Titel, Daten, Titelbild, Beschreibung und Währung.',
'trip.tabs.journal': 'Journal', // same-as-en
'trip.journal.title': 'Journal', // same-as-en
'trip.journal.hint':
+1
View File
@@ -107,6 +107,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Who is on this trip, and the guests without an account.',
'trip.settings.sharing': 'Sharing',
'trip.settings.sharingHint': 'A read-only link for anyone, and an invite link to join.',
'trip.settings.editTripHint': 'Title, dates, cover, description and currency.',
'trip.tabs.journal': 'Journal',
'trip.journal.title': 'Journal',
'trip.journal.hint':
+1
View File
@@ -107,6 +107,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Quién está en este viaje y los invitados sin cuenta.',
'trip.settings.sharing': 'Compartir',
'trip.settings.sharingHint': 'Un enlace de solo lectura para cualquiera y un enlace de invitación para unirse.',
'trip.settings.editTripHint': 'Título, fechas, portada, descripción y moneda.',
'trip.tabs.journal': 'Diario',
'trip.journal.title': 'Diario',
'trip.journal.hint':
+1
View File
@@ -107,6 +107,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Kes on sellel reisil, ja kontota külalised.',
'trip.settings.sharing': 'Jagamine',
'trip.settings.sharingHint': 'Kirjutuskaitstud link kõigile ja kutselink liitumiseks.',
'trip.settings.editTripHint': 'Pealkiri, kuupäevad, kaanepilt, kirjeldus ja valuuta.',
'trip.tabs.journal': 'Päevik',
'trip.journal.title': 'Päevik',
'trip.journal.hint':
+1
View File
@@ -108,6 +108,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Qui participe à ce voyage, et les invités sans compte.',
'trip.settings.sharing': 'Partage',
'trip.settings.sharingHint': "Un lien en lecture seule pour tous, et un lien d'invitation pour rejoindre.",
'trip.settings.editTripHint': 'Titre, dates, couverture, description et devise.',
'trip.tabs.journal': 'Journal', // same-as-en
'trip.journal.title': 'Journal', // same-as-en
'trip.journal.hint':
+1
View File
@@ -110,6 +110,7 @@ const trip: TranslationStrings = {
'trip.settings.sharing': 'Κοινοποίηση',
'trip.settings.sharingHint':
'Ένας σύνδεσμος μόνο για ανάγνωση για όλους και ένας σύνδεσμος πρόσκλησης για συμμετοχή.',
'trip.settings.editTripHint': 'Τίτλος, ημερομηνίες, εξώφυλλο, περιγραφή και νόμισμα.',
'trip.tabs.journal': 'Ημερολόγιο',
'trip.journal.title': 'Ημερολόγιο',
'trip.journal.hint':
+1
View File
@@ -111,6 +111,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'מי בטיול הזה, והאורחים שאין להם חשבון.',
'trip.settings.sharing': 'שיתוף',
'trip.settings.sharingHint': 'קישור לקריאה בלבד לכל אחד, וקישור הזמנה להצטרפות.',
'trip.settings.editTripHint': 'כותרת, תאריכים, תמונת שער, תיאור ומטבע.',
'trip.tabs.journal': 'יומן',
'trip.journal.title': 'יומן',
'trip.journal.hint':
+1
View File
@@ -110,6 +110,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Kik vannak ezen az utazáson, és a fiók nélküli vendégek.',
'trip.settings.sharing': 'Megosztás',
'trip.settings.sharingHint': 'Csak olvasható link bárkinek, és meghívó link a csatlakozáshoz.',
'trip.settings.editTripHint': 'Cím, dátumok, borítókép, leírás és pénznem.',
'trip.tabs.journal': 'Napló',
'trip.journal.title': 'Napló',
'trip.journal.hint':
+1
View File
@@ -103,6 +103,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Siapa saja dalam perjalanan ini, dan tamu tanpa akun.',
'trip.settings.sharing': 'Berbagi',
'trip.settings.sharingHint': 'Tautan hanya-baca untuk siapa saja, dan tautan undangan untuk bergabung.',
'trip.settings.editTripHint': 'Judul, tanggal, sampul, deskripsi, dan mata uang.',
'trip.tabs.journal': 'Jurnal',
'trip.journal.title': 'Jurnal',
'trip.journal.hint':
+1
View File
@@ -107,6 +107,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Chi partecipa a questo viaggio e gli ospiti senza account.',
'trip.settings.sharing': 'Condivisione',
'trip.settings.sharingHint': 'Un link di sola lettura per tutti e un link di invito per unirsi.',
'trip.settings.editTripHint': 'Titolo, date, copertina, descrizione e valuta.',
'trip.tabs.journal': 'Diario',
'trip.journal.title': 'Diario',
'trip.journal.hint':
+1
View File
@@ -100,6 +100,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'この旅行の参加者と、アカウントのないゲスト。',
'trip.settings.sharing': '共有',
'trip.settings.sharingHint': '誰でも見られる閲覧専用リンクと、参加用の招待リンク。',
'trip.settings.editTripHint': 'タイトル、日程、カバー画像、説明、通貨。',
'trip.tabs.journal': 'ジャーナル',
'trip.journal.title': 'ジャーナル',
'trip.journal.hint':
+1
View File
@@ -100,6 +100,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': '이 여행의 참여자와 계정 없는 게스트.',
'trip.settings.sharing': '공유',
'trip.settings.sharingHint': '누구나 볼 수 있는 읽기 전용 링크와 참여용 초대 링크.',
'trip.settings.editTripHint': '제목, 날짜, 커버 이미지, 설명, 통화.',
'trip.tabs.journal': '저널',
'trip.journal.title': '저널',
'trip.journal.hint':
+1
View File
@@ -107,6 +107,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Wie er op deze reis zijn, en gasten zonder account.',
'trip.settings.sharing': 'Delen',
'trip.settings.sharingHint': 'Een alleen-lezen-link voor iedereen en een uitnodigingslink om mee te doen.',
'trip.settings.editTripHint': 'Titel, data, omslagfoto, beschrijving en valuta.',
'trip.tabs.journal': 'Dagboek',
'trip.journal.title': 'Dagboek',
'trip.journal.hint':
+1
View File
@@ -117,6 +117,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Kto bierze udział w podróży oraz goście bez konta.',
'trip.settings.sharing': 'Udostępnianie',
'trip.settings.sharingHint': 'Link tylko do odczytu dla każdego i link z zaproszeniem do dołączenia.',
'trip.settings.editTripHint': 'Tytuł, daty, okładka, opis i waluta.',
'trip.tabs.journal': 'Dziennik',
'trip.journal.title': 'Dziennik',
'trip.journal.hint':
+1
View File
@@ -117,6 +117,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Кто участвует в поездке, и гости без аккаунта.',
'trip.settings.sharing': 'Доступ по ссылке',
'trip.settings.sharingHint': 'Ссылка только для чтения для всех и ссылка-приглашение для участия.',
'trip.settings.editTripHint': 'Название, даты, обложка, описание и валюта.',
'trip.tabs.journal': 'Дневник',
'trip.journal.title': 'Дневник',
'trip.journal.hint':
+1
View File
@@ -112,6 +112,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Kto je na tejto ceste a hostia bez účtu.',
'trip.settings.sharing': 'Zdieľanie',
'trip.settings.sharingHint': 'Odkaz len na čítanie pre kohokoľvek a pozvánka na pripojenie.',
'trip.settings.editTripHint': 'Názov, termíny, obálka, popis a mena.',
'trip.tabs.journal': 'Denník',
'trip.journal.title': 'Denník',
'trip.journal.hint':
+1
View File
@@ -107,6 +107,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Vilka som är med på resan, och gäster utan konto.',
'trip.settings.sharing': 'Dela',
'trip.settings.sharingHint': 'En skrivskyddad länk för alla och en inbjudningslänk för att gå med.',
'trip.settings.editTripHint': 'Titel, datum, omslagsbild, beskrivning och valuta.',
'trip.tabs.journal': 'Dagbok',
'trip.journal.title': 'Dagbok',
'trip.journal.hint':
+1
View File
@@ -102,6 +102,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'ใครอยู่ในทริปนี้ และแขกที่ไม่มีบัญชี',
'trip.settings.sharing': 'การแชร์',
'trip.settings.sharingHint': 'ลิงก์อ่านอย่างเดียวสำหรับทุกคน และลิงก์เชิญเพื่อเข้าร่วม',
'trip.settings.editTripHint': 'ชื่อ วันที่ ภาพปก คำอธิบาย และสกุลเงิน',
'trip.tabs.journal': 'บันทึก',
'trip.journal.title': 'บันทึก',
'trip.journal.hint': 'เชื่อมบันทึกของคุณเล่มหนึ่งเข้ากับทริปนี้ ผู้ที่คุณให้สิทธิ์จะเปิดได้ที่แท็บบันทึกตรงนี้ แบบสด',
+1
View File
@@ -108,6 +108,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Bu seyahatte kimlerin olduğu ve hesabı olmayan misafirler.',
'trip.settings.sharing': 'Paylaşım',
'trip.settings.sharingHint': 'Herkes için salt okunur bir bağlantı ve katılmak için davet bağlantısı.',
'trip.settings.editTripHint': 'Başlık, tarihler, kapak, açıklama ve para birimi.',
'trip.tabs.journal': 'Günlük',
'trip.journal.title': 'Günlük',
'trip.journal.hint':
+1
View File
@@ -117,6 +117,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Хто бере участь у подорожі, і гості без облікового запису.',
'trip.settings.sharing': 'Поширення',
'trip.settings.sharingHint': 'Посилання лише для читання для всіх і посилання-запрошення, щоб приєднатися.',
'trip.settings.editTripHint': 'Назва, дати, обкладинка, опис і валюта.',
'trip.tabs.journal': 'Щоденник',
'trip.journal.title': 'Щоденник',
'trip.journal.hint':
+1
View File
@@ -102,6 +102,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': 'Những ai trong chuyến đi này, và khách không có tài khoản.',
'trip.settings.sharing': 'Chia sẻ',
'trip.settings.sharingHint': 'Liên kết chỉ đọc cho mọi người và liên kết mời để tham gia.',
'trip.settings.editTripHint': 'Tiêu đề, ngày, ảnh bìa, mô tả và tiền tệ.',
'trip.tabs.journal': 'Nhật ký',
'trip.journal.title': 'Nhật ký',
'trip.journal.hint':
+1
View File
@@ -99,6 +99,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': '參與此旅行的人,以及沒有帳戶的訪客。',
'trip.settings.sharing': '分享',
'trip.settings.sharingHint': '任何人可用的唯讀連結,以及用於加入的邀請連結。',
'trip.settings.editTripHint': '標題、日期、封面、描述和貨幣。',
'trip.tabs.journal': '日誌',
'trip.journal.title': '日誌',
'trip.journal.hint': '將你的一本日誌連結到此旅行。獲得存取權限的人可在此處的日誌分頁中即時開啟它。',
+1
View File
@@ -99,6 +99,7 @@ const trip: TranslationStrings = {
'trip.settings.peopleHint': '参与此旅行的人,以及没有账户的访客。',
'trip.settings.sharing': '分享',
'trip.settings.sharingHint': '任何人可用的只读链接,以及用于加入的邀请链接。',
'trip.settings.editTripHint': '标题、日期、封面、描述和货币。',
'trip.tabs.journal': '日志',
'trip.journal.title': '日志',
'trip.journal.hint': '将你的一本日志关联到此旅行。获得访问权限的人可在此处的日志标签中实时打开它。',
+1 -1
View File
@@ -12,7 +12,7 @@ The short version: **the trip currency is the accounting base, the expense curre
## Trip currency
Every trip has exactly one currency. It is set when you create the trip and can be changed later in the same dialog: on **My Trips**, click the pencil on the trip card (**Edit Trip**). This needs the `trip_edit` permission. The **Trip settings** pill beside the trip name in the planner does not hold the currency; it only holds People, Sharing and Journal. The new-trip dialog pre-fills it with **your display currency**, falling back to **EUR** when you have left that on *Trip currency*. A trip created through MCP or by a plugin without a currency gets the same value. This is the one moment the display currency touches stored data: it seeds the field, and from then on the trip's currency is its own.
Every trip has exactly one currency. It is set when you create the trip and can be changed later in the same dialog: on **My Trips**, click the pencil on the trip card (**Edit Trip**), or in the planner open **Trip settings** and pick **Edit Trip** at the foot of its list. This needs the `trip_edit` permission. The new-trip dialog pre-fills it with **your display currency**, falling back to **EUR** when you have left that on *Trip currency*. A trip created through MCP or by a plugin without a currency gets the same value. This is the one moment the display currency touches stored data: it seeds the field, and from then on the trip's currency is its own.
This is the trip's **accounting base**. It is not a cosmetic label:
+2
View File
@@ -9,6 +9,8 @@
On a wide screen the areas are listed down the left side, each with a line saying what it is for; on a narrow one they are a row of pills above the content. The head of the dialog shows how many people and guests the trip has.
If you may edit the trip (`trip_edit`), **Edit Trip** sits at the foot of that list (at the end of the pills on a narrow screen). It closes Trip settings and opens the trip's own dialog, the same one as the pencil on the trip card in **My Trips**, so the title, description, dates, currency and cover can be changed without leaving the planner.
## People
*Who is on this trip, and the guests without an account.*