mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-13 15:51:00 +00:00
feat(collections): edit a saved place's provider ids, website, phone and price (#2646)
The update contract takes google_place_id, google_ftid, osm_id, website, phone, price and currency like saving does, website with the same normalisation and refusal. REST and update_collection_place share it, so an agent can correct a place instead of re-saving it and losing its labels, ratings, position and id. The assistant's change card names the new fields.
This commit is contained in:
@@ -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] });
|
||||
|
||||
@@ -45,6 +45,10 @@ const FIELD_LOOK: Record<CollectionCardDelta['fields'][number], { labelKey: stri
|
||||
category: { labelKey: 'assistant.change.label.category' },
|
||||
link: { labelKey: 'assistant.change.label.link' },
|
||||
photo: { labelKey: 'assistant.card.collection.photo' },
|
||||
website: { labelKey: 'assistant.change.label.website' },
|
||||
phone: { labelKey: 'assistant.change.label.phone' },
|
||||
price: { labelKey: 'assistant.change.label.price' },
|
||||
mapEntry: { labelKey: 'assistant.card.collection.mapEntry' },
|
||||
};
|
||||
|
||||
/** The verb's tile, as the row shows it without a place. */
|
||||
|
||||
@@ -171,6 +171,13 @@ export class CollectionPlacesRepository extends TrekRepository<CollectionPlaces>
|
||||
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;
|
||||
}>,
|
||||
|
||||
@@ -77,6 +77,13 @@ const FIELD_OF: Readonly<Record<string, (typeof COLLECTION_CARD_FIELDS)[number]>
|
||||
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'] {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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<CollectionPlacesRepository['updateFields']>[1] = {};
|
||||
const write: Parameters<CollectionPlacesRepository['updateFields']>[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;
|
||||
|
||||
@@ -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<string, unknown>) =>
|
||||
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 () => {
|
||||
|
||||
@@ -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<string, unknown>) =>
|
||||
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<string, unknown> };
|
||||
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<string, unknown>;
|
||||
};
|
||||
expect(cleared.place).toMatchObject({
|
||||
website: null,
|
||||
phone: null,
|
||||
price: null,
|
||||
currency: 'EUR',
|
||||
google_place_id: 'ChIJseptime',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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' });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(),
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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<typeof collectionPlaceUpdateRequestSchema>;
|
||||
|
||||
|
||||
@@ -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} مكان محفوظ',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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é',
|
||||
|
||||
@@ -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} αποθηκευμένα μέρη άλλαξαν',
|
||||
|
||||
@@ -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} מקומות שמורים עודכנו',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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} 件を変更',
|
||||
|
||||
@@ -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}곳 변경됨',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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} แห่ง',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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} 個儲存的地點',
|
||||
|
||||
@@ -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} 个保存的地点',
|
||||
|
||||
@@ -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. |
|
||||
|
||||
Reference in New Issue
Block a user