feat(places): say when a place has an address but no coordinates (#2837)

A place saved with a typed address and no search pick, link or map point gets no
coordinates, so it never shows on the map, and nothing said why. The place form warns
under the address while that is so, and the inspector, the phone's place sheet and
both place lists mark such a place. No geocoding on save; the form stays unchanged
for a service stop, which explains a missing position itself.
This commit is contained in:
Maurice
2026-11-13 09:37:06 +01:00
parent 0eac3a9f49
commit 5176dc29d6
44 changed files with 401 additions and 59 deletions
@@ -0,0 +1,39 @@
import { fireEvent, render, screen } from '../../../tests/helpers/render';
import { PlaceFormLocation } from './PlaceFormLocation';
import { describe, expect, it, vi } from 'vitest';
const HINT =
'This address has no coordinates yet, so the place will not show on the map. Pick it from the search above or enter latitude and longitude.';
function renderLocation(form: { address: string; lat: string; lng: string }, warnWithoutCoordinates = true) {
const onChange = vi.fn();
render(
<PlaceFormLocation fieldId="pf" form={form} onChange={onChange} warnWithoutCoordinates={warnWithoutCoordinates} />
);
return onChange;
}
describe('PlaceFormLocation', () => {
it('FE-PLANNER-PFLOC-001: an address typed without coordinates says the place will not be on the map (#2837)', () => {
renderLocation({ address: 'Rue de Rivoli 1, Paris', lat: '', lng: '' });
expect(screen.getByText(HINT)).toBeInTheDocument();
});
it('FE-PLANNER-PFLOC-002: no word with coordinates, without an address, or where the form explains it itself', () => {
renderLocation({ address: 'Rue de Rivoli 1, Paris', lat: '48.86', lng: '2.35' });
renderLocation({ address: '', lat: '', lng: '' });
renderLocation({ address: 'Rue de Rivoli 1, Paris', lat: '', lng: '' }, false);
expect(screen.queryByText(HINT)).toBeNull();
});
it('FE-PLANNER-PFLOC-003: the fields write through, and a pasted pair fills both coordinates', () => {
const onChange = renderLocation({ address: '', lat: '', lng: '' });
fireEvent.change(screen.getByLabelText('Address'), { target: { value: 'Rue de Rivoli 1' } });
expect(onChange).toHaveBeenCalledWith('address', 'Rue de Rivoli 1');
fireEvent.paste(screen.getByLabelText('Latitude'), { clipboardData: { getData: () => '48.8566, 2.3522' } });
expect(onChange).toHaveBeenCalledWith('lat', '48.8566');
expect(onChange).toHaveBeenCalledWith('lng', '2.3522');
});
});
@@ -0,0 +1,78 @@
import { MapPinOff } from 'lucide-react';
import { useTranslation } from '../../i18n';
import { EditorField, GRID_2, INPUT, PANEL } from '../shared/dialogParts';
import { NumericInput } from '../shared/NumericInput';
import { addressWithoutCoordinates } from './placeCoordinates';
import { parseCoordinatePair } from './PlaceFormModal.helpers';
type LocationField = 'address' | 'lat' | 'lng';
interface PlaceFormLocationProps {
/** The form's id prefix, so each label points at its own field. */
fieldId: string;
form: Record<LocationField, string>;
onChange: (field: LocationField, value: string) => void;
/** Off where the form explains a missing position in its own words (a service stop). */
warnWithoutCoordinates: boolean;
}
/**
* Where the place is: what a pick, a pasted link or the map filled in, or typed by hand.
* An address typed without coordinates says under the field that the place will not be
* on the map, and how to put it there (#2837).
*/
export function PlaceFormLocation({ fieldId, form, onChange, warnWithoutCoordinates }: PlaceFormLocationProps) {
const { t } = useTranslation();
const hint = warnWithoutCoordinates && addressWithoutCoordinates(form) && (
<span className="flex items-start gap-1 text-content-muted">
<MapPinOff size={12} strokeWidth={2} aria-hidden="true" className="mt-px flex-none text-warning" />
{t('places.noCoordinatesFormHint')}
</span>
);
return (
<div className={PANEL}>
<EditorField label={t('places.formAddress')} htmlFor={`${fieldId}-address`} hint={hint}>
<input
id={`${fieldId}-address`}
type="text"
value={form.address}
onChange={(e) => onChange('address', e.target.value)}
placeholder={t('places.formAddressPlaceholder')}
className={INPUT}
/>
</EditorField>
<div role="group" aria-label={t('collections.coordinates')} className={GRID_2}>
<EditorField label={t('places.formLatLabel')} htmlFor={`${fieldId}-lat`}>
<NumericInput
id={`${fieldId}-lat`}
mode="signed"
value={form.lat}
onValueChange={(v) => onChange('lat', v)}
onPaste={(e) => {
// "48.85, 2.35" pasted into the latitude fills both halves.
const pair = parseCoordinatePair(e.clipboardData.getData('text'));
if (pair) {
e.preventDefault();
onChange('lat', pair[0]);
onChange('lng', pair[1]);
}
}}
placeholder={t('places.formLat')}
className={INPUT}
/>
</EditorField>
<EditorField label={t('places.formLngLabel')} htmlFor={`${fieldId}-lng`}>
<NumericInput
id={`${fieldId}-lng`}
mode="signed"
value={form.lng}
onValueChange={(v) => onChange('lng', v)}
placeholder={t('places.formLng')}
className={INPUT}
/>
</EditorField>
</div>
</div>
);
}
@@ -15,10 +15,11 @@ import { Tooltip } from '../shared/Tooltip'
import { Search, RotateCcw, LocateFixed, PencilLine } from 'lucide-react'
import { useTranslation } from '../../i18n'
import { PlaceContactFields } from './PlaceContactFields'
import { PlaceFormLocation } from './PlaceFormLocation'
import { weekFromPeriods } from './placeHours'
import type { PlaceOpeningHours } from '@trek/shared'
import {
DEFAULT_FORM, endsBeforeStart, findDuplicatePlace, formPin, mergeResult, parseCoordinatePair, placeEditForm, placeFormPayload,
DEFAULT_FORM, endsBeforeStart, findDuplicatePlace, formPin, mergeResult, placeEditForm, placeFormPayload,
prefillForm, savedCategoryId, type PlaceFormData, type PlacePrefill,
} from './PlaceFormModal.helpers'
import { useNewCategory, usePlaceForm } from './usePlaceForm'
@@ -30,7 +31,6 @@ import { useLocationBias } from '../../hooks/useLocationBias'
import { BookingCostsSection } from './BookingCostsSection'
import type { BookingExpenseRequest } from './BookingCostsSection.types'
import type { Place, Category, Assignment, BudgetItem } from '../../types'
import { NumericInput } from '../shared/NumericInput'
import { formatDistance } from '../../utils/units'
import ServiceStopSection from '../Roadtrip/ServiceStopSection'
import { DEFAULT_SERVICE_KIND, serviceStopChoice, type ServiceStopMode } from '../Roadtrip/manualStop'
@@ -770,50 +770,7 @@ export default function PlaceFormModal(props: PlaceFormModalProps) {
)}
</div>
{/* Where it is: what a pick, a pasted link or the map filled in, or typed by hand. */}
<div className={PANEL}>
<EditorField label={t('places.formAddress')} htmlFor={`${fieldId}-address`}>
<input
id={`${fieldId}-address`}
type="text"
value={form.address}
onChange={e => handleChange('address', e.target.value)}
placeholder={t('places.formAddressPlaceholder')}
className={INPUT}
/>
</EditorField>
<div role="group" aria-label={t('collections.coordinates')} className={GRID_2}>
<EditorField label={t('places.formLatLabel')} htmlFor={`${fieldId}-lat`}>
<NumericInput
id={`${fieldId}-lat`}
mode="signed"
value={form.lat}
onValueChange={v => handleChange('lat', v)}
onPaste={e => {
// "48.85, 2.35" pasted into the latitude fills both halves.
const pair = parseCoordinatePair(e.clipboardData.getData('text'))
if (pair) {
e.preventDefault()
handleChange('lat', pair[0])
handleChange('lng', pair[1])
}
}}
placeholder={t('places.formLat')}
className={INPUT}
/>
</EditorField>
<EditorField label={t('places.formLngLabel')} htmlFor={`${fieldId}-lng`}>
<NumericInput
id={`${fieldId}-lng`}
mode="signed"
value={form.lng}
onValueChange={v => handleChange('lng', v)}
placeholder={t('places.formLng')}
className={INPUT}
/>
</EditorField>
</div>
</div>
<PlaceFormLocation fieldId={fieldId} form={form} onChange={handleChange} warnWithoutCoordinates={!serviceStop} />
{/* For a stop on a drive: the kind of stop, how long, and where it belongs.
The category it replaces is gone from the head band too: refuelling is
@@ -1871,3 +1871,16 @@ describe('PlaceInspector folded to its head (#3039)', () => {
expect(screen.queryByRole('button', { name: 'Collapse' })).toBeNull();
});
});
// ── An address without coordinates (#2837) ─────────────────────────────────────
describe('PlaceInspector without coordinates (#2837)', () => {
it('FE-PLANNER-INSPECTOR-114: a typed address without coordinates says the place is not on the map', () => {
const typed = buildPlace({ id: 3, name: 'Corner Cafe', address: 'Rue de Rivoli 1, Paris', lat: null, lng: null });
const { rerender } = render(<PlaceInspector {...defaultProps} place={typed} />);
expect(screen.getByText('No coordinates, so not on the map')).toBeInTheDocument();
rerender(<PlaceInspector {...defaultProps} />);
expect(screen.queryByText('No coordinates, so not on the map')).toBeNull();
});
});
@@ -1,4 +1,4 @@
import { Banknote, ChevronDown, ChevronUp, Mail, MapPin, Phone, Star, X } from 'lucide-react';
import { Banknote, ChevronDown, ChevronUp, Mail, MapPin, MapPinOff, Phone, Star, X } from 'lucide-react';
import type { KeyboardEvent, ReactNode, RefObject } from 'react';
import { useTranslation } from '../../i18n';
@@ -9,6 +9,7 @@ import { NEUTRAL_TINT, fs } from '../shared/DialogShell';
import PlaceAvatar from '../shared/PlaceAvatar';
import { Tooltip } from '../shared/Tooltip';
import PlacePhotoGallery from './PlacePhotoGallery';
import { addressWithoutCoordinates } from './placeCoordinates';
import { SoftPill, TimePill, tintOf } from './planParts';
/** The round buttons at the end of the band. */
@@ -81,6 +82,8 @@ export function InspectorHead({
const { t, locale, language } = useTranslation();
const ring = openNow === true ? 'bg-success' : openNow === false ? 'bg-danger' : 'bg-surface-card shadow-sm';
const hasCoords = !!(place.lat && place.lng);
// A typed address that never got coordinates: the place has no marker, and says why (#2837).
const offMap = addressWithoutCoordinates(place);
const hasPills =
openNow !== null ||
!!category ||
@@ -90,7 +93,8 @@ export function InspectorHead({
place.source === 'dawarich' ||
!!phone ||
!!email ||
hasCoords;
hasCoords ||
offMap;
return (
<header
className={`flex-none px-4 ${collapsed ? 'py-2.5' : 'border-b border-edge-faint pb-3 pt-3.5'}`}
@@ -144,6 +148,15 @@ export function InspectorHead({
{!collapsed && hasPills && (
<div className="mt-2 flex flex-wrap items-center gap-1.5">
{offMap && (
<Tooltip label={t('places.noCoordinatesHint')}>
<span className="inline-flex min-w-0">
<SoftPill tone="warning" icon={<MapPinOff size={11} strokeWidth={2} className="flex-none" />}>
{t('places.noCoordinates')}
</SoftPill>
</span>
</Tooltip>
)}
{openNow !== null && (
<SoftPill
tone={openNow ? 'success' : 'danger'}
@@ -1,4 +1,4 @@
// FE-COMP-PLACES-001 to FE-COMP-PLACES-015 + FE-PLANNER-SIDEBAR-016 to 067
// FE-COMP-PLACES-001 to FE-COMP-PLACES-015 + FE-PLANNER-SIDEBAR-016 to 068
import { render, screen, fireEvent, waitFor, act, within } from '../../../tests/helpers/render';
import userEvent from '@testing-library/user-event';
import { http, HttpResponse } from 'msw';
@@ -1168,3 +1168,16 @@ describe('Category filter: places without a category', () => {
expect(screen.queryByRole('button', { name: 'No Category' })).not.toBeInTheDocument();
});
});
describe('Places without coordinates', () => {
it('FE-PLANNER-SIDEBAR-068: a typed address without coordinates is marked as not on the map (#2837)', () => {
const typed = buildPlace({ name: 'Typed Cafe', address: 'Rue de Rivoli 1, Paris', lat: null, lng: null });
const picked = buildPlace({ name: 'Picked Cafe', address: 'Rue de Rivoli 2, Paris' });
const note = buildPlace({ name: 'Loose Idea', address: null, lat: null, lng: null });
render(<PlacesSidebar {...defaultProps} places={[typed, picked, note]} />);
const marks = screen.getAllByRole('img', { name: 'No coordinates, so not on the map' });
expect(marks).toHaveLength(1);
expect(marks[0].closest('[data-place-id]')).toHaveAttribute('data-place-id', String(typed.id));
});
});
@@ -1,7 +1,7 @@
import React, { useId } from 'react'
import Markdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import { Plus, Check, Star } from 'lucide-react'
import { Plus, Check, Star, MapPinOff } from 'lucide-react'
import PlaceAvatar from '../shared/PlaceAvatar'
import { Tooltip } from '../shared/Tooltip'
import { fs } from '../shared/DialogShell'
@@ -10,6 +10,7 @@ import { resolveTrackColor } from '../Map/trackColors'
import { MoreButton, SoftPill, type MenuEntry } from './planParts'
import type { Place, Category } from '../../types'
import { startPlaceDrag } from '../Map/markerDrag'
import { addressWithoutCoordinates } from './placeCoordinates'
interface MemoPlaceRowProps {
place: Place
@@ -115,6 +116,15 @@ export const MemoPlaceRow = React.memo(function MemoPlaceRow({
<span id={nameId} className="min-w-0 truncate font-semibold leading-tight text-content" style={fs(13, 'body')}>
{place.name}
</span>
{/* A typed address that never got coordinates: nothing stands for this place on the
map, and the row says so (#2837). */}
{addressWithoutCoordinates(place) && (
<Tooltip label={t('places.noCoordinates')}>
<span role="img" aria-label={t('places.noCoordinates')} className="inline-flex flex-none text-warning">
<MapPinOff size={12} strokeWidth={2.2} />
</span>
</Tooltip>
)}
{/* Average member rating (#1435). */}
{hasRating && (
<SoftPill className="tabular-nums" icon={<Star size={9} strokeWidth={2.2} fill="currentColor" className="flex-none text-warning" />}>
@@ -0,0 +1,25 @@
import { describe, expect, it } from 'vitest';
import { addressWithoutCoordinates } from './placeCoordinates';
describe('addressWithoutCoordinates', () => {
it('FE-PLANNER-COORDS-001: an address without coordinates is flagged, from a saved place or the form (#2837)', () => {
expect(addressWithoutCoordinates({ address: 'Rue de Rivoli 1, Paris', lat: null, lng: null })).toBe(true);
expect(addressWithoutCoordinates({ address: 'Rue de Rivoli 1, Paris', lat: '', lng: '' })).toBe(true);
// One half is not a position either.
expect(addressWithoutCoordinates({ address: 'Rue de Rivoli 1, Paris', lat: 48.86, lng: null })).toBe(true);
expect(addressWithoutCoordinates({ address: 'Rue de Rivoli 1, Paris', lat: '48.86', lng: ' ' })).toBe(true);
});
it('FE-PLANNER-COORDS-002: a place with coordinates, zero included, is not flagged', () => {
expect(addressWithoutCoordinates({ address: 'Rue de Rivoli 1, Paris', lat: 48.86, lng: 2.35 })).toBe(false);
expect(addressWithoutCoordinates({ address: 'Rue de Rivoli 1, Paris', lat: '48.86', lng: '2.35' })).toBe(false);
expect(addressWithoutCoordinates({ address: 'Null Island', lat: 0, lng: 0 })).toBe(false);
});
it('FE-PLANNER-COORDS-003: a place without an address is a note on purpose and is not flagged', () => {
expect(addressWithoutCoordinates({ address: null, lat: null, lng: null })).toBe(false);
expect(addressWithoutCoordinates({ address: ' ', lat: null, lng: null })).toBe(false);
expect(addressWithoutCoordinates({})).toBe(false);
});
});
@@ -0,0 +1,22 @@
type Coordinate = number | string | null | undefined;
/** A number the map can use. Zero is one; an empty form field or a blank string is not. */
const isCoordinate = (value: Coordinate): boolean =>
value != null && String(value).trim() !== '' && Number.isFinite(Number(value));
/**
* Whether a place has an address but nothing to put it on the map with (#2837).
*
* The address field is plain text: a place saved with a typed address, and no pick from
* the search, no pasted link and no point on the map, has no coordinates, so it gets no
* marker, no route and no navigation, and nothing used to say why. The form, the place's
* detail and the place lists say so. A place with neither is a note on purpose and is
* left alone. Reads a saved place (numbers) and the form (strings) alike.
*/
export function addressWithoutCoordinates(place: {
address?: string | null;
lat?: Coordinate;
lng?: Coordinate;
}): boolean {
return !!place.address?.trim() && !(isCoordinate(place.lat) && isCoordinate(place.lng));
}
@@ -1,6 +1,6 @@
import { ReactNode, useEffect, useMemo, useState } from 'react'
import {
Bookmark, CheckCheck, CheckCircle2, Download, ListChecks, Loader2, Plus,
Bookmark, CheckCheck, CheckCircle2, Download, ListChecks, Loader2, MapPinOff, Plus,
SlidersHorizontal, Tag, Trash2, X,
} from 'lucide-react'
import MDancingTrek from '../../../components/MDancingTrek'
@@ -26,6 +26,7 @@ import { filterPool, firstPlannedDayNumbers, plannedPlaceIds } from './placesBro
import { MCategoryFilterList, MRatingFloorChips, SquareCheck } from './MPlacesFilterControls'
import { countActivePlacesFilters } from '../../../../utils/placesFilter'
import { usePlacesPool } from '../../../../components/Planner/usePlacesPool'
import { addressWithoutCoordinates } from '../../../../components/Planner/placeCoordinates'
/**
* Fullscreen places pool (mode === 'browse'): All/Unplanned/Tracks filter
@@ -340,6 +341,16 @@ export default function MPlacesBrowser({ planner, shell }: MPlacesBrowserProps)
)}
<CatIcon size={12} strokeWidth={2.2} className="flex-none" style={{ color: cat?.color || 'var(--m-muted)' }} />
<span dir="auto" className="truncate text-[0.8125rem] font-semibold text-m-ink">{place.name}</span>
{/* A typed address without coordinates: this place has no marker (#2837). */}
{addressWithoutCoordinates(place) && (
<MapPinOff
size={12}
strokeWidth={2.2}
role="img"
aria-label={t('places.noCoordinates')}
className="flex-none text-[color:var(--m-st-pending)]"
/>
)}
</span>
{sub && (
<MarkdownText clamp className="mt-px font-geist text-[0.65625rem] text-m-muted">{sub}</MarkdownText>
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useRef, useState, type ClipboardEvent } from 'react'
import { MapPin } from 'lucide-react'
import { MapPin, MapPinOff } from 'lucide-react'
import MSheet from '../../../components/MSheet'
import {
DEFAULT_FORM,
@@ -12,6 +12,7 @@ import {
type PlaceFormData,
} from '../../../../components/Planner/PlaceFormModal.helpers'
import { usePlaceForm } from '../../../../components/Planner/usePlaceForm'
import { addressWithoutCoordinates } from '../../../../components/Planner/placeCoordinates'
import { Eyebrow, FIELD_AREA_CLS, FIELD_CLS, FormSheetBody, FormSheetFooter, FormSheetHeader } from './PlSheetChrome'
import { useAddonStore } from '../../../../store/addonStore'
import type { BookingExpenseRequest } from '../../../../components/Planner/BookingCostsSection.types'
@@ -331,6 +332,13 @@ export default function MPlaceEditSheet({ planner, onOpenExpense }: MPlaceEditSh
className={`${FIELD_CLS} flex-1 text-[0.8125rem] [font-variant-numeric:tabular-nums]`}
/>
</div>
{/* An address typed without coordinates puts nothing on the map (#2837). */}
{addressWithoutCoordinates(form) && (
<p className="mt-1.5 flex items-start gap-1.5 text-caption text-m-muted">
<MapPinOff size={13} strokeWidth={2} aria-hidden="true" className="mt-px flex-none text-[color:var(--m-st-pending)]" />
{t('places.noCoordinatesFormHint')}
</p>
)}
<Eyebrow className="mb-[6px] mt-3 uppercase">{t('places.formCategory')}</Eyebrow>
<PlCategoryPicker planner={planner} value={form.category_id} onChange={id => handleChange('category_id', id)} />
@@ -1,7 +1,7 @@
import { useMemo, useRef, useState } from 'react'
import DawarichIcon from '../../../../components/shared/DawarichIcon'
import {
Bookmark, ChevronRight, ExternalLink, Map as MapIcon, Navigation, Paperclip,
Bookmark, ChevronRight, ExternalLink, Map as MapIcon, MapPinOff, Navigation, Paperclip,
Pencil, Phone, Plus, Route, RouteOff, Sparkles, Trash2, Upload, X,
} from 'lucide-react'
import MSheet from '../../../components/MSheet'
@@ -24,6 +24,7 @@ import { safeHttpUrl } from '../../../../utils/safeUrl'
import { openFile } from '../../../../utils/fileDownload'
import { filesForPlace } from '../../../../utils/placeFiles'
import { navigationTargetLabel, getNavigationTargets, openNavigationTarget } from '../../../../components/Planner/placeNavigation'
import { addressWithoutCoordinates } from '../../../../components/Planner/placeCoordinates'
import { NavigationMenu } from '../../../../components/shared/NavigationMenu'
import { getPlaceBookings } from '../../../../utils/dayMerge'
import type { Assignment, Day, Reservation, TripMember } from '../../../../types'
@@ -219,6 +220,13 @@ export default function MPlaceSheet({ planner, shell }: MTripSheetsProps) {
{place.address && (
<div className="mt-[2px] font-geist text-[0.6875rem] leading-[1.4] text-m-muted"><bdi>{place.address}</bdi></div>
)}
{/* A typed address that never got coordinates: no marker, and the sheet says why (#2837). */}
{addressWithoutCoordinates(place) && (
<div className="mt-[3px] flex items-center gap-1 text-caption font-semibold text-[color:var(--m-st-pending)]">
<MapPinOff size={12} strokeWidth={2.2} aria-hidden="true" className="flex-none" />
{t('places.noCoordinates')}
</div>
)}
{/* The category under the name and address, as wide as its name:
squeezed under the photo it was cut to "Shoppi…". */}
{category && (
@@ -11,7 +11,7 @@ import { server } from '../../../helpers/msw/server'
import { resetAllStores, seedStore } from '../../../helpers/store'
import { fireEvent, render, screen, waitFor } from '../../../helpers/render'
// FE-MOB-PLEDIT-001 to FE-MOB-PLEDIT-048, plus the 009b, 025b and 029b variants
// FE-MOB-PLEDIT-001 to FE-MOB-PLEDIT-049, plus the 009b, 025b and 029b variants
// planner.t echoes the key, so every label/placeholder is asserted as its key.
const CATEGORIES = [
@@ -718,4 +718,16 @@ describe('MPlaceEditSheet', () => {
expect(planner.handleSavePlace).toHaveBeenCalledWith(expect.objectContaining({ name: 'Trailhead', osm_id: 'plugin:trail-finder:th-043' }))
expect(planner.handleSavePlace).not.toHaveBeenCalledWith(expect.objectContaining({ google_place_id: expect.anything() }))
})
it('FE-MOB-PLEDIT-049: a typed address without coordinates warns that the place will not be on the map (#2837)', () => {
setup()
expect(screen.queryByText('places.noCoordinatesFormHint')).not.toBeInTheDocument()
fireEvent.change(screen.getByPlaceholderText('places.formAddressPlaceholder'), { target: { value: '1-36-3 Asakusa' } })
expect(screen.getByText('places.noCoordinatesFormHint')).toBeInTheDocument()
fireEvent.change(screen.getByPlaceholderText('places.formLat'), { target: { value: '35.71' } })
fireEvent.change(screen.getByPlaceholderText('places.formLng'), { target: { value: '139.79' } })
expect(screen.queryByText('places.noCoordinatesFormHint')).not.toBeInTheDocument()
})
})
@@ -13,7 +13,7 @@ import type { Assignment, Category, Day, Place, TripFile, TripMember } from '../
import { resetAllStores, seedStore } from '../../../helpers/store'
import { act, fireEvent, render, screen, waitFor } from '../../../helpers/render'
// FE-MOB-PLSH-001 to FE-MOB-PLSH-032
// FE-MOB-PLSH-001 to FE-MOB-PLSH-033
vi.mock('../../../../src/utils/fileDownload', () => ({ openFile: vi.fn() }))
// The gallery says "no pictures" through the shared toast, the sheet's own writes through planner.toast.
@@ -561,4 +561,14 @@ describe('MPlaceSheet', () => {
expect(screen.queryByText('Someone elses ferry')).not.toBeInTheDocument()
})
it('FE-MOB-PLSH-033: a typed address without coordinates says the place is not on the map (#2837)', () => {
const typed = { ...PLACE, lat: null, lng: null } as unknown as Place
const { unmount } = renderSheet(makePlanner({ selectedPlace: typed }) as unknown as TripPlanner)
expect(screen.getByText('No coordinates, so not on the map')).toBeInTheDocument()
unmount()
renderSheet()
expect(screen.queryByText('No coordinates, so not on the map')).toBeNull()
})
})
@@ -11,7 +11,7 @@ import { resetAllStores, seedStore } from '../../../helpers/store'
import type { MTripShellApi, TripPlanner } from '../../../../src/mobile/screens/trip/MTripShell'
import type { AssignmentsMap, Category, Day, Place } from '../../../../src/types'
// FE-MOB-PBROW-001 to FE-MOB-PBROW-036
// FE-MOB-PBROW-001 to FE-MOB-PBROW-037
const CATEGORIES = [
{ id: 1, name: 'Sights', color: '#123456', icon: 'landmark' },
@@ -514,4 +514,12 @@ describe('MPlacesBrowser', () => {
expect(screen.getByText('places.selectionCount:2')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'common.selectAll' })).toBeInTheDocument()
})
it('FE-MOB-PBROW-037: a typed address without coordinates is marked as not on the map (#2837)', () => {
const TYPED = place({ id: 5, name: 'Typed Cafe', address: 'Rue de Rivoli 1', lat: null, lng: null } as Partial<Place>)
renderBrowser(makePlanner({ places: [TYPED, LOUVRE], assignments: {} } as Partial<TripPlanner>))
expect(within(row('Typed Cafe')).getByRole('img', { name: 'places.noCoordinates' })).toBeInTheDocument()
expect(within(row('Louvre')).queryByRole('img', { name: 'places.noCoordinates' })).toBeNull()
})
})
+4
View File
@@ -123,6 +123,10 @@ const places: TranslationStrings = {
'places.formLng': 'خط الطول (مثال: 2.3522)',
'places.formLatLabel': 'خط العرض',
'places.formLngLabel': 'خط الطول',
'places.noCoordinates': 'لا إحداثيات، لذا لا يظهر على الخريطة',
'places.noCoordinatesHint': 'عدّل المكان واختره من البحث، أو أدخل خط العرض وخط الطول.',
'places.noCoordinatesFormHint':
'لا توجد إحداثيات لهذا العنوان بعد، لذا لن يظهر المكان على الخريطة. اختره من البحث أعلاه أو أدخل خط العرض وخط الطول.',
'places.formCategory': 'الفئة',
'places.noCategory': 'بلا فئة',
'places.newCategory': 'فئة جديدة',
+4
View File
@@ -101,6 +101,10 @@ const places: TranslationStrings = {
'places.formLng': 'Uzunluq (məs. 2.3522)',
'places.formLatLabel': 'Enlik',
'places.formLngLabel': 'Uzunluq',
'places.noCoordinates': 'Koordinat yoxdur, xəritədə görünmür',
'places.noCoordinatesHint': 'Məkanı redaktə et və axtarışdan seç, ya da enlik və uzunluğu daxil et.',
'places.noCoordinatesFormHint':
'Bu ünvanın hələ koordinatı yoxdur, ona görə məkan xəritədə görünməyəcək. Onu yuxarıdakı axtarışdan seç və ya enlik və uzunluğu daxil et.',
'places.formCategory': 'Kateqoriya',
'places.noCategory': 'Kateqoriya yoxdur',
'places.newCategory': 'Yeni kateqoriya',
+4
View File
@@ -92,6 +92,10 @@ const places: TranslationStrings = {
'places.formLatLabel': 'Latitude', // same-as-en
'places.formLngLabel': 'Longitude', // same-as-en
'places.formCategory': 'Categoria',
'places.noCoordinates': 'Sem coordenadas, por isso fora do mapa',
'places.noCoordinatesHint': 'Edite o lugar e escolha-o na busca, ou informe a latitude e a longitude.',
'places.noCoordinatesFormHint':
'Este endereço ainda não tem coordenadas, então o lugar não aparecerá no mapa. Escolha-o na busca acima ou informe a latitude e a longitude.',
'places.noCategory': 'Sem categoria',
'places.newCategory': 'Nova categoria',
'places.openInMaps': 'Abrir em mapas',
+4
View File
@@ -89,6 +89,10 @@ const places: TranslationStrings = {
'places.formLng': 'Longitud (p. ex. 2.3522)',
'places.formLatLabel': 'Latitud',
'places.formLngLabel': 'Longitud',
'places.noCoordinates': 'Sense coordenades, per tant no surt al mapa',
'places.noCoordinatesHint': "Edita el lloc i tria'l a la cerca, o introdueix la latitud i la longitud.",
'places.noCoordinatesFormHint':
"Aquesta adreça encara no té coordenades, així que el lloc no sortirà al mapa. Tria'l a la cerca de dalt o introdueix la latitud i la longitud.",
'places.formCategory': 'Categoria',
'places.noCategory': 'Sense categoria',
'places.newCategory': 'Categoria nova',
+4
View File
@@ -99,6 +99,10 @@ const places: TranslationStrings = {
'places.formLng': 'Zeměpisná délka',
'places.formLatLabel': 'Zeměpisná šířka',
'places.formLngLabel': 'Zeměpisná délka',
'places.noCoordinates': 'Bez souřadnic, proto není na mapě',
'places.noCoordinatesHint': 'Uprav místo a vyber ho ve vyhledávání, nebo zadej zeměpisnou šířku a délku.',
'places.noCoordinatesFormHint':
'Tato adresa zatím nemá souřadnice, takže se místo na mapě nezobrazí. Vyber ho ve vyhledávání výše, nebo zadej zeměpisnou šířku a délku.',
'places.formCategory': 'Kategorie',
'places.noCategory': 'Bez kategorie',
'places.newCategory': 'Nová kategorie',
+5
View File
@@ -91,6 +91,11 @@ const places: TranslationStrings = {
'places.formLng': 'Längengrad (z.B. 2.3522)',
'places.formLatLabel': 'Breitengrad',
'places.formLngLabel': 'Längengrad',
'places.noCoordinates': 'Keine Koordinaten, daher nicht auf der Karte',
'places.noCoordinatesHint':
'Bearbeite den Ort und wähle ihn in der Suche aus, oder trage Breiten- und Längengrad ein.',
'places.noCoordinatesFormHint':
'Diese Adresse hat noch keine Koordinaten, der Ort erscheint also nicht auf der Karte. Wähle ihn oben in der Suche aus oder trage Breiten- und Längengrad ein.',
'places.formCategory': 'Kategorie',
'places.noCategory': 'Keine Kategorie',
'places.newCategory': 'Neue Kategorie',
+4
View File
@@ -98,6 +98,10 @@ const places: TranslationStrings = {
/** The small labels over the two coordinate fields of the desktop place dialog. */
'places.formLatLabel': 'Latitude',
'places.formLngLabel': 'Longitude',
'places.noCoordinates': 'No coordinates, so not on the map',
'places.noCoordinatesHint': 'Edit the place and pick it from the search, or enter its latitude and longitude.',
'places.noCoordinatesFormHint':
'This address has no coordinates yet, so the place will not show on the map. Pick it from the search above or enter latitude and longitude.',
'places.formCategory': 'Category',
'places.noCategory': 'No Category',
'places.newCategory': 'New category',
+4
View File
@@ -92,6 +92,10 @@ const places: TranslationStrings = {
'places.formLng': 'Longitud (p. ej. 2.3522)',
'places.formLatLabel': 'Latitud',
'places.formLngLabel': 'Longitud',
'places.noCoordinates': 'Sin coordenadas, así que no aparece en el mapa',
'places.noCoordinatesHint': 'Edita el lugar y elígelo en la búsqueda, o introduce la latitud y la longitud.',
'places.noCoordinatesFormHint':
'Esta dirección aún no tiene coordenadas, así que el lugar no aparecerá en el mapa. Elígelo en la búsqueda de arriba o introduce la latitud y la longitud.',
'places.formCategory': 'Categoría',
'places.noCategory': 'Sin categoría',
'places.newCategory': 'Nueva categoría',
+4
View File
@@ -92,6 +92,10 @@ const places: TranslationStrings = {
'places.formLng': 'Pikkuskraad (nt 2.3522)',
'places.formLatLabel': 'Laiuskraad',
'places.formLngLabel': 'Pikkuskraad',
'places.noCoordinates': 'Koordinaadid puuduvad, seega pole kaardil',
'places.noCoordinatesHint': 'Muuda kohta ja vali see otsingust või sisesta laius- ja pikkuskraad.',
'places.noCoordinatesFormHint':
'Sellel aadressil pole veel koordinaate, seega koht kaardile ei ilmu. Vali see ülalt otsingust või sisesta laius- ja pikkuskraad.',
'places.formCategory': 'Kategooria',
'places.noCategory': 'Kategooriata',
'places.newCategory': 'Uus kategooria',
+4
View File
@@ -93,6 +93,10 @@ const places: TranslationStrings = {
'places.formLatLabel': 'Latitude', // same-as-en
'places.formLngLabel': 'Longitude', // same-as-en
'places.formCategory': 'Catégorie',
'places.noCoordinates': 'Pas de coordonnées, donc absent de la carte',
'places.noCoordinatesHint': 'Modifie le lieu et choisis-le dans la recherche, ou saisis sa latitude et sa longitude.',
'places.noCoordinatesFormHint':
'Cette adresse n’a pas encore de coordonnées, le lieu n’apparaîtra donc pas sur la carte. Choisis-le dans la recherche ci-dessus ou saisis la latitude et la longitude.',
'places.noCategory': 'Sans catégorie',
'places.newCategory': 'Nouvelle catégorie',
'places.openInMaps': 'Ouvrir dans une app de cartes',
+5
View File
@@ -92,6 +92,11 @@ const places: TranslationStrings = {
'places.formLng': 'Γεωγραφικό Μήκος (π.χ. 2.3522)',
'places.formLatLabel': 'Γεωγραφικό Πλάτος',
'places.formLngLabel': 'Γεωγραφικό Μήκος',
'places.noCoordinates': 'Χωρίς συντεταγμένες, άρα όχι στον χάρτη',
'places.noCoordinatesHint':
'Επεξεργάσου το μέρος και διάλεξέ το από την αναζήτηση ή γράψε γεωγραφικό πλάτος και μήκος.',
'places.noCoordinatesFormHint':
'Αυτή η διεύθυνση δεν έχει ακόμη συντεταγμένες, οπότε το μέρος δεν θα εμφανιστεί στον χάρτη. Διάλεξέ το από την αναζήτηση παραπάνω ή γράψε γεωγραφικό πλάτος και μήκος.',
'places.formCategory': 'Κατηγορία',
'places.noCategory': 'Χωρίς Κατηγορία',
'places.newCategory': 'Νέα κατηγορία',
+4
View File
@@ -98,6 +98,10 @@ const places: TranslationStrings = {
'places.formLng': 'קו אורך (למשל 2.3522)',
'places.formLatLabel': 'קו רוחב',
'places.formLngLabel': 'קו אורך',
'places.noCoordinates': 'אין קואורדינטות, ולכן לא על המפה',
'places.noCoordinatesHint': 'ערכו את המקום ובחרו אותו מהחיפוש, או הזינו קו רוחב וקו אורך.',
'places.noCoordinatesFormHint':
'לכתובת הזו עדיין אין קואורדינטות, ולכן המקום לא יופיע על המפה. בחרו אותו מהחיפוש למעלה או הזינו קו רוחב וקו אורך.',
'places.formCategory': 'קטגוריה',
'places.noCategory': 'ללא קטגוריה',
'places.newCategory': 'קטגוריה חדשה',
+5
View File
@@ -91,6 +91,11 @@ const places: TranslationStrings = {
'places.formLng': 'Hosszúsági fok (pl. 2.3522)',
'places.formLatLabel': 'Szélességi fok',
'places.formLngLabel': 'Hosszúsági fok',
'places.noCoordinates': 'Nincsenek koordináták, ezért nincs a térképen',
'places.noCoordinatesHint':
'Szerkeszd a helyet, és válaszd ki a keresésből, vagy add meg a szélességet és a hosszúságot.',
'places.noCoordinatesFormHint':
'Ehhez a címhez még nincsenek koordináták, így a hely nem jelenik meg a térképen. Válaszd ki a fenti keresésből, vagy add meg a szélességet és a hosszúságot.',
'places.formCategory': 'Kategória',
'places.noCategory': 'Nincs kategória',
'places.newCategory': 'Új kategória',
+4
View File
@@ -82,6 +82,10 @@ const places: TranslationStrings = {
'places.formLng': 'Bujur (mis. 2.3522)',
'places.formLatLabel': 'Lintang',
'places.formLngLabel': 'Bujur',
'places.noCoordinates': 'Tanpa koordinat, jadi tidak ada di peta',
'places.noCoordinatesHint': 'Edit tempat ini dan pilih dari pencarian, atau masukkan lintang dan bujurnya.',
'places.noCoordinatesFormHint':
'Alamat ini belum punya koordinat, jadi tempat ini tidak akan muncul di peta. Pilih dari pencarian di atas atau masukkan lintang dan bujur.',
'places.formCategory': 'Kategori',
'places.noCategory': 'Tanpa Kategori',
'places.newCategory': 'Kategori baru',
+4
View File
@@ -91,6 +91,10 @@ const places: TranslationStrings = {
'places.formLng': 'Longitudine (es. 2.3522)',
'places.formLatLabel': 'Latitudine',
'places.formLngLabel': 'Longitudine',
'places.noCoordinates': 'Nessuna coordinata, quindi non è sulla mappa',
'places.noCoordinatesHint': 'Modifica il luogo e sceglilo dalla ricerca, oppure inserisci latitudine e longitudine.',
'places.noCoordinatesFormHint':
'Questo indirizzo non ha ancora coordinate, quindi il luogo non comparirà sulla mappa. Sceglilo dalla ricerca qui sopra oppure inserisci latitudine e longitudine.',
'places.formCategory': 'Categoria',
'places.noCategory': 'Nessuna categoria',
'places.newCategory': 'Nuova categoria',
+4
View File
@@ -83,6 +83,10 @@ const places: TranslationStrings = {
'places.formLng': '経度(例:2.3522)',
'places.formLatLabel': '緯度',
'places.formLngLabel': '経度',
'places.noCoordinates': '座標がないため地図に表示されません',
'places.noCoordinatesHint': '場所を編集して検索から選ぶか、緯度と経度を入力してください。',
'places.noCoordinatesFormHint':
'この住所にはまだ座標がないため、地図に表示されません。上の検索から選ぶか、緯度と経度を入力してください。',
'places.formCategory': 'カテゴリ',
'places.noCategory': 'カテゴリなし',
'places.newCategory': '新しいカテゴリ',
+4
View File
@@ -82,6 +82,10 @@ const places: TranslationStrings = {
'places.formLng': '경도 (예: 2.3522)',
'places.formLatLabel': '위도',
'places.formLngLabel': '경도',
'places.noCoordinates': '좌표가 없어 지도에 표시되지 않음',
'places.noCoordinatesHint': '장소를 편집해 검색에서 고르거나 위도와 경도를 입력하세요.',
'places.noCoordinatesFormHint':
'이 주소에는 아직 좌표가 없어 지도에 표시되지 않습니다. 위 검색에서 고르거나 위도와 경도를 입력하세요.',
'places.formCategory': '카테고리',
'places.noCategory': '카테고리 없음',
'places.newCategory': '새 카테고리',
+4
View File
@@ -91,6 +91,10 @@ const places: TranslationStrings = {
'places.formLng': 'Lengtegraad (bijv. 2.3522)',
'places.formLatLabel': 'Breedtegraad',
'places.formLngLabel': 'Lengtegraad',
'places.noCoordinates': 'Geen coördinaten, dus niet op de kaart',
'places.noCoordinatesHint': 'Bewerk de plek en kies hem in de zoekfunctie, of vul de breedte- en lengtegraad in.',
'places.noCoordinatesFormHint':
'Dit adres heeft nog geen coördinaten, dus de plek verschijnt niet op de kaart. Kies hem in de zoekfunctie hierboven of vul de breedte- en lengtegraad in.',
'places.formCategory': 'Categorie',
'places.noCategory': 'Geen categorie',
'places.newCategory': 'Nieuwe categorie',
+4
View File
@@ -91,6 +91,10 @@ const places: TranslationStrings = {
'places.formLng': 'Długość (np. 2.3522)',
'places.formLatLabel': 'Szerokość',
'places.formLngLabel': 'Długość',
'places.noCoordinates': 'Brak współrzędnych, więc nie ma na mapie',
'places.noCoordinatesHint': 'Edytuj miejsce i wybierz je z wyszukiwania albo wpisz szerokość i długość geograficzną.',
'places.noCoordinatesFormHint':
'Ten adres nie ma jeszcze współrzędnych, więc miejsce nie pojawi się na mapie. Wybierz je z wyszukiwania powyżej albo wpisz szerokość i długość geograficzną.',
'places.formCategory': 'Kategoria',
'places.noCategory': 'Brak kategorii',
'places.newCategory': 'Nowa kategoria',
+4
View File
@@ -107,6 +107,10 @@ const places: TranslationStrings = {
'places.formLng': 'Долгота (напр. 2.3522)',
'places.formLatLabel': 'Широта',
'places.formLngLabel': 'Долгота',
'places.noCoordinates': 'Нет координат, поэтому не на карте',
'places.noCoordinatesHint': 'Измени место и выбери его в поиске или введи широту и долготу.',
'places.noCoordinatesFormHint':
'У этого адреса пока нет координат, поэтому место не появится на карте. Выбери его в поиске выше или введи широту и долготу.',
'places.formCategory': 'Категория',
'places.noCategory': 'Без категории',
'places.newCategory': 'Новая категория',
+4
View File
@@ -100,6 +100,10 @@ const places: TranslationStrings = {
'places.formLng': 'Zemepisná dĺžka (napr. 2.3522)',
'places.formLatLabel': 'Zemepisná šírka',
'places.formLngLabel': 'Zemepisná dĺžka',
'places.noCoordinates': 'Bez súradníc, preto nie je na mape',
'places.noCoordinatesHint': 'Uprav miesto a vyber ho vo vyhľadávaní, alebo zadaj zemepisnú šírku a dĺžku.',
'places.noCoordinatesFormHint':
'Táto adresa zatiaľ nemá súradnice, takže sa miesto na mape nezobrazí. Vyber ho vo vyhľadávaní vyššie, alebo zadaj zemepisnú šírku a dĺžku.',
'places.formCategory': 'Kategória',
'places.noCategory': 'Bez kategórie',
'places.newCategory': 'Nová kategória',
+4
View File
@@ -91,6 +91,10 @@ const places: TranslationStrings = {
'places.formLng': 'Longitud (t.ex. 2.3522)',
'places.formLatLabel': 'Latitud',
'places.formLngLabel': 'Longitud',
'places.noCoordinates': 'Inga koordinater, så inte på kartan',
'places.noCoordinatesHint': 'Redigera platsen och välj den i sökningen, eller ange latitud och longitud.',
'places.noCoordinatesFormHint':
'Den här adressen har inga koordinater än, så platsen syns inte på kartan. Välj den i sökningen ovan eller ange latitud och longitud.',
'places.formCategory': 'Kategori',
'places.noCategory': 'Ingen kategori',
'places.newCategory': 'Ny kategori',
+4
View File
@@ -77,6 +77,10 @@ const places: TranslationStrings = {
'places.formLng': 'ลองจิจูด (เช่น 2.3522)',
'places.formLatLabel': 'ละติจูด',
'places.formLngLabel': 'ลองจิจูด',
'places.noCoordinates': 'ไม่มีพิกัด จึงไม่แสดงบนแผนที่',
'places.noCoordinatesHint': 'แก้ไขสถานที่แล้วเลือกจากการค้นหา หรือใส่ละติจูดและลองจิจูด',
'places.noCoordinatesFormHint':
'ที่อยู่นี้ยังไม่มีพิกัด สถานที่จึงจะไม่แสดงบนแผนที่ เลือกจากการค้นหาด้านบน หรือใส่ละติจูดและลองจิจูด',
'places.formCategory': 'หมวดหมู่',
'places.noCategory': 'ไม่มีหมวดหมู่',
'places.newCategory': 'หมวดหมู่ใหม่',
+4
View File
@@ -93,6 +93,10 @@ const places: TranslationStrings = {
'places.formLng': 'Boylam (ör. 2,3522)',
'places.formLatLabel': 'Enlem',
'places.formLngLabel': 'Boylam',
'places.noCoordinates': 'Koordinat yok, bu yüzden haritada değil',
'places.noCoordinatesHint': 'Yeri düzenle ve aramadan seç ya da enlem ve boylamını gir.',
'places.noCoordinatesFormHint':
'Bu adresin henüz koordinatı yok, bu yüzden yer haritada görünmeyecek. Yukarıdaki aramadan seç ya da enlem ve boylamı gir.',
'places.formCategory': 'Kategori',
'places.noCategory': 'Kategori Yok',
'places.newCategory': 'Yeni kategori',
+4
View File
@@ -107,6 +107,10 @@ const places: TranslationStrings = {
'places.formLng': 'Довгота (наприклад, 2.3522)',
'places.formLatLabel': 'Широта',
'places.formLngLabel': 'Довгота',
'places.noCoordinates': 'Немає координат, тому не на мапі',
'places.noCoordinatesHint': 'Відредагуй місце й вибери його в пошуку або введи широту й довготу.',
'places.noCoordinatesFormHint':
'Ця адреса ще не має координат, тож місце не з’явиться на мапі. Вибери його в пошуку вище або введи широту й довготу.',
'places.formCategory': 'Категорія',
'places.noCategory': 'Без категорії',
'places.newCategory': 'Нова категорія',
+4
View File
@@ -83,6 +83,10 @@ const places: TranslationStrings = {
'places.formLng': 'Kinh độ (ví dụ: 2,3522)',
'places.formLatLabel': 'Vĩ độ',
'places.formLngLabel': 'Kinh độ',
'places.noCoordinates': 'Không có tọa độ nên không hiện trên bản đồ',
'places.noCoordinatesHint': 'Hãy sửa địa điểm và chọn từ ô tìm kiếm, hoặc nhập vĩ độ và kinh độ.',
'places.noCoordinatesFormHint':
'Địa chỉ này chưa có tọa độ nên địa điểm sẽ không hiện trên bản đồ. Hãy chọn từ ô tìm kiếm phía trên hoặc nhập vĩ độ và kinh độ.',
'places.formCategory': 'Loại',
'places.noCategory': 'Không có danh mục',
'places.newCategory': 'Danh mục mới',
+4
View File
@@ -82,6 +82,10 @@ const places: TranslationStrings = {
'places.formLng': '經度(如 2.3522)',
'places.formLatLabel': '緯度',
'places.formLngLabel': '經度',
'places.noCoordinates': '沒有座標,因此不在地圖上',
'places.noCoordinatesHint': '編輯此地點並從搜尋中選擇,或輸入緯度和經度。',
'places.noCoordinatesFormHint':
'此地址還沒有座標,因此該地點不會顯示在地圖上。請從上方搜尋中選擇,或輸入緯度和經度。',
'places.formCategory': '分類',
'places.noCategory': '無分類',
'places.newCategory': '新增分類',
+4
View File
@@ -82,6 +82,10 @@ const places: TranslationStrings = {
'places.formLng': '经度(如 2.3522)',
'places.formLatLabel': '纬度',
'places.formLngLabel': '经度',
'places.noCoordinates': '没有坐标,因此不在地图上',
'places.noCoordinatesHint': '编辑该地点并从搜索中选择,或输入纬度和经度。',
'places.noCoordinatesFormHint':
'此地址还没有坐标,因此该地点不会显示在地图上。请从上方搜索中选择,或输入纬度和经度。',
'places.formCategory': '分类',
'places.noCategory': '无分类',
'places.newCategory': '新建分类',
+3 -3
View File
@@ -21,7 +21,7 @@ The right column of the Plan tab lists every place of the trip. Its head band ha
With a day open and **Planned** chosen, the list shows only that day's places, and a chip says **Showing the open day only**. Its **X** (*Show the whole trip*) closes the day. See [Trip Planner Overview](Trip-Planner-Overview#layout).
Each row shows the place's photo or category tile, a short stroke in the track's colour for an imported track, the category icon, the name, the average member rating, and a line with the description or the address. Click a row to open the [place inspector](#the-place-inspector), or drag it onto a day. While a day is open, a place that is not on it yet shows a **+** (*+ Day*) that puts it on that day.
Each row shows the place's photo or category tile, a short stroke in the track's colour for an imported track, the category icon, the name, the average member rating, and a line with the description or the address. A place with an address but no coordinates carries a crossed-out pin beside its name (*No coordinates, so not on the map*), on the phone too. Click a row to open the [place inspector](#the-place-inspector), or drag it onto a day. While a day is open, a place that is not on it yet shows a **+** (*+ Day*) that puts it on that day.
The **…** at the end of a row (*More options*) offers what a right-click offers: **Edit**, **+ Day** while a day is open, **Open Website**, **Google Maps**, **Save to Collection** with the [Collections](Collections) addon, and **Delete**.
@@ -71,7 +71,7 @@ Click **Select** (the tick next to the search field) to switch the list into sel
|---|---|
| Name | Required. Typed into the head band. |
| Category | Pick one, **No Category**, or create a new one with the **+** (a new category starts with the colour `#6366f1` and the `MapPin` icon). |
| Address | Free text. |
| Address | Free text. Typing an address does not place the pin: without a pick from the search, a pasted link, coordinates or a point on the map the place has no coordinates and does not appear on the map. The form says so under the field while that is the case. |
| Latitude / Longitude | Decimal degrees. See [Entering coordinates manually](#entering-coordinates-manually). |
| Start / End | Only when you edit the place from a day: the time of this visit. |
| Notes for this day | Only when you edit the place from a day: a note for this visit alone. It shows under the place in the day plan. |
@@ -275,7 +275,7 @@ Click a place in the places column, in a day, or on the map, and its inspector o
![The place inspector over the map: the photo, name, address, category and coordinates in its head, then the rating, the notes, the stay booked there under Bookings and the files, with Add to Day, Navigation, Save to Collection, Delete and Edit at its foot](assets/PlaceInspector.png)
- **The head band** shows the photo (a click opens it in a gallery, see [The photo gallery](#the-photo-gallery)), with a green ring while the place is open and a red one while it is closed, the name (double-click it to rename the place), the address on one line with the full address in a tooltip, and pills: **Open** or **Closed**, the category, the time of the visit, the Google rating, the phone number to call, the e-mail address, and the coordinates. The chevron (**Collapse**) folds the inspector to a slim bar with the photo and the name, so the place stays selected and its marker highlighted while you pan the map; **Expand** unfolds it again, and the next place you pick opens in full. **X** closes the inspector and deselects the place.
- **The head band** shows the photo (a click opens it in a gallery, see [The photo gallery](#the-photo-gallery)), with a green ring while the place is open and a red one while it is closed, the name (double-click it to rename the place), the address on one line with the full address in a tooltip, and pills: **Open** or **Closed**, the category, the time of the visit, the Google rating, the phone number to call, the e-mail address, and the coordinates. A place with an address but no coordinates shows **No coordinates, so not on the map** instead, with a tooltip on how to fix it. The chevron (**Collapse**) folds the inspector to a slim bar with the photo and the name, so the place stays selected and its marker highlighted while you pan the map; **Expand** unfolds it again, and the next place you pick opens in full. **X** closes the inspector and deselects the place.
- **Rating**: every member's vote, see [Rating a place](#rating-a-place).
- **Description**, **Notes** and **Notes for this day**, rendered as Markdown.
- **Bookings** pinned to this stop, as small cards with the status, date, time and booking code. For a hotel, the stays booked there are listed too (see [Accommodations](Accommodations)). Click a card (*Open booking*) to open the booking's detail. Next to them, **Participants** says who joins this stop.