fix(planner): a place added from a collection keeps its category (#3074)

The place form guessed the category of a picked place from its kind, so a
place taken from a collection inside a trip lost the category it was saved
under, while adding it from the collection kept it. A picked place that
carries a category this server has now brings it along; a category chosen
in the form by hand still wins.
This commit is contained in:
Maurice
2026-11-12 23:06:35 +01:00
parent f13a0aa6ee
commit a39f3fe40c
3 changed files with 49 additions and 1 deletions
@@ -410,3 +410,15 @@ export function formPin(form: Pick<PlaceFormData, 'lat' | 'lng'>): { lat: number
if (!Number.isFinite(lat) || !Number.isFinite(lng) || Math.abs(lat) > 90 || Math.abs(lng) > 180) return null
return { lat, lng }
}
/**
* The category a picked place already carries, as the form's value, when this server
* has it. A place saved in a collection keeps its category_id, and categories are the
* server's own, so it fits the trip as it stands. Guessing from the place's kind
* instead dropped it (#3074).
*/
export function savedCategoryId(result: Record<string, unknown>, categories: readonly { id: number }[]): string | null {
const id = result.category_id
if (typeof id !== 'number') return null
return categories.some(c => c.id === id) ? String(id) : null
}
@@ -2378,6 +2378,36 @@ describe('PlaceFormModal on the dialog frame', () => {
expect(onClose).not.toHaveBeenCalled();
});
it('FE-PLANNER-PLACEFORM-125: a place picked from a collection keeps the category it was saved under (#3074)', async () => {
withCollections();
vi.spyOn(collectionsApi, 'get').mockResolvedValue({
collection: { id: 1, owner_id: 1, name: 'Tokyo 2026', color: '#ec4899' },
places: [
{
id: 10,
collection_id: 1,
name: 'Zebra Cafe',
status: 'idea',
lat: 35.66,
lng: 139.7,
category_id: 7,
category: { id: 7, name: 'Must see', color: '#f59e0b', icon: 'Star' },
},
],
});
const onSave = vi.fn().mockResolvedValue(undefined);
// A cafe saved as "Must see": the guess from its kind would be the cafe category.
const categories = [
{ id: 3, name: 'Cafe', color: '#7c3aed', icon: 'Coffee' },
{ id: 7, name: 'Must see', color: '#f59e0b', icon: 'Star' },
];
render(<PlaceFormModal {...defaultProps} categories={categories} onSave={onSave} />);
fireEvent.click(await screen.findByText('Zebra Cafe'));
fireEvent.click(screen.getByRole('button', { name: /^Add$/i }));
await waitFor(() => expect(onSave).toHaveBeenCalled());
expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ name: 'Zebra Cafe', category_id: '7' }));
});
it('FE-PLANNER-PLACEFORM-099: Escape with nothing open closes the dialog', () => {
const onClose = vi.fn();
render(<PlaceFormModal {...defaultProps} onClose={onClose} />);
@@ -19,7 +19,7 @@ import { weekFromPeriods } from './placeHours'
import type { PlaceOpeningHours } from '@trek/shared'
import {
DEFAULT_FORM, endsBeforeStart, findDuplicatePlace, formPin, mergeResult, parseCoordinatePair, placeEditForm, placeFormPayload,
prefillForm, type PlaceFormData, type PlacePrefill,
prefillForm, savedCategoryId, type PlaceFormData, type PlacePrefill,
} from './PlaceFormModal.helpers'
import { useNewCategory, usePlaceForm } from './usePlaceForm'
import { usePlaceSearch } from './usePlaceSearch'
@@ -233,6 +233,12 @@ function usePlaceFormModal(props: PlaceFormModalProps) {
setForm(prev => {
const merged = mergeResult(prev, result, autoFilledRef.current)
if (prev.category_id && !categoryGuessedRef.current) return merged
// A place from a collection brings the category it was saved under (#3074).
const saved = savedCategoryId(result, categories || [])
if (saved) {
categoryGuessedRef.current = false
return { ...merged, category_id: saved }
}
return { ...merged, category_id: guessedCategory(result) }
})
setPickedSource(label)