feat(packing): drag a category by its head to move it among the others

Categories showed in the order they were created, which a list kept per day
cannot undo once the days swap. A grip in each category's head band moves
the whole category onto another's place on desktop. A category has no
position of its own, it shows where its first item is, so the move reorders
the items as one block through the item reorder the list already has: no
new column, and the order reaches everyone the way a dragged item does. The
other view's categories keep their order.
This commit is contained in:
Maurice
2026-10-18 13:06:32 +02:00
parent f7f6b83d33
commit 23b28a1f3f
32 changed files with 167 additions and 7 deletions
@@ -4,7 +4,7 @@ import { useToast } from '../shared/Toast'
import { useTranslation } from '../../i18n'
import {
Trash2, Plus, ChevronDown, ChevronRight,
X, Pencil, Check, MoreHorizontal, CheckCheck, RotateCcw, UserPlus,
X, Pencil, Check, MoreHorizontal, CheckCheck, RotateCcw, UserPlus, GripVertical,
} from 'lucide-react'
import type { PackingItem, PackingBag } from '../../types'
import { katColor } from './packingListPanel.helpers'
@@ -38,6 +38,18 @@ interface KategorieGruppeProps {
// Off while the list is shown A-Z: a drop there would move an item in an order
// the reader cannot see.
reorderable?: boolean
// Moving the whole category among the others, wired by the list, which sees every
// card. `active` is true while any category is being dragged, so a dragged ITEM
// passing over a card is left to the item rows.
categoryDrag?: {
active: boolean
isDragging: boolean
isOver: boolean
onStart: () => void
onOver: () => void
onEnd: () => void
onDrop: () => void
}
// Three-tier sharing (#858) — threaded down to each item's share control.
currentUserId?: number
onSetSharing?: (id: number, visibility: 'common' | 'personal' | 'shared', recipientIds: number[]) => void
@@ -46,7 +58,7 @@ interface KategorieGruppeProps {
onLeave?: (id: number, userId: number) => void
}
export function KategorieGruppe({ kategorie, items, tripId, allCategories, onRename, onDeleteAll, onDeleteItem, onAddItem, assignees, tripMembers, onSetAssignees, bagTrackingEnabled, bags, onCreateBag, canEdit = true, allItems, onReorder, reorderable = true, currentUserId, onSetSharing, onClone, onJoin, onLeave }: KategorieGruppeProps) {
export function KategorieGruppe({ kategorie, items, tripId, allCategories, onRename, onDeleteAll, onDeleteItem, onAddItem, assignees, tripMembers, onSetAssignees, bagTrackingEnabled, bags, onCreateBag, canEdit = true, allItems, onReorder, reorderable = true, categoryDrag, currentUserId, onSetSharing, onClone, onJoin, onLeave }: KategorieGruppeProps) {
const [offen, setOffen] = useState(true)
const [dragId, setDragId] = useState<number | null>(null)
const [overId, setOverId] = useState<number | null>(null)
@@ -120,8 +132,9 @@ export function KategorieGruppe({ kategorie, items, tripId, allCategories, onRen
return (
// Shaped after the phone's category card: a tinted head band carrying the
// name as written, the count, and the fold arrow on the far right.
<div style={{ marginBottom: 6, background: 'var(--bg-card)', borderRadius: 16, border: '1px solid var(--border-secondary)', overflow: 'visible' }}>
<CategoryDropFrame drag={categoryDrag}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, paddingBlock: 11, paddingInlineEnd: 12, paddingInlineStart: 14, background: 'var(--bg-tertiary)', borderRadius: offen ? '15px 15px 0 0' : 15 }}>
<CategoryGrip drag={categoryDrag} kategorie={kategorie} label={t('packing.dragCategory')} />
<span style={{ width: 10, height: 10, borderRadius: '50%', background: dot, flexShrink: 0 }} />
{editingName && canEdit ? (
@@ -344,6 +357,48 @@ export function KategorieGruppe({ kategorie, items, tripId, allCategories, onRen
))}
</div>
)}
</CategoryDropFrame>
)
}
type CategoryDrag = NonNullable<KategorieGruppeProps['categoryDrag']>
/**
* The card, and while a category is dragged its drop target. Captured, because
* the item rows stop their own drops from bubbling.
*/
function CategoryDropFrame({ drag, children }: { drag?: CategoryDrag; children: React.ReactNode }) {
const target = drag?.active ? drag : null
return (
<div
onDragOverCapture={target ? (e => { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; target.onOver() }) : undefined}
onDropCapture={target ? (e => { e.preventDefault(); e.stopPropagation(); target.onDrop() }) : undefined}
style={{
marginBottom: 6, background: 'var(--bg-card)', borderRadius: 16, border: '1px solid var(--border-secondary)', overflow: 'visible',
opacity: drag?.isDragging ? 0.4 : 1,
boxShadow: drag?.isOver ? '0 0 0 2px var(--accent)' : 'none',
transition: 'opacity 0.15s, box-shadow 0.15s',
}}
>
{children}
</div>
)
}
/** The handle a category is dragged by, in the head band before its colour dot. */
function CategoryGrip({ drag, kategorie, label }: { drag?: CategoryDrag; kategorie: string; label: string }) {
if (!drag) return null
return (
<span
draggable
role="button"
aria-label={label}
title={label}
onDragStart={e => { e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', kategorie); drag.onStart() }}
onDragEnd={drag.onEnd}
style={{ cursor: 'grab', display: 'flex', alignItems: 'center', color: 'var(--text-faint)', flexShrink: 0, marginInlineStart: -6 }}
>
<GripVertical size={14} />
</span>
)
}
@@ -1,3 +1,4 @@
import { useState } from 'react'
import type { PackingState } from './usePackingListPanel'
import { KategorieGruppe } from './PackingListPanelCategoryGroup'
import EmptyState from '../shared/EmptyState'
@@ -7,8 +8,13 @@ export function PackingList(S: PackingState) {
items, gruppiert, t, tripId, allCategories, handleRenameCategory, handleDeleteCategory, handleDeleteItem,
handleAddItemToCategory, categoryAssignees, tripMembers, handleSetAssignees,
bagTrackingEnabled, bags, handleCreateBagByName, canEdit, reorderPackingItems,
currentUserId, handleSetSharing, handleCloneItem, handleJoinItem, handleLeaveItem, sort,
currentUserId, handleSetSharing, handleCloneItem, handleJoinItem, handleLeaveItem, sort, moveCategory,
} = S
// Which category is being dragged and which card it is over. Held here, above
// the cards, because the drag starts in one card and ends in another.
const [draggedCategory, setDraggedCategory] = useState<string | null>(null)
const [overCategory, setOverCategory] = useState<string | null>(null)
const endCategoryDrag = () => { setDraggedCategory(null); setOverCategory(null) }
return (
<div style={{ flex: 1, overflowY: 'auto', padding: '10px 0 16px' }}>
{items.length === 0 ? (
@@ -40,6 +46,18 @@ export function PackingList(S: PackingState) {
allItems={items}
onReorder={(orderedIds) => reorderPackingItems(tripId, orderedIds)}
reorderable={sort === 'manual'}
categoryDrag={canEdit ? {
active: draggedCategory !== null,
isDragging: draggedCategory === kat,
isOver: overCategory === kat && draggedCategory !== null && draggedCategory !== kat,
onStart: () => { setDraggedCategory(kat); setOverCategory(null) },
onOver: () => setOverCategory(kat),
onEnd: endCategoryDrag,
onDrop: () => {
if (draggedCategory !== null) moveCategory(draggedCategory, kat)
endCategoryDrag()
},
} : undefined}
currentUserId={currentUserId}
onSetSharing={handleSetSharing}
onClone={handleCloneItem}
@@ -6,6 +6,7 @@ import {
groupPackingItems,
isLastCustomItemInCategory,
isPackingPlaceholder,
itemOrderWithCategoryMoved,
packingCategoryOrder,
packingItemWeight,
packingProgress,
@@ -14,7 +15,7 @@ import {
} from './packingListModel';
import { PACKING_PLACEHOLDER_NAME } from './packingListPanel.constants';
// FE-PACK-MODEL-001 to FE-PACK-MODEL-008
// FE-PACK-MODEL-001 to FE-PACK-MODEL-009
const shared = buildPackingItem({ id: 1, name: 'Tent', category: 'Camp', checked: 1 });
const mine = buildPackingItem({ id: 2, name: 'Socks', category: 'Clothes', is_private: 1 });
@@ -76,4 +77,28 @@ describe('packingListModel', () => {
expect(isPackingPlaceholder({ name: PACKING_PLACEHOLDER_NAME })).toBe(true);
expect(isPackingPlaceholder({ name: 'Tent' })).toBe(false);
});
it('FE-PACK-MODEL-009: moving a category moves its items as a block, the other view stays as it was', () => {
const day1 = buildPackingItem({ id: 11, name: 'Shirt', category: 'Day 1' });
const day2a = buildPackingItem({ id: 12, name: 'Shorts', category: 'Day 2' });
const own = buildPackingItem({ id: 13, name: 'Pills', category: 'Day 2', is_private: 1 });
const day3 = buildPackingItem({ id: 14, name: 'Jacket', category: 'Day 3' });
const day2b = buildPackingItem({ id: 15, name: 'Hat', category: 'Day 2' });
const items = [day1, day2a, own, day3, day2b];
const common = packingViewItems(items, 'common');
const order = itemOrderWithCategoryMoved(items, common, 'Day 3', 'Day 1', 'Other');
expect(order).toEqual([14, 11, 12, 15, 13]);
const moved = (order ?? []).flatMap((id) => items.filter((i) => i.id === id));
expect(packingCategoryOrder(packingViewItems(moved, 'common'), 'Other')).toEqual(['Day 3', 'Day 1', 'Day 2']);
expect(packingCategoryOrder(packingViewItems(moved, 'personal'), 'Other')).toEqual(['Day 2']);
expect(itemOrderWithCategoryMoved(items, common, 'Day 1', 'Day 3', 'Other')).toEqual([12, 15, 14, 11, 13]);
});
it('FE-PACK-MODEL-009b: nothing moves for the same category or one the view does not show', () => {
const items = [shared, stove, loose];
expect(itemOrderWithCategoryMoved(items, items, 'Camp', 'Camp', 'Other')).toBeNull();
expect(itemOrderWithCategoryMoved(items, items, 'Clothes', 'Camp', 'Other')).toBeNull();
expect(itemOrderWithCategoryMoved(items, items, 'Other', 'Camp', 'Other')).toEqual([4, 1, 3]);
});
});
@@ -25,6 +25,34 @@ export function packingCategoryOrder(viewItems: PackingItem[], defaultCategory:
return seen;
}
/**
* The whole list's item order with category `from` moved to where `to` stands.
*
* A category has no position of its own: it shows where its first item is. So a
* category is moved by moving its items, as one block, through the item reorder
* the list already has; nothing new is stored, and the order reaches every device
* the way a dragged item does. The view's items come first, grouped in the new
* order with each group in its own order; the other view's items follow in theirs,
* which leaves that view's categories as they were. Null when nothing would move.
*/
export function itemOrderWithCategoryMoved(
items: PackingItem[],
viewItems: PackingItem[],
from: string,
to: string,
defaultCategory: string
): number[] | null {
const order = packingCategoryOrder(viewItems, defaultCategory);
const fromIndex = order.indexOf(from);
const toIndex = order.indexOf(to);
if (fromIndex < 0 || toIndex < 0 || fromIndex === toIndex) return null;
order.splice(fromIndex, 1);
order.splice(toIndex, 0, from);
const inView = new Set(viewItems.map((i) => i.id));
const grouped = order.flatMap((cat) => viewItems.filter((i) => (i.category || defaultCategory) === cat));
return [...grouped, ...items.filter((i) => !inView.has(i.id))].map((i) => i.id);
}
export function packingStatusFiltered(items: PackingItem[], status: PackingStatusFilter): PackingItem[] {
if (status === 'open') return items.filter((i) => !i.checked);
if (status === 'done') return items.filter((i) => !!i.checked);
@@ -8,7 +8,7 @@ import { tripsApi } from '../../api/client'
import { useAddonStore } from '../../store/addonStore'
import type { PackingItem } from '../../types'
import { parseImportLines, sortItemsByName } from './packingListPanel.helpers'
import { groupPackingItems, packingCategoryOrder, packingProgress, packingViewItems } from './packingListModel'
import { groupPackingItems, itemOrderWithCategoryMoved, packingCategoryOrder, packingProgress, packingViewItems } from './packingListModel'
import { usePackingBags } from './usePackingBags'
import { usePackingCategoryAssignees } from './usePackingCategoryAssignees'
import { usePackingImport } from './usePackingImport'
@@ -106,6 +106,13 @@ export function usePackingList({ tripId, items, openImportSignal = 0, addCategor
const { checked: abgehakt, pct: fortschritt } = packingProgress(viewItems)
// A category dropped onto another takes its place; the slice reorders
// optimistically and rolls back with a toast of its own.
const moveCategory = (from: string, to: string) => {
const orderedIds = itemOrderWithCategoryMoved(items, viewItems, from, to, t('packing.defaultCategory'))
if (orderedIds) void reorderPackingItems(tripId, orderedIds)
}
const {
addingCategory, setAddingCategory, newCategoryName: newCatName, setNewCategoryName: setNewCatName,
addItemToCategory: handleAddItemToCategory, deleteItem: handleDeleteItem, addNewCategory: handleAddNewCategory,
@@ -213,7 +220,7 @@ export function usePackingList({ tripId, items, openImportSignal = 0, addCategor
handleSetSharing, handleCloneItem, handleJoinItem, handleLeaveItem,
tripId, items, inlineHeader, t, canEdit, isAdmin, font, reorderPackingItems,
filter, setFilter, sort, setSort, addingCategory, setAddingCategory, newCatName, setNewCatName,
tripMembers, categoryAssignees, handleSetAssignees, allCategories, gruppiert, abgehakt, fortschritt,
tripMembers, categoryAssignees, handleSetAssignees, allCategories, gruppiert, abgehakt, fortschritt, moveCategory,
handleAddItemToCategory, handleAddNewCategory, handleRenameCategory, handleDeleteCategory, handleDeleteItem, handleClearChecked,
bagTrackingEnabled, bags, unassignedWeightGrams, serverWeightsFresh, newBagName, setNewBagName, showAddBag, setShowAddBag, showBagModal, setShowBagModal,
handleCreateBag, handleCreateBagByName, handleDeleteBag, handleUpdateBag, handleSetBagMembers,
+1
View File
@@ -111,6 +111,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat.few': 'هل أنت متأكد أنك تريد حذف القائمة "{name}" التي تحتوي على {count} عناصر؟',
'packing.confirm.deleteCat.many': 'هل أنت متأكد أنك تريد حذف القائمة "{name}" التي تحتوي على {count} عنصرًا؟',
'packing.defaultCategory': 'أخرى',
'packing.dragCategory': 'اسحب لإعادة ترتيب الفئة',
'packing.toast.saveError': 'فشل الحفظ',
'packing.toast.deleteError': 'فشل الحذف',
'packing.toast.renameError': 'فشلت إعادة التسمية',
+1
View File
@@ -117,6 +117,7 @@ const packing: TranslationStrings = {
'“{name}” siyahısını daxilindəki {count} elementlə birlikdə silmək istədiyinizə əminsiniz?',
'packing.defaultCategory': 'Digər',
'packing.dragCategory': 'Kateqoriyanı yerini dəyişmək üçün sürüşdürün',
'packing.toast.saveError': 'Yadda saxlamaq mümkün olmadı',
'packing.toast.deleteError': 'Silmək mümkün olmadı',
'packing.toast.renameError': 'Adını dəyişmək mümkün olmadı',
+1
View File
@@ -82,6 +82,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat': 'Tem certeza de que deseja excluir a lista "{name}" com {count} itens?',
'packing.confirm.deleteCat.one': 'Tem certeza de que deseja excluir a lista "{name}" com {count} item?',
'packing.defaultCategory': 'Outros',
'packing.dragCategory': 'Arraste para reordenar a categoria',
'packing.toast.saveError': 'Falha ao salvar',
'packing.toast.deleteError': 'Falha ao excluir',
'packing.toast.renameError': 'Falha ao renomear',
+1
View File
@@ -70,6 +70,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat': 'Segur que vols eliminar la categoria "{name}" amb {count} elements?',
'packing.confirm.deleteCat.one': 'Segur que vols eliminar la categoria "{name}" amb {count} element?',
'packing.defaultCategory': 'Altres',
'packing.dragCategory': 'Arrossega per reordenar la categoria',
'packing.toast.saveError': "No s'ha pogut desar",
'packing.toast.deleteError': "No s'ha pogut eliminar",
'packing.toast.renameError': "No s'ha pogut canviar el nom",
+1
View File
@@ -90,6 +90,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat.one': 'Opravdu chcete smazat seznam „{name}“ s {count} položkou?',
'packing.confirm.deleteCat.few': 'Opravdu chcete smazat seznam „{name}“ s {count} položkami?',
'packing.defaultCategory': 'Ostatní',
'packing.dragCategory': 'Přetažením změníte pořadí kategorie',
'packing.toast.saveError': 'Uložení se nezdařilo',
'packing.toast.deleteError': 'Smazání se nezdařilo',
'packing.toast.renameError': 'Přejmenování se nezdařilo',
+1
View File
@@ -102,6 +102,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat': 'Möchtest du die Liste „{name}“ mit {count} Einträgen wirklich löschen?',
'packing.confirm.deleteCat.one': 'Möchtest du die Liste „{name}“ mit {count} Eintrag wirklich löschen?',
'packing.defaultCategory': 'Sonstiges',
'packing.dragCategory': 'Ziehen, um die Kategorie umzusortieren',
'packing.toast.saveError': 'Fehler beim Speichern',
'packing.toast.deleteError': 'Fehler beim Löschen',
'packing.toast.renameError': 'Fehler beim Umbenennen',
+1
View File
@@ -101,6 +101,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat': 'Are you sure you want to delete the list "{name}" with {count} items?',
'packing.confirm.deleteCat.one': 'Are you sure you want to delete the list "{name}" with {count} item?',
'packing.defaultCategory': 'Other',
'packing.dragCategory': 'Drag to reorder the category',
'packing.toast.saveError': 'Failed to save',
'packing.toast.deleteError': 'Failed to delete',
'packing.toast.renameError': 'Failed to rename',
+1
View File
@@ -83,6 +83,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat': '¿Seguro que quieres eliminar la lista "{name}" con {count} elementos?',
'packing.confirm.deleteCat.one': '¿Seguro que quieres eliminar la lista "{name}" con {count} elemento?',
'packing.defaultCategory': 'Otros',
'packing.dragCategory': 'Arrastra para reordenar la categoría',
'packing.toast.saveError': 'No se pudo guardar',
'packing.toast.deleteError': 'No se pudo eliminar',
'packing.toast.renameError': 'No se pudo renombrar',
+1
View File
@@ -101,6 +101,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat': 'Kas soovid kindlasti kustutada nimekirja "{name}" koos {count} esemega?',
'packing.confirm.deleteCat.one': 'Kas soovid kindlasti kustutada nimekirja "{name}" koos {count} esemega?',
'packing.defaultCategory': 'Muu',
'packing.dragCategory': 'Lohista kategooria järjekorra muutmiseks',
'packing.toast.saveError': 'Salvestamine ebaõnnestus',
'packing.toast.deleteError': 'Kustutamine ebaõnnestus',
'packing.toast.renameError': 'Ümbernimetamine ebaõnnestus',
+1
View File
@@ -83,6 +83,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat': 'Voulez-vous vraiment supprimer la liste « {name} » contenant {count} éléments ?',
'packing.confirm.deleteCat.one': 'Voulez-vous vraiment supprimer la liste « {name} » contenant {count} élément ?',
'packing.defaultCategory': 'Autre',
'packing.dragCategory': 'Faites glisser pour réordonner la catégorie',
'packing.toast.saveError': "Échec de l'enregistrement",
'packing.toast.deleteError': 'Échec de la suppression',
'packing.toast.renameError': 'Échec du renommage',
+1
View File
@@ -84,6 +84,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat': 'Είστε βέβαιοι ότι θέλετε να διαγράψετε τη λίστα "{name}" με {count} στοιχεία;',
'packing.confirm.deleteCat.one': 'Είστε βέβαιοι ότι θέλετε να διαγράψετε τη λίστα "{name}" με {count} στοιχείο;',
'packing.defaultCategory': 'Άλλα',
'packing.dragCategory': 'Σύρετε για να αλλάξετε τη σειρά της κατηγορίας',
'packing.toast.saveError': 'Αποτυχία αποθήκευσης',
'packing.toast.deleteError': 'Αποτυχία διαγραφής',
'packing.toast.renameError': 'Αποτυχία μετονομασίας',
+1
View File
@@ -85,6 +85,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat.one':
'Biztosan törölni szeretnéd a(z) „{name}” listát a benne lévő {count} elemmel együtt?',
'packing.defaultCategory': 'Egyéb',
'packing.dragCategory': 'Húzd a kategória átrendezéséhez',
'packing.toast.saveError': 'Nem sikerült menteni',
'packing.toast.deleteError': 'Nem sikerült törölni',
'packing.toast.renameError': 'Nem sikerült átnevezni',
+1
View File
@@ -76,6 +76,7 @@ const packing: TranslationStrings = {
'packing.confirm.clearChecked': 'Yakin ingin menghapus {count} item yang dicentang?',
'packing.confirm.deleteCat': 'Yakin ingin menghapus daftar "{name}" dengan {count} item?',
'packing.defaultCategory': 'Lainnya',
'packing.dragCategory': 'Seret untuk mengurutkan ulang kategori',
'packing.toast.saveError': 'Gagal menyimpan',
'packing.toast.deleteError': 'Gagal menghapus',
'packing.toast.renameError': 'Gagal mengganti nama',
+1
View File
@@ -83,6 +83,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat': 'Vuoi davvero eliminare la lista "{name}" con {count} elementi?',
'packing.confirm.deleteCat.one': 'Vuoi davvero eliminare la lista "{name}" con {count} elemento?',
'packing.defaultCategory': 'Altro',
'packing.dragCategory': 'Trascina per riordinare la categoria',
'packing.toast.saveError': 'Impossibile salvare',
'packing.toast.deleteError': 'Impossibile eliminare',
'packing.toast.renameError': 'Impossibile rinominare',
+1
View File
@@ -75,6 +75,7 @@ const packing: TranslationStrings = {
'packing.confirm.clearChecked': 'チェック済み{count}件を削除しますか?',
'packing.confirm.deleteCat': 'リスト「{name}」({count}件)を削除してもよろしいですか?',
'packing.defaultCategory': 'その他',
'packing.dragCategory': 'ドラッグしてカテゴリを並べ替え',
'packing.toast.saveError': '保存に失敗しました',
'packing.toast.deleteError': '削除に失敗しました',
'packing.toast.renameError': '名前の変更に失敗しました',
+1
View File
@@ -75,6 +75,7 @@ const packing: TranslationStrings = {
'packing.confirm.clearChecked': '체크된 {count}개 항목을 제거할까요?',
'packing.confirm.deleteCat': '"{name}" 목록({count}개 항목)을 삭제하시겠습니까?',
'packing.defaultCategory': '기타',
'packing.dragCategory': '드래그하여 카테고리 순서 변경',
'packing.toast.saveError': '저장 실패',
'packing.toast.deleteError': '삭제 실패',
'packing.toast.renameError': '이름 변경 실패',
+1
View File
@@ -83,6 +83,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat': 'Weet je zeker dat je de lijst "{name}" met {count} items wilt verwijderen?',
'packing.confirm.deleteCat.one': 'Weet je zeker dat je de lijst "{name}" met {count} item wilt verwijderen?',
'packing.defaultCategory': 'Overig',
'packing.dragCategory': 'Sleep om de categorie te verplaatsen',
'packing.toast.saveError': 'Opslaan mislukt',
'packing.toast.deleteError': 'Verwijderen mislukt',
'packing.toast.renameError': 'Hernoemen mislukt',
+1
View File
@@ -97,6 +97,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat.few': 'Czy na pewno chcesz usunąć listę „{name}” z {count} przedmiotami?',
'packing.confirm.deleteCat.many': 'Czy na pewno chcesz usunąć listę „{name}” z {count} przedmiotami?',
'packing.defaultCategory': 'Inne',
'packing.dragCategory': 'Przeciągnij, aby zmienić kolejność kategorii',
'packing.toast.saveError': 'Nie udało się zapisać',
'packing.toast.deleteError': 'Nie udało się usunąć',
'packing.toast.renameError': 'Nie udało się zmienić nazwy',
+1
View File
@@ -97,6 +97,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat.few': 'Вы уверены, что хотите удалить список «{name}» с {count} предметами?',
'packing.confirm.deleteCat.many': 'Вы уверены, что хотите удалить список «{name}» с {count} предметами?',
'packing.defaultCategory': 'Другое',
'packing.dragCategory': 'Перетащите, чтобы изменить порядок категории',
'packing.toast.saveError': 'Ошибка сохранения',
'packing.toast.deleteError': 'Ошибка удаления',
'packing.toast.renameError': 'Ошибка переименования',
+1
View File
@@ -109,6 +109,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat.one': 'Naozaj chcete odstrániť zoznam „{name}“ s {count} položkou?',
'packing.confirm.deleteCat.few': 'Naozaj chcete odstrániť zoznam „{name}“ s {count} položkami?',
'packing.defaultCategory': 'Ostatné',
'packing.dragCategory': 'Potiahnutím zmeníte poradie kategórie',
'packing.toast.saveError': 'Uloženie zlyhalo',
'packing.toast.deleteError': 'Odstránenie zlyhalo',
'packing.toast.renameError': 'Premenovanie zlyhalo',
+1
View File
@@ -83,6 +83,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat': 'Är du säker på att du vill ta bort listan "{name}" med {count} föremål?',
'packing.confirm.deleteCat.one': 'Är du säker på att du vill ta bort listan "{name}" med {count} föremål?',
'packing.defaultCategory': 'Övrigt',
'packing.dragCategory': 'Dra för att ändra kategorins ordning',
'packing.toast.saveError': 'Kunde inte spara',
'packing.toast.deleteError': 'Kunde inte ta bort',
'packing.toast.renameError': 'Kunde inte döpa om',
+1
View File
@@ -93,6 +93,7 @@ const packing: TranslationStrings = {
'packing.confirm.clearChecked': 'คุณแน่ใจหรือไม่ว่าต้องการลบรายการที่ทำเครื่องหมายแล้ว {count} รายการ',
'packing.confirm.deleteCat': 'คุณแน่ใจหรือไม่ว่าต้องการลบรายการ "{name}" ที่มี {count} รายการ',
'packing.defaultCategory': 'อื่นๆ',
'packing.dragCategory': 'ลากเพื่อจัดลำดับหมวดหมู่ใหม่',
'packing.toast.saveError': 'บันทึกไม่สำเร็จ',
'packing.toast.deleteError': 'ลบไม่สำเร็จ',
'packing.toast.renameError': 'เปลี่ยนชื่อไม่สำเร็จ',
+1
View File
@@ -83,6 +83,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat': '"{name}" listesini {count} öğesiyle birlikte silmek istediğinizden emin misiniz?',
'packing.confirm.deleteCat.one': '"{name}" listesini {count} öğesiyle birlikte silmek istediğinizden emin misiniz?',
'packing.defaultCategory': 'Diğer',
'packing.dragCategory': 'Kategoriyi yeniden sıralamak için sürükleyin',
'packing.toast.saveError': 'Kaydedilemedi',
'packing.toast.deleteError': 'Silinemedi',
'packing.toast.renameError': 'Yeniden adlandırılamadı',
+1
View File
@@ -97,6 +97,7 @@ const packing: TranslationStrings = {
'packing.confirm.deleteCat.few': 'Ви впевнені, що хочете видалити список «{name}» з {count} елементами?',
'packing.confirm.deleteCat.many': 'Ви впевнені, що хочете видалити список «{name}» з {count} елементами?',
'packing.defaultCategory': 'Інше',
'packing.dragCategory': 'Перетягніть, щоб змінити порядок категорії',
'packing.toast.saveError': 'Помилка збереження',
'packing.toast.deleteError': 'Помилка видалення',
'packing.toast.renameError': 'Помилка перейменування',
+1
View File
@@ -76,6 +76,7 @@ const packing: TranslationStrings = {
'packing.confirm.clearChecked': 'Bạn có chắc chắn muốn xóa {count} mục đã chọn không?',
'packing.confirm.deleteCat': 'Bạn có chắc muốn xóa danh sách "{name}" với {count} mục không?',
'packing.defaultCategory': 'Khác',
'packing.dragCategory': 'Kéo để sắp xếp lại danh mục',
'packing.toast.saveError': 'Không lưu được',
'packing.toast.deleteError': 'Không thể xóa',
'packing.toast.renameError': 'Không thể đổi tên',
+1
View File
@@ -75,6 +75,7 @@ const packing: TranslationStrings = {
'packing.confirm.clearChecked': '確定移除 {count} 個已勾選的物品?',
'packing.confirm.deleteCat': '確定要刪除清單「{name}」及其 {count} 個物品嗎?',
'packing.defaultCategory': '其他',
'packing.dragCategory': '拖曳以重新排列類別',
'packing.toast.saveError': '儲存失敗',
'packing.toast.deleteError': '刪除失敗',
'packing.toast.renameError': '重新命名失敗',
+1
View File
@@ -75,6 +75,7 @@ const packing: TranslationStrings = {
'packing.confirm.clearChecked': '确定移除 {count} 个已勾选的物品?',
'packing.confirm.deleteCat': '确定要删除清单“{name}”及其 {count} 个物品吗?',
'packing.defaultCategory': '其他',
'packing.dragCategory': '拖动以重新排列类别',
'packing.toast.saveError': '保存失败',
'packing.toast.deleteError': '删除失败',
'packing.toast.renameError': '重命名失败',