feat(collections): bulk-add selected trip places to a list (#1081)

Add a "Save to collection" action to the trip place list's selection bar (next to
bulk category + delete): it opens a list picker and copies every selected place
into the chosen list in one request, instead of one-by-one from each place.

- Server: saveFromTripPlaces (one access check + one WS notify), POST
  places/from-trip-many; dedups by name/coords, skips missing ids, honours force.
- Client: saveFromTripMany api + SaveTripPlacesToListModal; the selection-bar
  button is gated on the collections addon being enabled.
- Copy count / skipped-duplicates toast; strings in all 22 locales.
- Service + controller tests for the bulk path.
This commit is contained in:
Maurice
2026-07-01 17:22:50 +02:00
committed by Maurice
parent 1e0feb93dd
commit 6cdcc82d9c
31 changed files with 305 additions and 2 deletions
@@ -138,6 +138,15 @@ export const collectionSaveFromTripRequestSchema = z.object({
});
export type CollectionSaveFromTripRequest = z.infer<typeof collectionSaveFromTripRequestSchema>;
/** Bulk: copy several selected trip places into a list at once. */
export const collectionSaveFromTripManyRequestSchema = z.object({
collection_id: z.number(),
source_trip_id: z.number(),
source_place_ids: z.array(z.number()).min(1).max(1000),
force: z.boolean().optional(),
});
export type CollectionSaveFromTripManyRequest = z.infer<typeof collectionSaveFromTripManyRequestSchema>;
export const collectionPlaceUpdateRequestSchema = z.object({
name: z.string().min(1).optional(),
description: z.string().nullable().optional(),
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': 'حفظ {count} في قائمة',
'collections.addedNToList': 'تم حفظ {count} في {name}',
'collections.noOwnLists': 'ليس لديك قوائم بعد',
'collections.saveToListHint': 'يتم تخطي التكرارات تلقائيًا',
'collections.role.label': 'الدور',
'collections.role.viewer': 'مشاهد',
'collections.role.editor': 'محرر',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': 'Salvar {count} em uma lista',
'collections.addedNToList': '{count} salvos em {name}',
'collections.noOwnLists': 'Você ainda não tem listas',
'collections.saveToListHint': 'Duplicados são ignorados automaticamente',
'collections.role.label': 'Função',
'collections.role.viewer': 'Visualizador',
'collections.role.editor': 'Editor',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': 'Uložit {count} do seznamu',
'collections.addedNToList': 'Uloženo {count} do {name}',
'collections.noOwnLists': 'Zatím nemáte žádné seznamy',
'collections.saveToListHint': 'Duplikáty se automaticky přeskočí',
'collections.role.label': 'Role',
'collections.role.viewer': 'Divák',
'collections.role.editor': 'Editor',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': '{count} in eine Liste speichern',
'collections.addedNToList': '{count} in {name} gespeichert',
'collections.noOwnLists': 'Du hast noch keine Listen',
'collections.saveToListHint': 'Duplikate werden automatisch übersprungen',
'collections.role.label': 'Rolle',
'collections.role.viewer': 'Betrachter',
'collections.role.editor': 'Bearbeiter',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': 'Save {count} to a list',
'collections.addedNToList': 'Saved {count} to {name}',
'collections.noOwnLists': 'You have no lists yet',
'collections.saveToListHint': 'Duplicates are skipped automatically',
'collections.role.label': 'Role',
'collections.role.viewer': 'Viewer',
'collections.role.editor': 'Editor',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': 'Guardar {count} en una lista',
'collections.addedNToList': '{count} guardados en {name}',
'collections.noOwnLists': 'Aún no tienes listas',
'collections.saveToListHint': 'Los duplicados se omiten automáticamente',
'collections.role.label': 'Rol',
'collections.role.viewer': 'Lector',
'collections.role.editor': 'Editor',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': 'Enregistrer {count} dans une liste',
'collections.addedNToList': '{count} enregistrés dans {name}',
'collections.noOwnLists': 'Vous n\'avez pas encore de listes',
'collections.saveToListHint': 'Les doublons sont ignorés automatiquement',
'collections.role.label': 'Rôle',
'collections.role.viewer': 'Lecteur',
'collections.role.editor': 'Éditeur',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': 'Αποθήκευση {count} σε λίστα',
'collections.addedNToList': 'Αποθηκεύτηκαν {count} στο {name}',
'collections.noOwnLists': 'Δεν έχετε λίστες ακόμη',
'collections.saveToListHint': 'Τα διπλότυπα παραλείπονται αυτόματα',
'collections.role.label': 'Ρόλος',
'collections.role.viewer': 'Θεατής',
'collections.role.editor': 'Συντάκτης',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': '{count} mentése listába',
'collections.addedNToList': '{count} mentve ide: {name}',
'collections.noOwnLists': 'Még nincsenek listáid',
'collections.saveToListHint': 'A duplikátumok automatikusan kimaradnak',
'collections.role.label': 'Szerep',
'collections.role.viewer': 'Néző',
'collections.role.editor': 'Szerkesztő',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': 'Simpan {count} ke daftar',
'collections.addedNToList': '{count} disimpan ke {name}',
'collections.noOwnLists': 'Kamu belum punya daftar',
'collections.saveToListHint': 'Duplikat dilewati otomatis',
'collections.role.label': 'Peran',
'collections.role.viewer': 'Peninjau',
'collections.role.editor': 'Editor',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': 'Salva {count} in una lista',
'collections.addedNToList': '{count} salvati in {name}',
'collections.noOwnLists': 'Non hai ancora liste',
'collections.saveToListHint': 'I duplicati vengono ignorati automaticamente',
'collections.role.label': 'Ruolo',
'collections.role.viewer': 'Visualizzatore',
'collections.role.editor': 'Editore',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': '{count} 件をリストに保存',
'collections.addedNToList': '{count} 件を {name} に保存しました',
'collections.noOwnLists': 'リストがまだありません',
'collections.saveToListHint': '重複は自動的にスキップされます',
'collections.role.label': '役割',
'collections.role.viewer': '閲覧者',
'collections.role.editor': '編集者',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': '{count}개를 목록에 저장',
'collections.addedNToList': '{count}개를 {name}에 저장함',
'collections.noOwnLists': '아직 목록이 없습니다',
'collections.saveToListHint': '중복 항목은 자동으로 건너뜁니다',
'collections.role.label': '역할',
'collections.role.viewer': '뷰어',
'collections.role.editor': '편집자',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': '{count} in een lijst opslaan',
'collections.addedNToList': '{count} opgeslagen in {name}',
'collections.noOwnLists': 'Je hebt nog geen lijsten',
'collections.saveToListHint': 'Duplicaten worden automatisch overgeslagen',
'collections.role.label': 'Rol',
'collections.role.viewer': 'Kijker',
'collections.role.editor': 'Bewerker',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': 'Zapisz {count} na liście',
'collections.addedNToList': 'Zapisano {count} w {name}',
'collections.noOwnLists': 'Nie masz jeszcze list',
'collections.saveToListHint': 'Duplikaty są pomijane automatycznie',
'collections.role.label': 'Rola',
'collections.role.viewer': 'Widz',
'collections.role.editor': 'Edytor',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': 'Сохранить {count} в список',
'collections.addedNToList': 'Сохранено {count} в {name}',
'collections.noOwnLists': 'У вас пока нет списков',
'collections.saveToListHint': 'Дубликаты пропускаются автоматически',
'collections.role.label': 'Роль',
'collections.role.viewer': 'Наблюдатель',
'collections.role.editor': 'Редактор',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': 'Spara {count} i en lista',
'collections.addedNToList': 'Sparade {count} i {name}',
'collections.noOwnLists': 'Du har inga listor än',
'collections.saveToListHint': 'Dubbletter hoppas över automatiskt',
'collections.role.label': 'Roll',
'collections.role.viewer': 'Läsare',
'collections.role.editor': 'Redigerare',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': '{count} öğeyi bir listeye kaydet',
'collections.addedNToList': '{count} öğe {name} listesine kaydedildi',
'collections.noOwnLists': 'Henüz listeniz yok',
'collections.saveToListHint': 'Yinelenenler otomatik olarak atlanır',
'collections.role.label': 'Rol',
'collections.role.viewer': 'Görüntüleyici',
'collections.role.editor': 'Editör',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': 'Зберегти {count} до списку',
'collections.addedNToList': 'Збережено {count} до {name}',
'collections.noOwnLists': 'У вас ще немає списків',
'collections.saveToListHint': 'Дублікати пропускаються автоматично',
'collections.role.label': 'Роль',
'collections.role.viewer': 'Глядач',
'collections.role.editor': 'Редактор',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': 'Lưu {count} vào danh sách',
'collections.addedNToList': 'Đã lưu {count} vào {name}',
'collections.noOwnLists': 'Bạn chưa có danh sách nào',
'collections.saveToListHint': 'Các mục trùng lặp được bỏ qua tự động',
'collections.role.label': 'Vai trò',
'collections.role.viewer': 'Người xem',
'collections.role.editor': 'Người sửa',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': '將 {count} 個儲存到清單',
'collections.addedNToList': '已將 {count} 個儲存到 {name}',
'collections.noOwnLists': '你還沒有清單',
'collections.saveToListHint': '重複項目會被自動略過',
'collections.role.label': '角色',
'collections.role.viewer': '檢視者',
'collections.role.editor': '編輯者',
+4
View File
@@ -1,6 +1,10 @@
import type { TranslationStrings } from '../types';
const collection: TranslationStrings = {
'collections.saveNToList': '将 {count} 个保存到列表',
'collections.addedNToList': '已将 {count} 个保存到 {name}',
'collections.noOwnLists': '你还没有列表',
'collections.saveToListHint': '重复项会被自动跳过',
'collections.role.label': '角色',
'collections.role.viewer': '查看者',
'collections.role.editor': '编辑者',