diff --git a/frontend/src/assets/tripLog.css b/frontend/src/assets/tripLog.css index bd22158e..1634f6dd 100644 --- a/frontend/src/assets/tripLog.css +++ b/frontend/src/assets/tripLog.css @@ -315,7 +315,7 @@ padding-inline: 0.55rem; } - .trip-log-item__label:not(.trip-log-item__label--kept) { + .trip-log-item__label { display: none; } } diff --git a/frontend/src/components/TripLogItem.vue b/frontend/src/components/TripLogItem.vue index eb176f55..eeb9fac0 100644 --- a/frontend/src/components/TripLogItem.vue +++ b/frontend/src/components/TripLogItem.vue @@ -13,7 +13,6 @@ import { endLabel, loggedDistanceKm, odometerDistanceKm, PURPOSE_ICONS } from '@ import { COMMUTE_PLACE_ICONS, commutePlaceAt } from '@/utils/commutePlaces' import type { CommutePlace } from '@/services/commutePlacesApi' import { useUnits } from '@/composables/useUnits' -import { useTwoTapConfirm } from '@/composables/useTwoTapConfirm' import ChipGroup from './ChipGroup.vue' import type { ChipOption } from '@/utils/chips' @@ -35,7 +34,7 @@ const emit = defineEmits<{ /** One of the trip's ends, to set as home or work. */ (e: 'pickPlace', latitude: number, longitude: number): void (e: 'setHidden', hidden: boolean): void - /** Join with the trip after it; confirmed with a second tap, since it cannot be undone. */ + /** Asks to join with the trip after it; the page confirms, since it cannot be undone. */ (e: 'joinNext'): void }>() @@ -107,12 +106,6 @@ function choosePurpose(purpose: TripPurpose | null) { emit('save', purpose, props.entry.notes) } -const joinConfirm = useTwoTapConfirm() - -function joinNext() { - if (joinConfirm.tap(true)) emit('joinNext') -} - function saveNotes() { const notes = normalizedNotes() if (notes === props.entry.notes) return @@ -242,23 +235,15 @@ function saveNotes() { diff --git a/frontend/src/components/__tests__/TripLogItem.spec.ts b/frontend/src/components/__tests__/TripLogItem.spec.ts index 85d84124..908fd866 100644 --- a/frontend/src/components/__tests__/TripLogItem.spec.ts +++ b/frontend/src/components/__tests__/TripLogItem.spec.ts @@ -210,18 +210,14 @@ describe('TripLogItem', () => { ).toBe(false) }) - it('joins with the next trip only on a second tap', async () => { + it('asks to join with the next trip', async () => { const wrapper = mountItem(logEntry()) - const join = () => - wrapper - .findAll('button') - .find((b) => b.text().startsWith('Join') || b.text().startsWith('Tap'))! - await join().trigger('click') - expect(wrapper.emitted('joinNext')).toBeUndefined() - expect(join().text()).toBe('Tap again to join') + await wrapper + .findAll('button') + .find((b) => b.text() === 'Join with next trip')! + .trigger('click') - await join().trigger('click') expect(wrapper.emitted('joinNext')).toHaveLength(1) }) diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 92ce1f99..6b2687dd 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -781,7 +781,7 @@ "show": "Show again", "hideHint": "Leave this trip out of the map, the statistics, the dashboard and these totals, for a positioning glitch. It stays listed under the hidden trips.", "joinNext": "Join with next trip", - "joinNextConfirm": "Tap again to join", + "joinNextConfirm": "Join trips", "joinNextHint": "Joins this trip with the one after it, for a drive a short stop such as a fuel station split in two. Cannot be undone.", "showHidden": "Show hidden trip | Show {n} hidden trips" }, diff --git a/frontend/src/locales/nl.json b/frontend/src/locales/nl.json index 46d9891e..e202fb14 100644 --- a/frontend/src/locales/nl.json +++ b/frontend/src/locales/nl.json @@ -781,7 +781,7 @@ "show": "Weer tonen", "hideHint": "Laat deze rit weg uit de kaart, de statistieken, het dashboard en deze totalen, bij een fout in de positie. Hij blijft staan bij de verborgen ritten.", "joinNext": "Samenvoegen met volgende rit", - "joinNextConfirm": "Tik nogmaals om samen te voegen", + "joinNextConfirm": "Ritten samenvoegen", "joinNextHint": "Voegt deze rit samen met de rit erna, voor een rit die door een korte stop, zoals bij een tankstation, in tweeën is gesplitst. Kan niet ongedaan worden gemaakt.", "showHidden": "Verborgen rit tonen | {n} verborgen ritten tonen" }, diff --git a/frontend/src/views/TripLogView.vue b/frontend/src/views/TripLogView.vue index 970d1ebf..6745e08a 100644 --- a/frontend/src/views/TripLogView.vue +++ b/frontend/src/views/TripLogView.vue @@ -8,6 +8,7 @@ import { useUiSettingsStore } from '@/stores/settingsUi' import StatusCard from '@/components/StatusCard.vue' import TripLogItem from '@/components/TripLogItem.vue' import CommutePlacesModal from '@/components/CommutePlacesModal.vue' +import DetailModal from '@/components/DetailModal.vue' import { useCommutePlacesStore } from '@/stores/commutePlaces' import { TRIP_PURPOSES, type TripPurpose } from '@/services/tripLogApi' import { downloadCsv } from '@/utils/download' @@ -121,8 +122,17 @@ function setHidden(id: number, hidden: boolean) { if (vin.value) void store.setHidden(vin.value, id, hidden) } -function joinNext(id: number) { - if (vin.value) void store.mergeWithNext(vin.value, id) +// Joining cannot be undone, so it is asked for in a dialog rather than on the trip's own row. +const joiningId = ref(null) +const joinBusy = computed(() => joiningId.value !== null && store.saving.has(joiningId.value)) + +function cancelJoin() { + if (!joinBusy.value) joiningId.value = null +} + +async function confirmJoin() { + if (vin.value && joiningId.value !== null) await store.mergeWithNext(vin.value, joiningId.value) + joiningId.value = null } function classifyAll(purpose: TripPurpose) { @@ -234,10 +244,27 @@ function exportCsv() { @save="(purpose, notes) => save(entry.id, purpose, notes)" @pick-place="openPlaces" @set-hidden="(hidden) => setHidden(entry.id, hidden)" - @join-next="joinNext(entry.id)" + @join-next="joiningId = entry.id" /> + +

{{ t('tripLog.joinNextHint') }}

+ +
+ ({ vi.mock('@/utils/download', () => ({ downloadCsv: vi.fn() })) +// The page looks up addresses in rounds with pauses between them, and stops when it closes; left +// open, a test's page would carry on asking in the tests after it. +enableAutoUnmount(afterEach) + const i18n = createI18n({ legacy: false, locale: 'en', messages: { en } }) const car = { id: 1, vin: 'FAKEVN00000000001', vehicleType: 'hev' } as Vehicle @@ -57,7 +61,7 @@ async function mountView() { const wrapper = mount(TripLogView, { global: { plugins: [i18n, pinia], - stubs: { FontAwesomeIcon: true, CommutePlacesModal: true }, + stubs: { FontAwesomeIcon: true, CommutePlacesModal: true, teleport: true }, }, }) await flushPromises() @@ -129,18 +133,30 @@ describe('TripLogView', () => { expect(wrapper.find('.trip-log-totals').text()).toContain('Unclassified (0)') }) - it('joins a trip with the next one on a second tap, and offers none for the newest', async () => { + it('joins a trip with the next one once confirmed, and offers none for the newest', async () => { vi.mocked(tripLogApi.mergeWithNext).mockResolvedValue(logEntry({ id: 1, distanceKm: 80 })) const wrapper = await mountView() const items = wrapper.findAll('.trip-log-item') expect(items[0]!.text()).not.toContain('Join with next trip') - const join = () => items[1]!.findAll('button').find((b) => /join/i.test(b.text()))! - await join().trigger('click') - await join().trigger('click') + await button(wrapper, 'Join with next trip')[0]!.trigger('click') + expect(tripLogApi.mergeWithNext).not.toHaveBeenCalled() + + await button(wrapper, 'Join trips')[0]!.trigger('click') await flushPromises() expect(tripLogApi.mergeWithNext).toHaveBeenCalledWith(car.vin, 1) + expect(button(wrapper, 'Join trips')).toHaveLength(0) + }) + + it('leaves the trips as they are when the join is cancelled', async () => { + const wrapper = await mountView() + + await button(wrapper, 'Join with next trip')[0]!.trigger('click') + await button(wrapper, 'Cancel')[0]!.trigger('click') + + expect(tripLogApi.mergeWithNext).not.toHaveBeenCalled() + expect(button(wrapper, 'Join trips')).toHaveLength(0) }) it('hides a trip, which then leaves the list', async () => {