fix(lists): prevent duplicate list item rendering on create
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 59s
This commit is contained in:
1 parent
5cfa390ee0
commit
f1af10ce1c
2 files changed
+43
-15
No files matched your search
@@ -64,12 +64,25 @@ export function getItemDisplayContent(item: ListItem): string {
|
||||
return item.content;
|
||||
}
|
||||
|
||||
export function deduplicateItems(items: ListItem[]): ListItem[] {
|
||||
const seen = new Set<string>();
|
||||
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<SharedListsViewProps> = ({ onRefreshGlobal, currentUser }) => {
|
||||
const [lists, setLists] = useState<SharedList[]>([]);
|
||||
const [activeListId, setActiveListId] = useState<string>('');
|
||||
|
||||
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<SharedListsViewProps> = ({ 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<SharedListsViewProps> = ({ 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<SharedListsViewProps> = ({ 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<SharedListsViewProps> = ({ 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<SharedListsViewProps> = ({ 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<SharedListsViewProps> = ({ 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<SharedListsViewProps> = ({ 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<string, ListItem[]>();
|
||||
@@ -690,7 +718,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
||||
{/* List Tabs */}
|
||||
<div className="flex flex-wrap items-center gap-1">
|
||||
{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 (
|
||||
<div key={lst.id} className="relative group flex items-center">
|
||||
@@ -909,10 +937,10 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!newItemText.trim()}
|
||||
disabled={isSubmittingItem || !newItemText.trim()}
|
||||
className="btn-primary h-7 px-3 text-xs disabled:opacity-40 disabled:cursor-not-allowed flex items-center gap-1 flex-shrink-0"
|
||||
>
|
||||
Hinzufügen
|
||||
{isSubmittingItem ? 'Wird hinzugefügt…' : 'Hinzufügen'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user