feat(collections): import a whole trip's places into a list (#1903)

* feat(collections): preview which of a trip's places a list already holds

The bulk import endpoint has been there since the select-mode copy, but nothing
calls it and it only reports duplicates after the fact. A dialog needs the verdict
up front, so /:id/importable/:tripId returns the trip's places with the duplicate
check the import itself applies, plus whether any day holds the place.

* feat(collections): import a whole trip's places into a list

Two steps: pick a trip, then tick its places. Places already on the list are
greyed out and unpickable, and the ones no day holds start selected — those are
the plans a trip left behind, which is the reason to import at all. Sorted so a
mostly-saved trip reads as the two that are left rather than a wall of grey.

* feat(collections): reach the trip import from the filter row and an empty list

* i18n(collections): translate the trip import into all locales

* fix(collections): stop the label dialog's buttons wrapping mid-word

Three actions in a max-w-sm footer wrapped into two lines each. The assign
button also painted white text on the accent, which disappears on a light one.

* docs(collections): document the trip import
This commit is contained in:
Maurice
2026-08-12 11:54:36 +02:00
committed by GitHub
parent b0fd73681b
commit b842d74a5d
37 changed files with 1286 additions and 11 deletions
@@ -39,7 +39,7 @@ export default function BulkAssignLabelModal({ isOpen, labels, count, onAssign,
}
return (
<Modal isOpen={isOpen} onClose={onClose} title={t('collections.labels.assignN', { count })} size="sm">
<Modal isOpen={isOpen} onClose={onClose} title={t('collections.labels.assignN', { count })} size="lg">
{labels.length === 0 ? (
<div className="flex flex-col items-center gap-3 py-6 text-center">
<Tags size={26} className="text-content-faint" />
@@ -67,18 +67,20 @@ export default function BulkAssignLabelModal({ isOpen, labels, count, onAssign,
)
})}
</div>
{/* nowrap on all three: the labels are long in several locales and used to
wrap into two lines inside the buttons before the dialog got wider. */}
<div className="flex justify-end gap-2 pt-2 border-t border-edge">
<button type="button" onClick={onManage} className="mr-auto flex items-center gap-1.5 px-3 py-2 rounded-lg text-[13px] text-content-secondary hover:bg-surface-hover">
<button type="button" onClick={onManage} className="mr-auto flex items-center gap-1.5 px-3 py-2 rounded-lg text-[13px] text-content-secondary whitespace-nowrap hover:bg-surface-hover">
<Settings2 size={14} /> {t('collections.labels.manage')}
</button>
<button type="button" onClick={onClose} className="px-3 py-2 rounded-lg border border-edge text-content-secondary text-[13px] hover:bg-surface-hover">
<button type="button" onClick={onClose} className="px-3 py-2 rounded-lg border border-edge text-content-secondary text-[13px] whitespace-nowrap hover:bg-surface-hover">
{t('common.cancel')}
</button>
<button
type="button"
onClick={assign}
disabled={picked.length === 0 || busy}
className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-accent text-white text-[13px] font-semibold hover:opacity-90 disabled:opacity-50"
className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-accent text-accent-text text-[13px] font-semibold whitespace-nowrap hover:opacity-90 disabled:opacity-50"
>
{busy ? <Loader2 size={14} className="animate-spin" /> : <Check size={14} />} {t('collections.labels.assign')}
</button>