diff --git a/client/src/components/Planner/PlaceFormModal.test.tsx b/client/src/components/Planner/PlaceFormModal.test.tsx
index 9861a63cb..8ed801975 100644
--- a/client/src/components/Planner/PlaceFormModal.test.tsx
+++ b/client/src/components/Planner/PlaceFormModal.test.tsx
@@ -55,6 +55,21 @@ beforeEach(() => {
});
describe('PlaceFormModal', () => {
+ it('FE-COMP-PLACEFORM-2472: an empty search offers adding the place by hand and takes the query as the name', async () => {
+ const user = userEvent.setup();
+ server.use(http.post('/api/maps/search', () => HttpResponse.json({ places: [], source: 'osm' })));
+ render(
);
+ const search = screen.getByPlaceholderText(/Search places/i);
+ await user.type(search, 'Tiny Garage Cafe');
+ await user.click(screen.getByRole('button', { name: 'Search' }));
+ expect(await screen.findByText('Nothing found for “Tiny Garage Cafe”')).toBeInTheDocument();
+ await user.click(screen.getByRole('button', { name: 'Add by hand' }));
+ expect(screen.queryByText('Nothing found for “Tiny Garage Cafe”')).not.toBeInTheDocument();
+ // The search field keeps the query and the name field now holds it too.
+ expect(screen.getAllByDisplayValue('Tiny Garage Cafe')).toHaveLength(2);
+ expect(screen.getByLabelText('Phone')).toBeInTheDocument();
+ });
+
it('FE-COMP-PLACEFORM-001: renders modal when isOpen is true', () => {
render(
);
expect(document.body).toBeInTheDocument();
diff --git a/client/src/components/Planner/PlaceFormModal.tsx b/client/src/components/Planner/PlaceFormModal.tsx
index d2ccf5b71..58ce9bc24 100644
--- a/client/src/components/Planner/PlaceFormModal.tsx
+++ b/client/src/components/Planner/PlaceFormModal.tsx
@@ -12,7 +12,7 @@ import CollectionPicker from '../Collections/CollectionPicker'
import PlaceDetailsColumn, { type PlaceDetailsSelection } from './PlaceDetailsColumn'
import { useToast } from '../shared/Toast'
import { Tooltip } from '../shared/Tooltip'
-import { Search, Paperclip, X, AlertTriangle, Loader2, Plus, RotateCcw, MapPin, Navigation, LocateFixed } from 'lucide-react'
+import { Search, Paperclip, X, AlertTriangle, Loader2, Plus, RotateCcw, MapPin, Navigation, LocateFixed, PencilLine } from 'lucide-react'
import { useTranslation } from '../../i18n'
import CustomTimePicker from '../shared/CustomTimePicker'
import { PlaceContactFields } from './PlaceContactFields'
@@ -164,6 +164,9 @@ function usePlaceFormModal(props: PlaceFormModalProps) {
return id != null ? String(id) : ''
}
const [pendingFiles, setPendingFiles] = useState([])
+ // The query the last full search found nothing for (#2472): the cue to add the
+ // place by hand instead of a silent empty list.
+ const [emptySearch, setEmptySearch] = useState
(null)
/**
* The leg of the drive the traveller picked, or empty while the projection's own
* answer stands. Empty rather than seeded, because there is nothing to project onto
@@ -451,6 +454,7 @@ function usePlaceFormModal(props: PlaceFormModalProps) {
searchMetaRef.current = { query: trimmed, source: result.source || 'unknown' }
setNearbyList(false)
setMapsResults(result.places || [])
+ setEmptySearch((result.places || []).length === 0 ? trimmed : null)
setSearchSource(result.source || '')
} catch (err: unknown) {
if (epoch !== searchEpochRef.current) return
@@ -818,6 +822,7 @@ function usePlaceFormModal(props: PlaceFormModalProps) {
setShowNewCategory,
isSaving,
pendingFiles,
+ emptySearch, setEmptySearch,
fileRef,
acSuggestions,
setAcSuggestions,
@@ -1037,7 +1042,7 @@ export default function PlaceFormModal(props: PlaceFormModalProps) {
// in from the map already has what the search would give.
autoFocus={!place && !prefillCoords}
value={mapsSearch}
- onChange={e => setMapsSearch(e.target.value)}
+ onChange={e => { setMapsSearch(e.target.value); S.setEmptySearch(null) }}
onKeyDown={handleSearchKeyDown}
onBlur={() => setTimeout(() => setAcSuggestions([]), 150)}
onFocus={() => {
@@ -1103,6 +1108,30 @@ export default function PlaceFormModal(props: PlaceFormModalProps) {