fix(packing): name everyone who brings a shared item

A common item brought by several members showed the first bringer's avatar
and a bare "+2". The tooltip only named the first person, and on the phone
neither the row nor the item sheet said who else brings it (discussion
#2782).

packingBringerNames lists the bringer and every contributor; the desktop
row, the phone row and the phone item sheet use it for "brought by".
This commit is contained in:
Maurice
2026-11-04 17:53:15 +01:00
parent b41f6e3542
commit da324121bd
5 changed files with 42 additions and 3 deletions
@@ -18,6 +18,7 @@ import { PopoverItem } from './PackingPopover'
import { POPOVER, POPOVER_CAPTION, POPOVER_DIVIDER } from './packingPopoverStyles'
import type { TripMember } from './usePackingListPanel'
import { NumericInput } from '../shared/NumericInput'
import { packingBringerNames } from './packingBringers'
interface ArtikelZeileProps {
item: PackingItem
@@ -226,7 +227,7 @@ export function ArtikelZeile({ item, tripId, categories, onCategoryChange: _onCa
</span>
)}
{!isPlaceholder && broughtBy && (
<span className="packing-row-badge" title={t('packing.broughtBy', { name: broughtBy })} aria-label={t('packing.broughtBy', { name: broughtBy })}
<span className="packing-row-badge" title={t('packing.broughtBy', { name: packingBringerNames(item) })} aria-label={t('packing.broughtBy', { name: packingBringerNames(item) })}
style={{ display: 'inline-flex', alignItems: 'center', gap: 3, flexShrink: 0 }}>
{ownerAvatar
? <img src={ownerAvatar} alt="" style={{ width: 20, height: 20, borderRadius: '50%', objectFit: 'cover', display: 'block' }} />
@@ -0,0 +1,22 @@
// FE-PACK-BRINGERS-001 to FE-PACK-BRINGERS-003
import { describe, expect, it } from 'vitest';
import { packingBringerNames } from './packingBringers';
describe('packingBringerNames', () => {
it('FE-PACK-BRINGERS-001: names the bringer first, then everyone who also brings it', () => {
const item = {
owner_username: 'anna',
contributors: [{ username: 'ben' }, { username: 'carla' }],
};
expect(packingBringerNames(item)).toBe('anna, ben, carla');
});
it('FE-PACK-BRINGERS-002: names a person once, even when listed twice', () => {
expect(packingBringerNames({ owner_username: 'anna', contributors: [{ username: 'anna' }] })).toBe('anna');
});
it('FE-PACK-BRINGERS-003: copes with an item without a bringer or contributors', () => {
expect(packingBringerNames({ owner_username: null, contributors: [{ username: 'ben' }] })).toBe('ben');
expect(packingBringerNames({})).toBe('');
});
});
@@ -0,0 +1,14 @@
interface BroughtItem {
owner_username?: string | null;
contributors?: { username: string }[] | null;
}
/**
* Everyone bringing a common item, as one readable list: its bringer first, then
* whoever also brings it. The row only has room for one avatar and a "+n", so this
* is what its tooltip and the item sheet name (#2782).
*/
export function packingBringerNames(item: BroughtItem): string {
const names = [item.owner_username, ...(item.contributors ?? []).map((c) => c.username)];
return [...new Set(names.filter((n): n is string => !!n))].join(', ');
}
@@ -6,6 +6,7 @@ import { avatarSrc } from '../../../../utils/avatarSrc'
import type { PackingItem, TripMember } from '../../../../types'
import type { TripPlanner } from '../MTripShell'
import { isPackingPlaceholder } from '../../../../components/Packing/packingListModel'
import { packingBringerNames } from '../../../../components/Packing/packingBringers'
export interface MPackItemSheetProps {
planner: TripPlanner
@@ -160,7 +161,7 @@ export default function MPackItemSheet({
<div className="rounded-[14px] border border-[color:var(--m-rowbr)] bg-[color:var(--m-ic)] p-3">
{item.owner_username && (
<div className="mb-[9px] font-geist text-[0.6875rem] text-m-muted">
{t('packing.broughtBy', { name: item.owner_username })}
{t('packing.broughtBy', { name: packingBringerNames(item) })}
</div>
)}
<div className="flex gap-2">
@@ -28,6 +28,7 @@ import MPackItemSheet from './MPackItemSheet'
import MPackingImportSheet from './MPackingImportSheet'
import PackingPrintPreview from '../../../../components/Packing/PackingPrintPreview'
import { usePackingExport } from '../../../../components/Packing/usePackingExport'
import { packingBringerNames } from '../../../../components/Packing/packingBringers'
type ActionView = 'menu' | 'apply' | 'save'
@@ -718,7 +719,7 @@ function PackingItemRow({ item, planner, currentUserId, editMode, canEdit, bagTr
</span>
)}
{!badgeSharedToMe && !badgeSharedByMe && badgeBroughtBy && (
<span className="flex flex-none items-center gap-[3px]" title={item.owner_username || undefined}>
<span className="flex flex-none items-center gap-[3px]" title={t('packing.broughtBy', { name: packingBringerNames(item) })} aria-label={t('packing.broughtBy', { name: packingBringerNames(item) })}>
{ownerAvatarUrl
? <img src={ownerAvatarUrl} alt={item.owner_username || ''} className="h-5 w-5 flex-none rounded-full object-cover" />
: <span className="flex h-5 w-5 flex-none items-center justify-center rounded-full bg-[color:var(--m-ic)] font-geist text-[0.5625rem] font-bold text-m-muted">{(item.owner_username || '?')[0]?.toUpperCase()}</span>}