revert(collections): a saved place carries no price, website or phone again

Takes back the editable price, currency, website and phone on collection
places from 0df82185b and its review follow-up in a910c2a83. Costs are kept
inside a trip only; a collection does not carry them, and #2471 is a
feature request rather than a bug. The collection files are back to how
they were before, including the dimmed save button from f16698bdb, which
only mattered for the price field.
This commit is contained in:
Maurice
2026-09-23 20:32:41 +02:00
parent f16698bdb1
commit a1d6348c5f
40 changed files with 25 additions and 937 deletions
@@ -1,6 +1,5 @@
import {
collectionDeleteManyRequestSchema,
collectionPlaceCurrencySchema,
collectionPlaceUpdateRequestSchema,
collectionReorderRequestSchema,
collectionSavePlaceRequestSchema,
@@ -48,65 +47,6 @@ describe('collectionPlaceUpdateRequestSchema', () => {
});
});
// #2471: price, currency, website and phone were columns the update contract did
// not know, so the validation pipe stripped them and a saved place's cost could
// never be edited.
describe('collectionPlaceUpdateRequestSchema price, currency, website and phone', () => {
it('passes all four through', () => {
const parsed = collectionPlaceUpdateRequestSchema.parse({
price: 12.5,
currency: 'CHF',
website: 'https://museum.example',
phone: '+41 44 123 45 67',
});
expect(parsed).toEqual({
price: 12.5,
currency: 'CHF',
website: 'https://museum.example',
phone: '+41 44 123 45 67',
});
});
it('accepts null on each to clear it, and zero as a price', () => {
const none = { price: null, currency: null, website: null, phone: null };
expect(collectionPlaceUpdateRequestSchema.parse(none)).toEqual(none);
expect(collectionPlaceUpdateRequestSchema.parse({ price: 0 }).price).toBe(0);
});
it('leaves all four absent when an unrelated field changes', () => {
const parsed = collectionPlaceUpdateRequestSchema.parse({ name: 'Kunsthaus' });
for (const key of ['price', 'currency', 'website', 'phone']) expect(key in parsed).toBe(false);
});
it('refuses a negative or non-finite price', () => {
for (const price of [-1, -0.01, Number.NaN, Number.POSITIVE_INFINITY]) {
expect(collectionPlaceUpdateRequestSchema.safeParse({ price }).success).toBe(false);
}
});
it('normalises the currency to a trimmed upper-case code', () => {
expect(collectionPlaceUpdateRequestSchema.parse({ currency: ' usd ' }).currency).toBe('USD');
expect(collectionPlaceCurrencySchema.parse('jpy')).toBe('JPY');
});
it('refuses a currency that is not three letters', () => {
for (const currency of ['EU', 'EURO', '€', '12A', '']) {
expect(collectionPlaceUpdateRequestSchema.safeParse({ currency }).success).toBe(false);
}
});
it('holds the website to http or https, like the save side', () => {
expect(collectionPlaceUpdateRequestSchema.safeParse({ website: 'javascript:alert(1)' }).success).toBe(false);
expect(collectionPlaceUpdateRequestSchema.safeParse({ website: 'http://museum.example' }).success).toBe(true);
});
it('trims the phone and caps it at 60 characters', () => {
expect(collectionPlaceUpdateRequestSchema.parse({ phone: ' +49 30 1234 ' }).phone).toBe('+49 30 1234');
expect(collectionPlaceUpdateRequestSchema.safeParse({ phone: '1'.repeat(60) }).success).toBe(true);
expect(collectionPlaceUpdateRequestSchema.safeParse({ phone: '1'.repeat(61) }).success).toBe(false);
});
});
// The two ratchet schemas replace hand-rolled "array of numbers" checks — they
// must stay exactly that permissive (empty arrays included, no int/positive).
describe('collectionReorderRequestSchema / collectionDeleteManyRequestSchema', () => {
@@ -184,31 +184,10 @@ export const collectionSaveFromTripManyRequestSchema = z.object({
});
export type CollectionSaveFromTripManyRequest = z.infer<typeof collectionSaveFromTripManyRequestSchema>;
/**
* A saved place's currency. A collection has no base currency to fall back on,
* so a price always names its own: three letters, trimmed and upper-cased so
* 'eur ' lands as 'EUR'.
*/
export const collectionPlaceCurrencySchema = z
.string()
.trim()
.regex(/^[A-Za-z]{3}$/, { message: 'must be a three-letter ISO 4217 code' })
.toUpperCase();
export const collectionPlaceUpdateRequestSchema = z.object({
name: z.string().min(1).optional(),
description: z.string().nullable().optional(),
notes: z.string().nullable().optional(),
// A rough cost such as an entry fee (#2471). The columns were always there and
// travelled along on save and on copy into a trip, but nothing could edit them.
// Stricter than the save side on purpose (no negative amount, a real currency
// code); the edit forms only resend a field the user changed, so an older row
// never blocks an unrelated edit.
price: z.number().nonnegative().nullable().optional(),
currency: collectionPlaceCurrencySchema.nullable().optional(),
website: placeWebsiteSchema.nullable().optional(),
// Capped like the file import's phone, which is where most of them come from.
phone: z.string().trim().max(60).nullable().optional(),
// Editable coordinates so a place added by GPS can be corrected later (#1435).
lat: z.number().nullable().optional(),
lng: z.number().nullable().optional(),
-3
View File
@@ -59,9 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'افتراضي',
'collections.sort.nameAsc': 'الاسم (أ–ي)',
'collections.coordinates': 'الإحداثيات',
'collections.price': 'السعر',
'collections.priceHint': 'تكلفة تقريبية، مثل رسوم الدخول. تنتقل مع المكان عند نسخه إلى رحلة.',
'collections.phone': 'الهاتف',
'collections.showList': 'إظهار القائمة',
'collections.expandMap': 'توسيع الخريطة',
'collections.hero.mine': 'مجموعتي',
-4
View File
@@ -59,10 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'Padrão',
'collections.sort.nameAsc': 'Nome (A–Z)',
'collections.coordinates': 'Coordenadas',
'collections.price': 'Preço',
'collections.priceHint':
'Um custo aproximado, como uma entrada. Ele vai junto quando você copia o lugar para uma viagem.',
'collections.phone': 'Telefone',
'collections.showList': 'Mostrar lista',
'collections.expandMap': 'Ampliar mapa',
'collections.hero.mine': 'Minha coleção',
-3
View File
@@ -59,9 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'Predeterminat',
'collections.sort.nameAsc': 'Nom (A–Z)',
'collections.coordinates': 'Coordenades',
'collections.price': 'Preu',
'collections.priceHint': 'Un cost aproximat, com ara una entrada. Acompanya el lloc quan el copies a un viatge.',
'collections.phone': 'Telèfon',
'collections.showList': 'Mostra la llista',
'collections.expandMap': 'Ampliar el mapa',
'collections.hero.mine': 'La meva col·lecció',
-3
View File
@@ -59,9 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'Výchozí',
'collections.sort.nameAsc': 'Název (A–Z)',
'collections.coordinates': 'Souřadnice',
'collections.price': 'Cena',
'collections.priceHint': 'Přibližná cena, například vstupné. Přenese se s místem při kopírování do výletu.',
'collections.phone': 'Telefon',
'collections.showList': 'Zobrazit seznam',
'collections.expandMap': 'Zvětšit mapu',
'collections.hero.mine': 'Moje sbírka',
-4
View File
@@ -59,10 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'Standard',
'collections.sort.nameAsc': 'Name (A–Z)',
'collections.coordinates': 'Koordinaten',
'collections.price': 'Preis',
'collections.priceHint':
'Ungefähre Kosten, etwa ein Eintritt. Sie kommen mit, wenn du den Ort in eine Reise kopierst.',
'collections.phone': 'Telefon',
'collections.showList': 'Liste zeigen',
'collections.expandMap': 'Karte vergrößern',
'collections.hero.mine': 'Meine Sammlung',
-3
View File
@@ -59,9 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'Default',
'collections.sort.nameAsc': 'Name (A–Z)',
'collections.coordinates': 'Coordinates',
'collections.price': 'Price',
'collections.priceHint': 'A rough cost, such as an entry fee. It goes along when you copy the place into a trip.',
'collections.phone': 'Phone',
'collections.showList': 'Show list',
'collections.expandMap': 'Expand map',
'collections.hero.mine': 'My collection',
-4
View File
@@ -59,10 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'Predeterminado',
'collections.sort.nameAsc': 'Nombre (A–Z)',
'collections.coordinates': 'Coordenadas',
'collections.price': 'Precio',
'collections.priceHint':
'Un precio orientativo, como el de una entrada. Se incluye cuando copias el lugar a un viaje.',
'collections.phone': 'Teléfono',
'collections.showList': 'Mostrar lista',
'collections.expandMap': 'Ampliar mapa',
'collections.hero.mine': 'Mi colección',
-4
View File
@@ -59,10 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'Par défaut',
'collections.sort.nameAsc': 'Nom (A–Z)',
'collections.coordinates': 'Coordonnées',
'collections.price': 'Prix',
'collections.priceHint':
'Un coût approximatif, comme un droit d’entrée. Il suit le lieu quand vous le copiez vers un voyage.',
'collections.phone': 'Téléphone',
'collections.showList': 'Afficher la liste',
'collections.expandMap': 'Agrandir la carte',
'collections.hero.mine': 'Ma collection',
-4
View File
@@ -59,10 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'Προεπιλογή',
'collections.sort.nameAsc': 'Όνομα (Α–Ω)',
'collections.coordinates': 'Συντεταγμένες',
'collections.price': 'Τιμή',
'collections.priceHint':
'Ένα κατά προσέγγιση κόστος, όπως ένα εισιτήριο εισόδου. Μεταφέρεται μαζί με το μέρος κατά την αντιγραφή σε ταξίδι.',
'collections.phone': 'Τηλέφωνο',
'collections.showList': 'Εμφάνιση λίστας',
'collections.expandMap': 'Μεγέθυνση χάρτη',
'collections.hero.mine': 'Η συλλογή μου',
+1 -6
View File
@@ -59,10 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'Alapértelmezett',
'collections.sort.nameAsc': 'Név (A–Z)',
'collections.coordinates': 'Koordináták',
'collections.price': 'Ár',
'collections.priceHint':
'Hozzávetőleges költség, például belépődíj. A hellyel együtt átkerül, amikor utazásba másolod.',
'collections.phone': 'Telefon',
'collections.showList': 'Lista megjelenítése',
'collections.expandMap': 'Térkép nagyítása',
'collections.hero.mine': 'Saját gyűjtemény',
@@ -161,8 +157,7 @@ const collection: TranslationStrings = {
'collections.file.targetExisting': 'Hozzáadás listához',
'collections.file.targetExistingHint': 'Egy meglévő listához',
'collections.file.searchLists': 'Listák keresése',
'collections.file.intoHint':
'A listában már meglévő helyek változatlanok maradnak, ahogy a neve és a színe is. A fájl címkéi bekerülnek.',
'collections.file.intoHint': 'A listában már meglévő helyek változatlanok maradnak, ahogy a neve és a színe is. A fájl címkéi bekerülnek.',
'collections.file.confirmInto': 'Hozzáadás a listához',
'collections.file.doneInto': '{count} hely hozzáadva ehhez: {name}',
'collections.file.doneIntoDuplicates': '{count} hozzáadva ehhez: {name}, {duplicates} már benne volt',
-4
View File
@@ -59,10 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'Bawaan',
'collections.sort.nameAsc': 'Nama (A–Z)',
'collections.coordinates': 'Koordinat',
'collections.price': 'Harga',
'collections.priceHint':
'Perkiraan biaya, misalnya tiket masuk. Ikut tersalin saat tempat ini disalin ke perjalanan.',
'collections.phone': 'Telepon',
'collections.showList': 'Tampilkan daftar',
'collections.expandMap': 'Perbesar peta',
'collections.hero.mine': 'Koleksi saya',
-4
View File
@@ -59,10 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'Predefinito',
'collections.sort.nameAsc': 'Nome (A–Z)',
'collections.coordinates': 'Coordinate',
'collections.price': 'Prezzo',
'collections.priceHint':
'Un costo indicativo, come un biglietto d’ingresso. Viene copiato insieme al luogo quando lo copi in un viaggio.',
'collections.phone': 'Telefono',
'collections.showList': 'Mostra elenco',
'collections.expandMap': 'Espandi mappa',
'collections.hero.mine': 'La mia raccolta',
-3
View File
@@ -59,9 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'デフォルト',
'collections.sort.nameAsc': '名前順(A–Z)',
'collections.coordinates': '座標',
'collections.price': '価格',
'collections.priceHint': '入場料などのおおよその費用です。場所を旅行にコピーすると一緒にコピーされます。',
'collections.phone': '電話',
'collections.showList': 'リストを表示',
'collections.expandMap': '地図を拡大',
'collections.hero.mine': 'マイコレクション',
-3
View File
@@ -59,9 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': '기본',
'collections.sort.nameAsc': '이름순 (A–Z)',
'collections.coordinates': '좌표',
'collections.price': '가격',
'collections.priceHint': '입장료 같은 대략적인 비용입니다. 장소를 여행에 복사하면 함께 복사됩니다.',
'collections.phone': '전화',
'collections.showList': '목록 표시',
'collections.expandMap': '지도 확대',
'collections.hero.mine': '내 컬렉션',
-4
View File
@@ -59,10 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'Standaard',
'collections.sort.nameAsc': 'Naam (A–Z)',
'collections.coordinates': 'Coördinaten',
'collections.price': 'Prijs',
'collections.priceHint':
'Een globale prijs, zoals een entreeprijs. Gaat mee wanneer je de plek naar een reis kopieert.',
'collections.phone': 'Telefoon',
'collections.showList': 'Lijst tonen',
'collections.expandMap': 'Kaart vergroten',
'collections.hero.mine': 'Mijn collectie',
-4
View File
@@ -59,10 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'Domyślnie',
'collections.sort.nameAsc': 'Nazwa (A–Z)',
'collections.coordinates': 'Współrzędne',
'collections.price': 'Cena',
'collections.priceHint':
'Przybliżony koszt, na przykład bilet wstępu. Przechodzi razem z miejscem, gdy kopiujesz je do podróży.',
'collections.phone': 'Telefon',
'collections.showList': 'Pokaż listę',
'collections.expandMap': 'Powiększ mapę',
'collections.hero.mine': 'Moja kolekcja',
-4
View File
@@ -59,10 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'По умолчанию',
'collections.sort.nameAsc': 'Название (А–Я)',
'collections.coordinates': 'Координаты',
'collections.price': 'Цена',
'collections.priceHint':
'Примерная стоимость, например входной билет. Переносится вместе с местом при копировании в поездку.',
'collections.phone': 'Телефон',
'collections.showList': 'Показать список',
'collections.expandMap': 'Развернуть карту',
'collections.hero.mine': 'Моя коллекция',
-4
View File
@@ -59,10 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'Standard',
'collections.sort.nameAsc': 'Namn (A–Ö)',
'collections.coordinates': 'Koordinater',
'collections.price': 'Pris',
'collections.priceHint':
'En ungefärlig kostnad, till exempel ett inträde. Den följer med när du kopierar platsen till en resa.',
'collections.phone': 'Telefon',
'collections.showList': 'Visa lista',
'collections.expandMap': 'Förstora karta',
'collections.hero.mine': 'Min samling',
-4
View File
@@ -59,10 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'Varsayılan',
'collections.sort.nameAsc': 'Ad (A–Z)',
'collections.coordinates': 'Koordinatlar',
'collections.price': 'Fiyat',
'collections.priceHint':
'Giriş ücreti gibi yaklaşık bir maliyet. Yeri bir geziye kopyaladığınızda onunla birlikte taşınır.',
'collections.phone': 'Telefon',
'collections.showList': 'Listeyi göster',
'collections.expandMap': 'Haritayı genişlet',
'collections.hero.mine': 'Koleksiyonum',
-4
View File
@@ -59,10 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'За замовчуванням',
'collections.sort.nameAsc': 'Назва (А–Я)',
'collections.coordinates': 'Координати',
'collections.price': 'Ціна',
'collections.priceHint':
'Приблизна вартість, наприклад вхідний квиток. Переноситься разом із місцем під час копіювання в подорож.',
'collections.phone': 'Телефон',
'collections.showList': 'Показати список',
'collections.expandMap': 'Розгорнути карту',
'collections.hero.mine': 'Моя колекція',
-4
View File
@@ -59,10 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': 'Mặc định',
'collections.sort.nameAsc': 'Tên (A–Z)',
'collections.coordinates': 'Tọa độ',
'collections.price': 'Giá',
'collections.priceHint':
'Chi phí ước tính, chẳng hạn vé vào cửa. Được sao chép cùng địa điểm khi bạn sao chép nó vào chuyến đi.',
'collections.phone': 'Điện thoại',
'collections.showList': 'Hiện danh sách',
'collections.expandMap': 'Mở rộng bản đồ',
'collections.hero.mine': 'Bộ sưu tập của tôi',
-3
View File
@@ -59,9 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': '預設',
'collections.sort.nameAsc': '名稱 (A–Z)',
'collections.coordinates': '座標',
'collections.price': '價格',
'collections.priceHint': '大致費用,例如門票。將地點複製到行程時會一併帶上。',
'collections.phone': '電話',
'collections.showList': '顯示清單',
'collections.expandMap': '展開地圖',
'collections.hero.mine': '我的收藏',
-3
View File
@@ -59,9 +59,6 @@ const collection: TranslationStrings = {
'collections.sort.default': '默认',
'collections.sort.nameAsc': '名称 (A–Z)',
'collections.coordinates': '坐标',
'collections.price': '价格',
'collections.priceHint': '大致费用,例如门票。将地点复制到行程时会一并带上。',
'collections.phone': '电话',
'collections.showList': '显示列表',
'collections.expandMap': '展开地图',
'collections.hero.mine': '我的收藏',