mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
feat(collections): select toolbar — select-all, move/duplicate to another list (#1081)
- The select toggle now sits at the right of the filter row (same height as the status/category dropdowns) instead of the top toolbar. - Select mode gains a "select all / deselect all" toggle and shows even with nothing selected yet. - Selected places can be moved or duplicated into another of your lists via a target-list picker (move re-points collection_id; duplicate re-saves the place data, carrying description / category / notes / etc.). - New strings across all 22 locales.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import React from 'react'
|
||||
import { List as ListIcon, Map as MapIcon, Search, Bookmark, CheckSquare, X, Trash2, Copy, Plus } from 'lucide-react'
|
||||
import { List as ListIcon, Map as MapIcon, Search, Bookmark, CheckCheck, X, Trash2, Copy, CopyPlus, FolderInput, Plus } from 'lucide-react'
|
||||
import Navbar from '../components/Layout/Navbar'
|
||||
import Modal from '../components/shared/Modal'
|
||||
import ListsRail from '../components/Collections/ListsRail'
|
||||
@@ -9,6 +9,7 @@ import CollectionList from '../components/Collections/CollectionList'
|
||||
import CollectionFilterBar from '../components/Collections/CollectionFilterBar'
|
||||
import CollectionMapPanel from '../components/Collections/CollectionMapPanel'
|
||||
import CopyToTripModal from '../components/Collections/CopyToTripModal'
|
||||
import MoveToListModal from '../components/Collections/MoveToListModal'
|
||||
import ShareCollectionModal from '../components/Collections/ShareCollectionModal'
|
||||
import AddPlaceToCollectionModal from '../components/Collections/AddPlaceToCollectionModal'
|
||||
import CollectionPlaceDetail from '../components/Collections/CollectionPlaceDetail'
|
||||
@@ -105,6 +106,9 @@ export default function CollectionsPage(): React.ReactElement {
|
||||
categoryOptions={c.categoryOptions}
|
||||
onStatusFilter={c.setStatusFilter}
|
||||
onCategoryFilter={c.setCategoryFilter}
|
||||
showSelect={showSelect}
|
||||
selectMode={c.selectMode}
|
||||
onToggleSelect={() => c.setSelectMode(!c.selectMode)}
|
||||
t={t}
|
||||
/>
|
||||
) : null
|
||||
@@ -204,17 +208,6 @@ export default function CollectionsPage(): React.ReactElement {
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
{showSelect && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => c.setSelectMode(!c.selectMode)}
|
||||
className={`col-iconbtn${c.selectMode ? ' on' : ''}`}
|
||||
aria-label={t('collections.selectMode')}
|
||||
title={t('collections.selectMode')}
|
||||
>
|
||||
<CheckSquare size={16} />
|
||||
</button>
|
||||
)}
|
||||
{canAddPlace && (
|
||||
<button type="button" onClick={() => c.setShowAddPlace(true)} className="col-iconbtn" aria-label={t('collections.addPlace')} title={t('collections.addPlace')}>
|
||||
<Plus size={16} />
|
||||
@@ -234,14 +227,23 @@ export default function CollectionsPage(): React.ReactElement {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{c.selectMode && c.selectedIds.length > 0 && (
|
||||
{c.selectMode && (
|
||||
<div className="col-selbar">
|
||||
<button type="button" onClick={c.handleSelectAll} className="col-selbar-btn">
|
||||
<CheckCheck size={14} /> {c.allVisibleSelected ? t('collections.deselectAll') : t('collections.selectAll')}
|
||||
</button>
|
||||
<span className="lbl">{t('collections.selectedCount', { count: c.selectedIds.length })}</span>
|
||||
<div className="col-toolbar-spacer" />
|
||||
<button type="button" onClick={c.openCopyForSelection} className="col-selbar-btn">
|
||||
<Copy size={14} /> {t('collections.copyN', { count: c.selectedIds.length })}
|
||||
<button type="button" onClick={() => c.setListPickerMode('move')} disabled={c.selectedIds.length === 0} className="col-selbar-btn">
|
||||
<FolderInput size={14} /> {t('collections.moveToList')}
|
||||
</button>
|
||||
<button type="button" onClick={c.handleDeleteSelected} className="col-selbar-btn danger">
|
||||
<button type="button" onClick={() => c.setListPickerMode('copy')} disabled={c.selectedIds.length === 0} className="col-selbar-btn">
|
||||
<CopyPlus size={14} /> {t('collections.duplicateToList')}
|
||||
</button>
|
||||
<button type="button" onClick={c.openCopyForSelection} disabled={c.selectedIds.length === 0} className="col-selbar-btn">
|
||||
<Copy size={14} /> {t('collections.copyToTrip')}
|
||||
</button>
|
||||
<button type="button" onClick={c.handleDeleteSelected} disabled={c.selectedIds.length === 0} className="col-selbar-btn danger">
|
||||
<Trash2 size={14} /> {t('common.delete')}
|
||||
</button>
|
||||
<button type="button" onClick={() => c.setSelectMode(false)} className="col-selbar-btn" aria-label={t('common.cancel')}>
|
||||
@@ -306,6 +308,18 @@ export default function CollectionsPage(): React.ReactElement {
|
||||
t={t}
|
||||
/>
|
||||
|
||||
{/* Move / duplicate the selection into another list */}
|
||||
{c.listPickerMode && (
|
||||
<MoveToListModal
|
||||
mode={c.listPickerMode}
|
||||
lists={c.ownedLists.filter(l => l.id !== c.activeId)}
|
||||
count={c.selectedIds.length}
|
||||
onPick={c.listPickerMode === 'move' ? c.handleMoveToList : c.handleDuplicateToList}
|
||||
onClose={() => c.setListPickerMode(null)}
|
||||
t={t}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Share / fusion */}
|
||||
{c.canShare && typeof c.activeId === 'number' && c.activeCollection && (
|
||||
<ShareCollectionModal
|
||||
|
||||
@@ -53,6 +53,7 @@ export function useCollections() {
|
||||
loadAll, setActive, refreshActive, loadCollection,
|
||||
deleteCollection,
|
||||
setStatus, updatePlace, deletePlace, deleteMany, copyToTrip, clearSelection,
|
||||
moveToList, duplicateToList, setSelectedIds,
|
||||
acceptInvite, declineInvite,
|
||||
setView, setStatusFilter, setCategoryFilter, setSearch, setSelectedPlaceId, setSelectMode, toggleSelect,
|
||||
} = store
|
||||
@@ -211,6 +212,35 @@ export function useCollections() {
|
||||
}
|
||||
}, [selectedIds, deleteMany, toast, t])
|
||||
|
||||
// Select-all toggles between every visible place and none.
|
||||
const allVisibleSelected = visiblePlaces.length > 0 && visiblePlaces.every(p => selectedIds.includes(p.id))
|
||||
const handleSelectAll = useCallback(() => {
|
||||
setSelectedIds(allVisibleSelected ? [] : visiblePlaces.map(p => p.id))
|
||||
}, [allVisibleSelected, visiblePlaces, setSelectedIds])
|
||||
|
||||
// Move / duplicate the selection into another list (a list-picker modal).
|
||||
const [listPickerMode, setListPickerMode] = useState<'move' | 'copy' | null>(null)
|
||||
const handleMoveToList = useCallback(async (targetId: number) => {
|
||||
if (selectedIds.length === 0) return
|
||||
try {
|
||||
await moveToList(selectedIds, targetId)
|
||||
toast.success(t('collections.movedCount', { count: selectedIds.length }))
|
||||
setListPickerMode(null)
|
||||
} catch (err) {
|
||||
toast.error(getApiErrorMessage(err, t('common.error')))
|
||||
}
|
||||
}, [selectedIds, moveToList, toast, t])
|
||||
const handleDuplicateToList = useCallback(async (targetId: number) => {
|
||||
if (selectedIds.length === 0) return
|
||||
try {
|
||||
await duplicateToList(selectedIds, targetId)
|
||||
toast.success(t('collections.duplicatedCount', { count: selectedIds.length }))
|
||||
setListPickerMode(null)
|
||||
} catch (err) {
|
||||
toast.error(getApiErrorMessage(err, t('common.error')))
|
||||
}
|
||||
}, [selectedIds, duplicateToList, toast, t])
|
||||
|
||||
const handleAcceptInvite = useCallback(async (collectionId: number) => {
|
||||
try {
|
||||
await acceptInvite(collectionId)
|
||||
@@ -311,5 +341,7 @@ export function useCollections() {
|
||||
handleSelectList, handleDeleteList,
|
||||
handleStatusChange, handleDeletePlace, handleDeleteSelected,
|
||||
handleAcceptInvite, handleDeclineInvite,
|
||||
allVisibleSelected, handleSelectAll,
|
||||
listPickerMode, setListPickerMode, handleMoveToList, handleDuplicateToList,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user