diff --git a/client/src/components/Assistant/cards/collection/CollectionChangeSlots.test.tsx b/client/src/components/Assistant/cards/collection/CollectionChangeSlots.test.tsx index ec4698630..aa83c4d2b 100644 --- a/client/src/components/Assistant/cards/collection/CollectionChangeSlots.test.tsx +++ b/client/src/components/Assistant/cards/collection/CollectionChangeSlots.test.tsx @@ -1,5 +1,5 @@ /** - * FE-AI-COLL-020 to FE-AI-COLL-031 + * FE-AI-COLL-020 to FE-AI-COLL-031, FE-AI-COLL-041 * * The collections inside the change card: a held save with its list, its * status and the lists that hold the place already; a change of status, of @@ -165,6 +165,14 @@ describe('collection change cards', () => { expect(screen.getByText('+2')).toBeInTheDocument(); }); + it('FE-AI-COLL-041: a change of the website and the map entry names both (#2646)', () => { + const delta = emptyDelta({ fields: ['website', 'mapEntry'] }); + const card = placesCard({ places: [savedPlace({ delta })] }); + draw([pending('update_collection_place', 'Septime', card, 'genericUpdate')]); + expect(screen.getByText('Website')).toBeInTheDocument(); + expect(screen.getByText('Map entry')).toBeInTheDocument(); + }); + it('FE-AI-COLL-029: trip places saved to a list are trip places by nature, so no warning is given', () => { const louvre = savedPlace({ id: null, name: 'Louvre', inTripPlaceId: 6 }); const card = placesCard({ places: [savedPlace({ id: null, inTripPlaceId: 5 }), louvre] }); diff --git a/client/src/components/Assistant/cards/collection/CollectionChangeSlots.tsx b/client/src/components/Assistant/cards/collection/CollectionChangeSlots.tsx index 877a46e7a..07c9cabe2 100644 --- a/client/src/components/Assistant/cards/collection/CollectionChangeSlots.tsx +++ b/client/src/components/Assistant/cards/collection/CollectionChangeSlots.tsx @@ -45,6 +45,10 @@ const FIELD_LOOK: Record category_id: number | null; image_url: string | null; links: string | null; + google_place_id: string | null; + google_ftid: string | null; + osm_id: string | null; + website: string | null; + phone: string | null; + price: number | null; + currency: string | null; collection_id: number; owner_id: number; }>, diff --git a/server/src/nest/assistant/cards/collection/collection-changes.ts b/server/src/nest/assistant/cards/collection/collection-changes.ts index 8bc1949ca..c2744d26b 100644 --- a/server/src/nest/assistant/cards/collection/collection-changes.ts +++ b/server/src/nest/assistant/cards/collection/collection-changes.ts @@ -77,6 +77,13 @@ const FIELD_OF: Readonly category_id: 'category', links: 'link', image_url: 'photo', + website: 'website', + phone: 'phone', + price: 'price', + currency: 'price', + google_place_id: 'mapEntry', + google_ftid: 'mapEntry', + osm_id: 'mapEntry', }; function fieldsOf(args: ToolArgs): CollectionCardDelta['fields'] { diff --git a/server/src/nest/collections/collections.helpers.ts b/server/src/nest/collections/collections.helpers.ts index 9cf4d1330..e8a0447b8 100644 --- a/server/src/nest/collections/collections.helpers.ts +++ b/server/src/nest/collections/collections.helpers.ts @@ -1,4 +1,4 @@ -import type { CollectionLink } from '@trek/shared'; +import type { CollectionLink, CollectionPlaceUpdateRequest } from '@trek/shared'; /** * Pure helpers of the collections domain, moved verbatim out of @@ -20,3 +20,31 @@ export function parseLinks(raw: unknown): CollectionLink[] | undefined { export function serializeLinks(links: CollectionLink[] | undefined): string | null { return links && links.length ? JSON.stringify(links) : null; } + +/** The provider ids, contact and price columns of a saved place, as an update writes them. */ +type ListingWrite = Partial<{ + google_place_id: string | null; + google_ftid: string | null; + osm_id: string | null; + website: string | null; + phone: string | null; + price: number | null; + currency: string | null; +}>; + +/** + * The fields saving a place accepts and editing it did not until #2646: each one + * the update names, null clearing it, the rest left out so the stored value stays. + * The website arrives normalised and checked by the contract, as on save. + */ +export function listingWriteOf(body: CollectionPlaceUpdateRequest): ListingWrite { + const write: ListingWrite = {}; + if (body.google_place_id !== undefined) write.google_place_id = body.google_place_id ?? null; + if (body.google_ftid !== undefined) write.google_ftid = body.google_ftid ?? null; + if (body.osm_id !== undefined) write.osm_id = body.osm_id ?? null; + if (body.website !== undefined) write.website = body.website ?? null; + if (body.phone !== undefined) write.phone = body.phone ?? null; + if (body.price !== undefined) write.price = body.price ?? null; + if (body.currency !== undefined) write.currency = body.currency ?? null; + return write; +} diff --git a/server/src/nest/collections/collections.mcp.ts b/server/src/nest/collections/collections.mcp.ts index b20c35589..c4a5acc52 100644 --- a/server/src/nest/collections/collections.mcp.ts +++ b/server/src/nest/collections/collections.mcp.ts @@ -327,7 +327,7 @@ export class CollectionsMcp { @Tool({ name: 'update_collection_place', description: - "Update a saved place's name, address, coordinates (lat/lng), description, notes, status, category, links, tags, labels, image, or move it to another collection (set collection_id). Moving a place out of a list needs the same role there as deleting it (owner or admin).", + "Update a saved place's name, address, coordinates (lat/lng), description, notes, status, category, links, tags, labels, image, provider ids (google_place_id, google_ftid, osm_id), website, phone, price and currency, or move it to another collection (set collection_id). Only the fields you pass change; null clears one. A bare domain as website gets https://, any other scheme is refused. Moving a place out of a list needs the same role there as deleting it (owner or admin).", inputSchema: { placeId: idSchema, ...collectionPlaceUpdateRequestSchema.shape, links: linksInput }, annotations: TOOL_ANNOTATIONS_WRITE, when: collectionsAddonOn, diff --git a/server/src/nest/collections/collections.service.ts b/server/src/nest/collections/collections.service.ts index f0bc01366..c2ccb5108 100644 --- a/server/src/nest/collections/collections.service.ts +++ b/server/src/nest/collections/collections.service.ts @@ -75,7 +75,7 @@ import type { import { NotificationsService } from '../notifications/notifications.service'; import { PlaceImportService, type ExportedCollectionFile } from '../place-import/place-import.service'; import { UnitOfWork } from '../database/unit-of-work'; -import { parseLinks, serializeLinks } from './collections.helpers'; +import { listingWriteOf, parseLinks, serializeLinks } from './collections.helpers'; // Errors: a DomainError carrying a status; TrekExceptionFilter maps it to that code with an `{ error }` body. @@ -872,7 +872,7 @@ export class CollectionsService { await this.assertCanEdit(userId, currentCollection); const prevImage = body.image_url !== undefined ? (await this.collectionPlaces.imageUrl(placeId)) ?? null : null; - const write: Parameters[1] = {}; + const write: Parameters[1] = listingWriteOf(body); if (body.name !== undefined) write.name = body.name; if (body.description !== undefined) write.description = body.description ?? null; if (body.notes !== undefined) write.notes = body.notes ?? null; diff --git a/server/tests/e2e/collections.e2e.test.ts b/server/tests/e2e/collections.e2e.test.ts index 6fb4dc49c..10a19fd75 100644 --- a/server/tests/e2e/collections.e2e.test.ts +++ b/server/tests/e2e/collections.e2e.test.ts @@ -208,6 +208,66 @@ describe('Collections e2e (real auth guard + real service + temp SQLite)', () => expect(await countRows(orm, CollectionPlaces, { collection: col.id })).toBe(0); }); + // #2646: saving took the provider ids, website, phone and price while editing + // did not, so fixing one meant deleting and re-saving the place, which lost its + // labels, ratings, position and id. + it('COLLECTIONS-E2E-091: PATCH sets and clears the provider ids, website, phone and price', async () => { + const col = ( + await request(server) + .post('/api/addons/collections') + .set('Cookie', sessionCookie(ownerId)) + .send({ name: 'Listings' }) + ).body; + const saved = await request(server) + .post('/api/addons/collections/places') + .set('Cookie', sessionCookie(ownerId)) + .send({ collection_id: col.id, name: 'Septime', phone: '+33 1 43 67 38 29', price: 40, currency: 'EUR' }); + expect(saved.status).toBe(200); + const placeId = saved.body.place.id; + const patch = (body: Record) => + request(server) + .patch(`/api/addons/collections/places/${placeId}`) + .set('Cookie', sessionCookie(ownerId)) + .send(body); + + const patched = await patch({ + google_place_id: 'ChIJseptime', + google_ftid: '0x47e6:0x1b2', + osm_id: 'node/42', + website: 'septime-charonne.fr', + price: 55, + }); + expect(patched.status).toBe(200); + // A bare domain gets https:// as on save; what the update leaves out stays. + expect(patched.body).toMatchObject({ + id: placeId, + google_place_id: 'ChIJseptime', + google_ftid: '0x47e6:0x1b2', + osm_id: 'node/42', + website: 'https://septime-charonne.fr', + phone: '+33 1 43 67 38 29', + price: 55, + currency: 'EUR', + }); + + const refused = await patch({ website: 'javascript:alert(1)' }); + expect(refused.status).toBe(400); + expect(refused.body.error).toMatch(/^website: /); + expect((await findRow(orm, CollectionPlaces, { id: placeId }))?.website).toBe('https://septime-charonne.fr'); + + const cleared = await patch({ website: null, phone: null, price: null, currency: null, google_place_id: null }); + expect(cleared.status).toBe(200); + expect(await findRow(orm, CollectionPlaces, { id: placeId })).toMatchObject({ + website: null, + phone: null, + price: null, + currency: null, + google_place_id: null, + google_ftid: '0x47e6:0x1b2', + osm_id: 'node/42', + }); + }); + // Regression for #1437: editing a place (PATCH without a status field) must NOT // reset a 'want'/'visited' place back to 'idea'. it('COLLECTIONS-E2E-012: PATCH without status leaves the saved status unchanged', async () => { diff --git a/server/tests/unit/mcp/tools-collections.test.ts b/server/tests/unit/mcp/tools-collections.test.ts index 3bfc3ff9b..ec708fbe2 100644 --- a/server/tests/unit/mcp/tools-collections.test.ts +++ b/server/tests/unit/mcp/tools-collections.test.ts @@ -565,6 +565,51 @@ describe('Tool: update_collection_place / set_collection_place_status / rate_col }); expect((await findRow(orm, CollectionPlaces, { id: pid }))?.collection_id).toBe(shared); }); + + // #2646: the fields saving takes, through the same contract and checks as REST. + it('update_collection_place sets and clears provider ids, website, phone and price, and refuses a script link', async () => { + const { user } = createUser(testDb); + const col = await seedCollection(user.id, 'Listings'); + const pid = await seedPlace(col, user.id, 'Septime'); + const update = (h: McpHarness, args: Record) => + h.client.callTool({ name: 'update_collection_place', arguments: { placeId: pid, ...args } }); + await withHarness(user.id, async (h) => { + const set = parseToolResult( + await update(h, { + google_place_id: 'ChIJseptime', + osm_id: 'node/42', + website: 'septime-charonne.fr', + phone: '+33 1 43 67 38 29', + price: 55, + currency: 'EUR', + }), + ) as { place: Record }; + expect(set.place).toMatchObject({ + name: 'Septime', + google_place_id: 'ChIJseptime', + osm_id: 'node/42', + website: 'https://septime-charonne.fr', + phone: '+33 1 43 67 38 29', + price: 55, + currency: 'EUR', + }); + + const refused = await update(h, { website: 'javascript:alert(1)' }); + expect(refused.isError).toBe(true); + expect((refused.content as { text: string }[])[0].text).toContain('Invalid arguments'); + + const cleared = parseToolResult(await update(h, { website: null, phone: null, price: null })) as { + place: Record; + }; + expect(cleared.place).toMatchObject({ + website: null, + phone: null, + price: null, + currency: 'EUR', + google_place_id: 'ChIJseptime', + }); + }); + }); }); // --------------------------------------------------------------------------- diff --git a/server/tests/unit/nest/assistant/cards/collection/collection-changes.test.ts b/server/tests/unit/nest/assistant/cards/collection/collection-changes.test.ts index c7ff13dba..2324f7997 100644 --- a/server/tests/unit/nest/assistant/cards/collection/collection-changes.test.ts +++ b/server/tests/unit/nest/assistant/cards/collection/collection-changes.test.ts @@ -1,5 +1,5 @@ /** - * ASTCOL-020 to ASTCOL-035: the change cards of the collections' writes, as + * ASTCOL-020 to ASTCOL-035 and ASTCOL-037: the change cards of the collections' writes, as * describeToolCall writes them (the card through the guarded registry, so it * passed the union), and the lookups they read first. */ @@ -104,6 +104,21 @@ describe('collection change cards', () => { expect(placesOf(removed.card).places[0]?.delta?.removed).toBe(true); }); + it('ASTCOL-037: the provider ids, contact and price fields an update now takes are named once each (#2646)', () => { + const items = itemsValue([savedPlace()]); + const args = { + placeId: 34, + website: 'septime-charonne.fr', + phone: null, + price: 45, + currency: 'EUR', + google_place_id: 'ChIJseptime', + osm_id: 'node/1', + }; + const card = placesOf(preview('update_collection_place', args, { items }).card); + expect(card.places[0]?.delta?.fields).toEqual(['website', 'phone', 'price', 'mapEntry']); + }); + it('ASTCOL-023: a change of a saved place names its fields, its labels as its own list allows them and the list it moves to', () => { const items = itemsValue([savedPlace()]); // In its own list: the labels asked for replace the ones it carries. diff --git a/server/tests/unit/nest/collections.helpers.test.ts b/server/tests/unit/nest/collections.helpers.test.ts index c86cc722d..738d6cbfd 100644 --- a/server/tests/unit/nest/collections.helpers.test.ts +++ b/server/tests/unit/nest/collections.helpers.test.ts @@ -1,9 +1,9 @@ /** - * The link helpers of the collections domain, which read and write the JSON - * TEXT column a list or a saved place keeps its links in. - * COLLECTIONS-HELPERS-001 to 003. + * The pure helpers of the collections domain: the link helpers, which read and + * write the JSON TEXT column a list or a saved place keeps its links in, and the + * listing fields an update writes. COLLECTIONS-HELPERS-001 to 005. */ -import { parseLinks, serializeLinks } from '../../../src/nest/collections/collections.helpers'; +import { listingWriteOf, parseLinks, serializeLinks } from '../../../src/nest/collections/collections.helpers'; import { describe, expect, it } from 'vitest'; @@ -29,3 +29,32 @@ describe('serializeLinks', () => { expect(serializeLinks(undefined)).toBeNull(); }); }); + +describe('listingWriteOf', () => { + it('COLLECTIONS-HELPERS-004: writes each provider id, contact and price field the update names, null clearing it', () => { + expect( + listingWriteOf({ + google_place_id: 'ChIJseptime', + google_ftid: null, + osm_id: 'node/42', + website: 'https://septime-charonne.fr', + phone: null, + price: 0, + currency: 'EUR', + }), + ).toEqual({ + google_place_id: 'ChIJseptime', + google_ftid: null, + osm_id: 'node/42', + website: 'https://septime-charonne.fr', + phone: null, + price: 0, + currency: 'EUR', + }); + }); + + it('COLLECTIONS-HELPERS-005: leaves out what the update does not name, so the stored value stays', () => { + expect(listingWriteOf({ name: 'Septime', notes: 'Book ahead' })).toEqual({}); + expect(listingWriteOf({ website: undefined, phone: '+33 1' })).toEqual({ phone: '+33 1' }); + }); +}); diff --git a/shared/src/assistant/cards/collection-places.card.ts b/shared/src/assistant/cards/collection-places.card.ts index d9317585c..ff90106da 100644 --- a/shared/src/assistant/cards/collection-places.card.ts +++ b/shared/src/assistant/cards/collection-places.card.ts @@ -32,6 +32,11 @@ export const COLLECTION_CARD_FIELDS = [ 'category', 'link', 'photo', + 'website', + 'phone', + 'price', + /** The place's Google or OpenStreetMap entry (its provider ids). */ + 'mapEntry', ] as const; /** What a change does to a label of a list; `none` on an answer. */ @@ -75,7 +80,7 @@ export const collectionCardDeltaSchema = z.object({ rating: z.object({ to: z.number().int().min(1).max(5).nullable() }).nullable(), labelsAdded: z.array(idSchema).max(10), labelsRemoved: z.array(idSchema).max(10), - fields: z.array(z.enum(COLLECTION_CARD_FIELDS)).max(8), + fields: z.array(z.enum(COLLECTION_CARD_FIELDS)).max(COLLECTION_CARD_FIELDS.length), movedTo: collectionCardListSchema.nullable(), removed: z.boolean(), }); diff --git a/shared/src/assistant/cards/collection.contract.spec.ts b/shared/src/assistant/cards/collection.contract.spec.ts index 0799d73cf..f30de610c 100644 --- a/shared/src/assistant/cards/collection.contract.spec.ts +++ b/shared/src/assistant/cards/collection.contract.spec.ts @@ -86,7 +86,7 @@ describe('collection cards', () => { const delta = deltaOf({ status: { from: 'idea', to: 'visited' }, rating: { to: null }, fields: ['photo'] }); const card = { ...placesCard, places: [{ ...place, delta }] }; expect(collectionPlacesCardSchema.parse(card).places[0]?.delta).toEqual(delta); - const unknownField = { ...placesCard, places: [{ ...place, delta: deltaOf({ fields: ['phone'] }) }] }; + const unknownField = { ...placesCard, places: [{ ...place, delta: deltaOf({ fields: ['email'] }) }] }; expect(collectionPlacesCardSchema.safeParse(unknownField).success).toBe(false); }); diff --git a/shared/src/collection/collection.schema.spec.ts b/shared/src/collection/collection.schema.spec.ts index 6f03fc70a..8bec2b4d2 100644 --- a/shared/src/collection/collection.schema.spec.ts +++ b/shared/src/collection/collection.schema.spec.ts @@ -28,6 +28,36 @@ describe('collectionStatusSchema', () => { }); describe('collectionPlaceUpdateRequestSchema', () => { + // #2646: the fields saving takes, with the same checks, so a place is corrected in place. + it('takes the provider ids, website, phone and price, and null clears each', () => { + const set = { + google_place_id: 'ChIJseptime', + google_ftid: '0x47e6:0x1b2', + osm_id: 'node/42', + website: 'https://septime-charonne.fr', + phone: '+33 1 43 67 38 29', + price: 55, + currency: 'EUR', + }; + expect(collectionPlaceUpdateRequestSchema.parse(set)).toEqual(set); + const cleared = Object.fromEntries(Object.keys(set).map((key) => [key, null])); + expect(collectionPlaceUpdateRequestSchema.parse(cleared)).toEqual(cleared); + }); + + it('normalises a bare domain as website and refuses a script link, as saving does', () => { + expect(collectionPlaceUpdateRequestSchema.parse({ website: 'septime-charonne.fr' }).website).toBe( + 'https://septime-charonne.fr', + ); + expect(collectionPlaceUpdateRequestSchema.safeParse({ website: 'javascript:alert(1)' }).success).toBe(false); + }); + + it('leaves the new fields absent when an update does not name them', () => { + const parsed = collectionPlaceUpdateRequestSchema.parse({ name: 'Septime' }); + for (const key of ['google_place_id', 'google_ftid', 'osm_id', 'website', 'phone', 'price', 'currency']) { + expect(key in parsed, key).toBe(false); + } + }); + // Regression for #1437: an update that omits `status` must NOT inject a default // of 'idea', otherwise the server overwrites the stored status on every edit-save. it('does not inject a status when the field is absent', () => { diff --git a/shared/src/collection/collection.schema.ts b/shared/src/collection/collection.schema.ts index 9e153578a..e30106fe3 100644 --- a/shared/src/collection/collection.schema.ts +++ b/shared/src/collection/collection.schema.ts @@ -223,6 +223,16 @@ export const collectionPlaceUpdateRequestSchema = z.object({ label_ids: z.array(z.number()).optional(), // Custom thumbnail (#1136): null clears it (falls back to the auto-fetched photo). image_url: placeImageUrlSchema.nullable().optional(), + // The fields saving accepts but editing did not (#2646), with the same checks, so + // a place can be corrected in place instead of re-saved, which loses its labels, + // ratings, position and id. Each one is left alone when absent; null clears it. + google_place_id: z.string().nullable().optional(), + google_ftid: z.string().nullable().optional(), + osm_id: z.string().nullable().optional(), + website: placeWebsiteSchema.nullable().optional(), + phone: z.string().nullable().optional(), + price: z.number().nullable().optional(), + currency: z.string().nullable().optional(), }); export type CollectionPlaceUpdateRequest = z.infer; diff --git a/shared/src/i18n/ar/assistantCollection.ts b/shared/src/i18n/ar/assistantCollection.ts index 7a67fb5c6..a9cf0b1e3 100644 --- a/shared/src/i18n/ar/assistantCollection.ts +++ b/shared/src/i18n/ar/assistantCollection.ts @@ -55,6 +55,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'نقل إلى {name}', 'assistant.card.collection.removed': 'أُزيل من القائمة', 'assistant.card.collection.photo': 'صورة', + 'assistant.card.collection.mapEntry': 'سجل الخريطة', 'assistant.card.collection.places': 'الأماكن المحفوظة في {name}', 'assistant.card.collection.saved': 'الأماكن المحفوظة', 'assistant.card.collection.statusSet': 'تغيّر {count} مكان محفوظ', diff --git a/shared/src/i18n/az/assistantCollection.ts b/shared/src/i18n/az/assistantCollection.ts index 1da91f56e..d7e8e22c4 100644 --- a/shared/src/i18n/az/assistantCollection.ts +++ b/shared/src/i18n/az/assistantCollection.ts @@ -27,6 +27,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': '{name} siyahısına köçür', 'assistant.card.collection.removed': 'Siyahıdan çıxarıldı', 'assistant.card.collection.photo': 'Foto', + 'assistant.card.collection.mapEntry': 'Xəritə qeydi', 'assistant.card.collection.places': '{name} siyahısındakı saxlanılmış məkanlar', 'assistant.card.collection.saved': 'Saxlanılmış məkanlar', 'assistant.card.collection.statusSet': '{count} saxlanılmış məkan dəyişdi', diff --git a/shared/src/i18n/br/assistantCollection.ts b/shared/src/i18n/br/assistantCollection.ts index 8b71455f2..b6a2337f9 100644 --- a/shared/src/i18n/br/assistantCollection.ts +++ b/shared/src/i18n/br/assistantCollection.ts @@ -27,6 +27,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Mover para {name}', 'assistant.card.collection.removed': 'Removido da lista', 'assistant.card.collection.photo': 'Foto', + 'assistant.card.collection.mapEntry': 'Registro no mapa', 'assistant.card.collection.places': 'Lugares salvos em {name}', 'assistant.card.collection.saved': 'Lugares salvos', 'assistant.card.collection.statusSet': '{count} lugares salvos alterados', diff --git a/shared/src/i18n/ca/assistantCollection.ts b/shared/src/i18n/ca/assistantCollection.ts index 95f68bbe6..8355335f0 100644 --- a/shared/src/i18n/ca/assistantCollection.ts +++ b/shared/src/i18n/ca/assistantCollection.ts @@ -27,6 +27,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Mou a {name}', 'assistant.card.collection.removed': 'Tret de la llista', 'assistant.card.collection.photo': 'Foto', + 'assistant.card.collection.mapEntry': 'Fitxa del mapa', 'assistant.card.collection.places': 'Llocs desats a {name}', 'assistant.card.collection.saved': 'Llocs desats', 'assistant.card.collection.statusSet': '{count} llocs desats canviats', diff --git a/shared/src/i18n/cs/assistantCollection.ts b/shared/src/i18n/cs/assistantCollection.ts index 5828f70a3..392875641 100644 --- a/shared/src/i18n/cs/assistantCollection.ts +++ b/shared/src/i18n/cs/assistantCollection.ts @@ -34,6 +34,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Přesunout do {name}', 'assistant.card.collection.removed': 'Odebráno ze seznamu', 'assistant.card.collection.photo': 'Fotka', + 'assistant.card.collection.mapEntry': 'Záznam na mapě', 'assistant.card.collection.places': 'Uložená místa v {name}', 'assistant.card.collection.saved': 'Uložená místa', 'assistant.card.collection.statusSet': '{count} uložených míst změněno', diff --git a/shared/src/i18n/de/assistantCollection.ts b/shared/src/i18n/de/assistantCollection.ts index 6596670de..9cb507ca6 100644 --- a/shared/src/i18n/de/assistantCollection.ts +++ b/shared/src/i18n/de/assistantCollection.ts @@ -27,6 +27,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Nach {name} verschieben', 'assistant.card.collection.removed': 'Aus der Liste entfernt', 'assistant.card.collection.photo': 'Foto', + 'assistant.card.collection.mapEntry': 'Karteneintrag', 'assistant.card.collection.places': 'Gespeicherte Orte in {name}', 'assistant.card.collection.saved': 'Gespeicherte Orte', 'assistant.card.collection.statusSet': '{count} gespeicherte Orte geändert', diff --git a/shared/src/i18n/en/assistantCollection.ts b/shared/src/i18n/en/assistantCollection.ts index 232302166..4a8ee8d8e 100644 --- a/shared/src/i18n/en/assistantCollection.ts +++ b/shared/src/i18n/en/assistantCollection.ts @@ -27,6 +27,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Move to {name}', 'assistant.card.collection.removed': 'Removed from the list', 'assistant.card.collection.photo': 'Photo', + 'assistant.card.collection.mapEntry': 'Map entry', 'assistant.card.collection.places': 'Saved places in {name}', 'assistant.card.collection.saved': 'Saved places', 'assistant.card.collection.statusSet': '{count} saved places changed', diff --git a/shared/src/i18n/es/assistantCollection.ts b/shared/src/i18n/es/assistantCollection.ts index 9608ceae5..23cb91c9a 100644 --- a/shared/src/i18n/es/assistantCollection.ts +++ b/shared/src/i18n/es/assistantCollection.ts @@ -27,6 +27,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Mover a {name}', 'assistant.card.collection.removed': 'Quitado de la lista', 'assistant.card.collection.photo': 'Foto', + 'assistant.card.collection.mapEntry': 'Ficha del mapa', 'assistant.card.collection.places': 'Lugares guardados en {name}', 'assistant.card.collection.saved': 'Lugares guardados', 'assistant.card.collection.statusSet': '{count} lugares guardados cambiados', diff --git a/shared/src/i18n/et/assistantCollection.ts b/shared/src/i18n/et/assistantCollection.ts index 5e21e8799..2eaba509a 100644 --- a/shared/src/i18n/et/assistantCollection.ts +++ b/shared/src/i18n/et/assistantCollection.ts @@ -27,6 +27,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Teisalda nimekirja {name}', 'assistant.card.collection.removed': 'Nimekirjast eemaldatud', 'assistant.card.collection.photo': 'Foto', + 'assistant.card.collection.mapEntry': 'Kaardikirje', 'assistant.card.collection.places': 'Salvestatud kohad nimekirjas {name}', 'assistant.card.collection.saved': 'Salvestatud kohad', 'assistant.card.collection.statusSet': '{count} salvestatud kohta muudetud', diff --git a/shared/src/i18n/fr/assistantCollection.ts b/shared/src/i18n/fr/assistantCollection.ts index d5eb2ae86..eedce6217 100644 --- a/shared/src/i18n/fr/assistantCollection.ts +++ b/shared/src/i18n/fr/assistantCollection.ts @@ -28,6 +28,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.removed': 'Retiré de la liste', 'assistant.card.collection.photo': 'Photo', // same-as-en 'assistant.card.collection.places': 'Lieux enregistrés dans {name}', + 'assistant.card.collection.mapEntry': 'Fiche sur la carte', 'assistant.card.collection.saved': 'Lieux enregistrés', 'assistant.card.collection.statusSet': '{count} lieux enregistrés modifiés', 'assistant.card.collection.statusSet.one': '{count} lieu enregistré modifié', diff --git a/shared/src/i18n/gr/assistantCollection.ts b/shared/src/i18n/gr/assistantCollection.ts index 6991d129a..e953e6e0c 100644 --- a/shared/src/i18n/gr/assistantCollection.ts +++ b/shared/src/i18n/gr/assistantCollection.ts @@ -27,6 +27,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Μετακίνηση στη λίστα {name}', 'assistant.card.collection.removed': 'Αφαιρέθηκε από τη λίστα', 'assistant.card.collection.photo': 'Φωτογραφία', + 'assistant.card.collection.mapEntry': 'Καταχώριση χάρτη', 'assistant.card.collection.places': 'Αποθηκευμένα μέρη στη λίστα {name}', 'assistant.card.collection.saved': 'Αποθηκευμένα μέρη', 'assistant.card.collection.statusSet': '{count} αποθηκευμένα μέρη άλλαξαν', diff --git a/shared/src/i18n/he/assistantCollection.ts b/shared/src/i18n/he/assistantCollection.ts index 7b87b514c..92353a209 100644 --- a/shared/src/i18n/he/assistantCollection.ts +++ b/shared/src/i18n/he/assistantCollection.ts @@ -34,6 +34,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'העברה ל-{name}', 'assistant.card.collection.removed': 'הוסר מהרשימה', 'assistant.card.collection.photo': 'תמונה', + 'assistant.card.collection.mapEntry': 'רשומה במפה', 'assistant.card.collection.places': 'מקומות שמורים ב-{name}', 'assistant.card.collection.saved': 'מקומות שמורים', 'assistant.card.collection.statusSet': '{count} מקומות שמורים עודכנו', diff --git a/shared/src/i18n/hu/assistantCollection.ts b/shared/src/i18n/hu/assistantCollection.ts index 62ba4becb..e57945c37 100644 --- a/shared/src/i18n/hu/assistantCollection.ts +++ b/shared/src/i18n/hu/assistantCollection.ts @@ -27,6 +27,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Áthelyezés ide: {name}', 'assistant.card.collection.removed': 'Eltávolítva a listából', 'assistant.card.collection.photo': 'Fotó', + 'assistant.card.collection.mapEntry': 'Térképes bejegyzés', 'assistant.card.collection.places': 'Mentett helyek itt: {name}', 'assistant.card.collection.saved': 'Mentett helyek', 'assistant.card.collection.statusSet': '{count} mentett hely módosítva', diff --git a/shared/src/i18n/id/assistantCollection.ts b/shared/src/i18n/id/assistantCollection.ts index 16ad7c7c5..a8ce5ff84 100644 --- a/shared/src/i18n/id/assistantCollection.ts +++ b/shared/src/i18n/id/assistantCollection.ts @@ -20,6 +20,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Pindahkan ke {name}', 'assistant.card.collection.removed': 'Dihapus dari daftar', 'assistant.card.collection.photo': 'Foto', + 'assistant.card.collection.mapEntry': 'Entri peta', 'assistant.card.collection.places': 'Tempat tersimpan di {name}', 'assistant.card.collection.saved': 'Tempat tersimpan', 'assistant.card.collection.statusSet': '{count} tempat tersimpan diubah', diff --git a/shared/src/i18n/it/assistantCollection.ts b/shared/src/i18n/it/assistantCollection.ts index 96edc8618..c4654ba5d 100644 --- a/shared/src/i18n/it/assistantCollection.ts +++ b/shared/src/i18n/it/assistantCollection.ts @@ -27,6 +27,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Sposta in {name}', 'assistant.card.collection.removed': 'Rimosso dalla lista', 'assistant.card.collection.photo': 'Foto', + 'assistant.card.collection.mapEntry': 'Scheda sulla mappa', 'assistant.card.collection.places': 'Luoghi salvati in {name}', 'assistant.card.collection.saved': 'Luoghi salvati', 'assistant.card.collection.statusSet': '{count} luoghi salvati modificati', diff --git a/shared/src/i18n/ja/assistantCollection.ts b/shared/src/i18n/ja/assistantCollection.ts index 1f748ca5a..879ae228f 100644 --- a/shared/src/i18n/ja/assistantCollection.ts +++ b/shared/src/i18n/ja/assistantCollection.ts @@ -20,6 +20,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': '{name} に移動', 'assistant.card.collection.removed': 'リストから削除しました', 'assistant.card.collection.photo': '写真', + 'assistant.card.collection.mapEntry': '地図の項目', 'assistant.card.collection.places': '{name} の保存した場所', 'assistant.card.collection.saved': '保存した場所', 'assistant.card.collection.statusSet': '保存した場所 {count} 件を変更', diff --git a/shared/src/i18n/ko/assistantCollection.ts b/shared/src/i18n/ko/assistantCollection.ts index 335515c00..1362424f5 100644 --- a/shared/src/i18n/ko/assistantCollection.ts +++ b/shared/src/i18n/ko/assistantCollection.ts @@ -20,6 +20,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': '{name}(으)로 이동', 'assistant.card.collection.removed': '목록에서 제거됨', 'assistant.card.collection.photo': '사진', + 'assistant.card.collection.mapEntry': '지도 항목', 'assistant.card.collection.places': '{name}에 저장한 장소', 'assistant.card.collection.saved': '저장한 장소', 'assistant.card.collection.statusSet': '저장한 장소 {count}곳 변경됨', diff --git a/shared/src/i18n/nl/assistantCollection.ts b/shared/src/i18n/nl/assistantCollection.ts index b7147bf92..77ca357ba 100644 --- a/shared/src/i18n/nl/assistantCollection.ts +++ b/shared/src/i18n/nl/assistantCollection.ts @@ -27,6 +27,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Verplaatsen naar {name}', 'assistant.card.collection.removed': 'Uit de lijst verwijderd', 'assistant.card.collection.photo': 'Foto', + 'assistant.card.collection.mapEntry': 'Kaartvermelding', 'assistant.card.collection.places': 'Bewaarde plaatsen in {name}', 'assistant.card.collection.saved': 'Bewaarde plaatsen', 'assistant.card.collection.statusSet': '{count} bewaarde plaatsen gewijzigd', diff --git a/shared/src/i18n/pl/assistantCollection.ts b/shared/src/i18n/pl/assistantCollection.ts index 61becec1c..414db97a0 100644 --- a/shared/src/i18n/pl/assistantCollection.ts +++ b/shared/src/i18n/pl/assistantCollection.ts @@ -41,6 +41,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Przenieś do {name}', 'assistant.card.collection.removed': 'Usunięto z listy', 'assistant.card.collection.photo': 'Zdjęcie', + 'assistant.card.collection.mapEntry': 'Wpis na mapie', 'assistant.card.collection.places': 'Zapisane miejsca na liście {name}', 'assistant.card.collection.saved': 'Zapisane miejsca', 'assistant.card.collection.statusSet': 'Zmieniono {count} zapisanego miejsca', diff --git a/shared/src/i18n/ru/assistantCollection.ts b/shared/src/i18n/ru/assistantCollection.ts index cbec12a6e..191fe7b71 100644 --- a/shared/src/i18n/ru/assistantCollection.ts +++ b/shared/src/i18n/ru/assistantCollection.ts @@ -41,6 +41,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Переместить в «{name}»', 'assistant.card.collection.removed': 'Удалено из списка', 'assistant.card.collection.photo': 'Фото', + 'assistant.card.collection.mapEntry': 'Запись на карте', 'assistant.card.collection.places': 'Сохранённые места в «{name}»', 'assistant.card.collection.saved': 'Сохранённые места', 'assistant.card.collection.statusSet': 'Изменено сохранённых мест: {count}', diff --git a/shared/src/i18n/sk/assistantCollection.ts b/shared/src/i18n/sk/assistantCollection.ts index 3204a1444..63d04822f 100644 --- a/shared/src/i18n/sk/assistantCollection.ts +++ b/shared/src/i18n/sk/assistantCollection.ts @@ -34,6 +34,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Presunúť do {name}', 'assistant.card.collection.removed': 'Odstránené zo zoznamu', 'assistant.card.collection.photo': 'Fotka', + 'assistant.card.collection.mapEntry': 'Záznam na mape', 'assistant.card.collection.places': 'Uložené miesta v {name}', 'assistant.card.collection.saved': 'Uložené miesta', 'assistant.card.collection.statusSet': '{count} uložených miest zmenených', diff --git a/shared/src/i18n/sv/assistantCollection.ts b/shared/src/i18n/sv/assistantCollection.ts index 08bb3f0d6..6728a8966 100644 --- a/shared/src/i18n/sv/assistantCollection.ts +++ b/shared/src/i18n/sv/assistantCollection.ts @@ -27,6 +27,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Flytta till {name}', 'assistant.card.collection.removed': 'Borttagen från listan', 'assistant.card.collection.photo': 'Foto', + 'assistant.card.collection.mapEntry': 'Kartpost', 'assistant.card.collection.places': 'Sparade platser i {name}', 'assistant.card.collection.saved': 'Sparade platser', 'assistant.card.collection.statusSet': '{count} sparade platser ändrade', diff --git a/shared/src/i18n/th/assistantCollection.ts b/shared/src/i18n/th/assistantCollection.ts index 537940ba5..c308deda8 100644 --- a/shared/src/i18n/th/assistantCollection.ts +++ b/shared/src/i18n/th/assistantCollection.ts @@ -20,6 +20,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'ย้ายไปที่ {name}', 'assistant.card.collection.removed': 'นำออกจากรายการแล้ว', 'assistant.card.collection.photo': 'รูปภาพ', + 'assistant.card.collection.mapEntry': 'รายการบนแผนที่', 'assistant.card.collection.places': 'สถานที่ที่บันทึกไว้ใน {name}', 'assistant.card.collection.saved': 'สถานที่ที่บันทึกไว้', 'assistant.card.collection.statusSet': 'เปลี่ยนสถานที่ที่บันทึกไว้ {count} แห่ง', diff --git a/shared/src/i18n/tr/assistantCollection.ts b/shared/src/i18n/tr/assistantCollection.ts index ae0ef519f..84f0f5cb7 100644 --- a/shared/src/i18n/tr/assistantCollection.ts +++ b/shared/src/i18n/tr/assistantCollection.ts @@ -27,6 +27,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': '{name} listesine taşı', 'assistant.card.collection.removed': 'Listeden çıkarıldı', 'assistant.card.collection.photo': 'Fotoğraf', + 'assistant.card.collection.mapEntry': 'Harita kaydı', 'assistant.card.collection.places': '{name} listesindeki kayıtlı yerler', 'assistant.card.collection.saved': 'Kayıtlı yerler', 'assistant.card.collection.statusSet': '{count} kayıtlı yer değişti', diff --git a/shared/src/i18n/uk/assistantCollection.ts b/shared/src/i18n/uk/assistantCollection.ts index 1dcf28c2b..3c402d7dc 100644 --- a/shared/src/i18n/uk/assistantCollection.ts +++ b/shared/src/i18n/uk/assistantCollection.ts @@ -41,6 +41,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Перемістити в «{name}»', 'assistant.card.collection.removed': 'Прибрано зі списку', 'assistant.card.collection.photo': 'Фото', + 'assistant.card.collection.mapEntry': 'Запис на мапі', 'assistant.card.collection.places': 'Збережені місця в «{name}»', 'assistant.card.collection.saved': 'Збережені місця', 'assistant.card.collection.statusSet': 'Змінено збережених місць: {count}', diff --git a/shared/src/i18n/vi/assistantCollection.ts b/shared/src/i18n/vi/assistantCollection.ts index f43adfb91..94f96451d 100644 --- a/shared/src/i18n/vi/assistantCollection.ts +++ b/shared/src/i18n/vi/assistantCollection.ts @@ -20,6 +20,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': 'Chuyển sang {name}', 'assistant.card.collection.removed': 'Đã xoá khỏi danh sách', 'assistant.card.collection.photo': 'Ảnh', + 'assistant.card.collection.mapEntry': 'Mục trên bản đồ', 'assistant.card.collection.places': 'Địa điểm đã lưu trong {name}', 'assistant.card.collection.saved': 'Địa điểm đã lưu', 'assistant.card.collection.statusSet': 'Đã thay đổi {count} địa điểm đã lưu', diff --git a/shared/src/i18n/zh-TW/assistantCollection.ts b/shared/src/i18n/zh-TW/assistantCollection.ts index 36b8aa7c1..11684450d 100644 --- a/shared/src/i18n/zh-TW/assistantCollection.ts +++ b/shared/src/i18n/zh-TW/assistantCollection.ts @@ -20,6 +20,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': '移動到 {name}', 'assistant.card.collection.removed': '已從清單中移除', 'assistant.card.collection.photo': '照片', + 'assistant.card.collection.mapEntry': '地圖項目', 'assistant.card.collection.places': '{name} 中儲存的地點', 'assistant.card.collection.saved': '儲存的地點', 'assistant.card.collection.statusSet': '已變更 {count} 個儲存的地點', diff --git a/shared/src/i18n/zh/assistantCollection.ts b/shared/src/i18n/zh/assistantCollection.ts index 336c5af2f..2c82fbf6b 100644 --- a/shared/src/i18n/zh/assistantCollection.ts +++ b/shared/src/i18n/zh/assistantCollection.ts @@ -20,6 +20,7 @@ const assistantCollection: TranslationStrings = { 'assistant.card.collection.moveTo': '移动到 {name}', 'assistant.card.collection.removed': '已从列表中移除', 'assistant.card.collection.photo': '照片', + 'assistant.card.collection.mapEntry': '地图条目', 'assistant.card.collection.places': '{name} 中保存的地点', 'assistant.card.collection.saved': '保存的地点', 'assistant.card.collection.statusSet': '已更改 {count} 个保存的地点', diff --git a/wiki/MCP-Addon-Tools.md b/wiki/MCP-Addon-Tools.md index 25456b784..01b30787c 100644 --- a/wiki/MCP-Addon-Tools.md +++ b/wiki/MCP-Addon-Tools.md @@ -111,7 +111,7 @@ Requires `collections:read` or `collections:write` scope. | `reorder_collections` | Reorder the user's collections — pass every collection id in the desired order. | | `save_place_to_collection` | Save a place into a collection from a raw payload. Returns a duplicate marker instead of saving when a similar place already exists, unless `force` is true. | | `save_trip_places_to_collection` | Copy one or more existing trip places into a collection. Duplicates are skipped unless `force` is true. | -| `update_collection_place` | Update a saved place's name, address, coordinates, description, notes, status, category, links, tags, labels, image, or move it to another collection (moving out of a list needs the owner or admin role there). | +| `update_collection_place` | Update a saved place's name, address, coordinates, description, notes, status, category, links, tags, labels, image, provider ids, website, phone, price and currency, or move it to another collection (moving out of a list needs the owner or admin role there). Only the fields passed change; `null` clears one. | | `set_collection_place_status` | Set a saved place's status: `idea`, `want`, `visited`, or `revisit` (been there and wants to go back; counts as visited). | | `set_collection_place_status_from_trip` | Set a status on every saved copy of the given trip places, in every list they are on, for marking places visited after a day out. Ids are trip place ids. Lists you may only read are skipped. Returns how many saved places changed and how many of the trip places were found in at least one list. | | `rate_collection_place` | Set or clear the current user's 1-5 star rating on a saved place. Every member rates independently; pass `null` to remove the vote. |