mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-18 21:16:47 +00:00
revert(collections): a saved place carries no price, website or phone again
Takes back the editable price, currency, website and phone on collection places from0df82185band its review follow-up ina910c2a83. 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 fromf16698bdb, which only mattered for the price field.
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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': 'مجموعتي',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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ó',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Η συλλογή μου',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'マイコレクション',
|
||||
|
||||
@@ -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': '내 컬렉션',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Моя коллекция',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': 'Моя колекція',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '我的收藏',
|
||||
|
||||
@@ -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': '我的收藏',
|
||||
|
||||
Reference in New Issue
Block a user