From f1af10ce1c059ec177b44870a544d22bac8d26f2 Mon Sep 17 00:00:00 2001 From: Sandro Date: Sun, 25 Oct 2026 20:05:43 +0100 Subject: [PATCH] fix(lists): prevent duplicate list item rendering on create --- .../src/features/lists/SharedListsView.tsx | 54 ++++++++++++++----- internal/api/server.go | 4 +- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/frontend/src/features/lists/SharedListsView.tsx b/frontend/src/features/lists/SharedListsView.tsx index adb12ea..c3c3ec9 100644 --- a/frontend/src/features/lists/SharedListsView.tsx +++ b/frontend/src/features/lists/SharedListsView.tsx @@ -64,12 +64,25 @@ export function getItemDisplayContent(item: ListItem): string { return item.content; } +export function deduplicateItems(items: ListItem[]): ListItem[] { + const seen = new Set(); + const result: ListItem[] = []; + for (const item of items) { + if (item && item.id && !seen.has(item.id)) { + seen.add(item.id); + result.push(item); + } + } + return result; +} + export const SharedListsView: React.FC = ({ onRefreshGlobal, currentUser }) => { const [lists, setLists] = useState([]); const [activeListId, setActiveListId] = useState(''); const [newItemText, setNewItemText] = useState(''); const [newItemQty, setNewItemQty] = useState(''); + const [isSubmittingItem, setIsSubmittingItem] = useState(false); const [wsConnected, setWsConnected] = useState(false); const [showNewListModal, setShowNewListModal] = useState(false); @@ -103,9 +116,13 @@ export const SharedListsView: React.FC = ({ onRefreshGloba // fallback } } - setLists(all); - if (all.length > 0 && (!activeListId || !all.some((l) => l.id === activeListId))) { - setActiveListId(all[0].id); + const deduplicated = all.map((l) => ({ + ...l, + items: deduplicateItems(l.items || []), + })); + setLists(deduplicated); + if (deduplicated.length > 0 && (!activeListId || !deduplicated.some((l) => l.id === activeListId))) { + setActiveListId(deduplicated[0].id); } } catch { // ignore @@ -152,11 +169,12 @@ export const SharedListsView: React.FC = ({ onRefreshGloba const msgListId = msg.listId || (msg as any).list_id; if (msg.event === 'ITEM_CREATED' && msgListId) { const newItem: ListItem = msg.payload; + if (!newItem || !newItem.id) return; setLists((prev) => prev.map((l) => { if (l.id !== msgListId) return l; if (l.items.some((i) => i.id === newItem.id)) return l; - return { ...l, items: [newItem, ...l.items] }; + return { ...l, items: deduplicateItems([newItem, ...l.items]) }; }) ); } else if (msg.event === 'ITEM_TOGGLED' && msgListId) { @@ -190,7 +208,8 @@ export const SharedListsView: React.FC = ({ onRefreshGloba msg.event === 'LIST_DELETED' || msg.event === 'LIST_SHARED' || msg.event === 'LIST_UNSHARED' || - msg.event === 'ITEM_REORDERED' + msg.event === 'ITEM_REORDERED' || + msg.event === 'LIST_UPDATED' ) { loadLists(); } @@ -216,7 +235,8 @@ export const SharedListsView: React.FC = ({ onRefreshGloba msg.event === 'LIST_UNSHARED' || msg.event === 'LIST_CREATED' || msg.event === 'LIST_DELETED' || - msg.event === 'ITEM_REORDERED' + msg.event === 'ITEM_REORDERED' || + msg.event === 'LIST_UPDATED' ) { loadLists(); } @@ -227,7 +247,8 @@ export const SharedListsView: React.FC = ({ onRefreshGloba const handleAddItem = async (e: React.FormEvent) => { e.preventDefault(); - if (!activeList || !newItemText.trim()) return; + if (!activeList || !newItemText.trim() || isSubmittingItem) return; + setIsSubmittingItem(true); triggerHaptic(15); setShowSuggestions(false); @@ -239,11 +260,17 @@ export const SharedListsView: React.FC = ({ onRefreshGloba try { const item = await api.createListItem(activeList.id, parsed.content, parsed.category, parsed.quantity); setLists((prev) => - prev.map((l) => (l.id === activeList.id ? { ...l, items: [item, ...l.items] } : l)) + prev.map((l) => { + if (l.id !== activeList.id) return l; + if (l.items.some((i) => i.id === item.id)) return l; + return { ...l, items: deduplicateItems([item, ...l.items]) }; + }) ); onRefreshGlobal(); } catch (err: any) { alert(err.message || 'Fehler beim Erstellen des Listeneintrags'); + } finally { + setIsSubmittingItem(false); } }; @@ -627,8 +654,9 @@ export const SharedListsView: React.FC = ({ onRefreshGloba } }; - const openItems = activeList?.items.filter((i) => !i.isCompleted) || []; - const completedItems = activeList?.items.filter((i) => i.isCompleted) || []; + const safeActiveItems = useMemo(() => deduplicateItems(activeList?.items || []), [activeList?.items]); + const openItems = useMemo(() => safeActiveItems.filter((i) => !i.isCompleted), [safeActiveItems]); + const completedItems = useMemo(() => safeActiveItems.filter((i) => i.isCompleted), [safeActiveItems]); const groupedOpenItems = useMemo(() => { const groupsMap = new Map(); @@ -690,7 +718,7 @@ export const SharedListsView: React.FC = ({ onRefreshGloba {/* List Tabs */}
{lists.map((lst) => { - const openCount = lst.items.filter((i) => !i.isCompleted).length; + const openCount = deduplicateItems(lst.items || []).filter((i) => !i.isCompleted).length; const isSelected = activeList?.id === lst.id; return (
@@ -909,10 +937,10 @@ export const SharedListsView: React.FC = ({ onRefreshGloba />
diff --git a/internal/api/server.go b/internal/api/server.go index 53c1f1c..ecebb35 100644 --- a/internal/api/server.go +++ b/internal/api/server.go @@ -586,7 +586,7 @@ func (s *Server) handleCaptureInbox(w http.ResponseWriter, r *http.Request) { thought = converted if target == "list_item" && targetListID != "" { if lst, lErr := s.store.GetListByID(userID, targetListID); lErr == nil && lst != nil { - s.wsHub.BroadcastListEvent(lst.ID, "ITEM_CREATED", lst) + s.wsHub.BroadcastListEvent(lst.ID, "LIST_UPDATED", lst) } } } @@ -663,7 +663,7 @@ func (s *Server) handleTriageAction(w http.ResponseWriter, r *http.Request) { // If converted to a list item, broadcast via WebSocket to the space if convertTo == "list_item" && targetListID != "" { if lst, err := s.store.GetListByID(userID, targetListID); err == nil && lst != nil { - s.wsHub.BroadcastListEvent(lst.ID, "ITEM_CREATED", lst) + s.wsHub.BroadcastListEvent(lst.ID, "LIST_UPDATED", lst) } }