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;
|
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 }) => {
|
export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGlobal, currentUser }) => {
|
||||||
const [lists, setLists] = useState<SharedList[]>([]);
|
const [lists, setLists] = useState<SharedList[]>([]);
|
||||||
const [activeListId, setActiveListId] = useState<string>('');
|
const [activeListId, setActiveListId] = useState<string>('');
|
||||||
|
|
||||||
const [newItemText, setNewItemText] = useState('');
|
const [newItemText, setNewItemText] = useState('');
|
||||||
const [newItemQty, setNewItemQty] = useState('');
|
const [newItemQty, setNewItemQty] = useState('');
|
||||||
|
const [isSubmittingItem, setIsSubmittingItem] = useState(false);
|
||||||
|
|
||||||
const [wsConnected, setWsConnected] = useState(false);
|
const [wsConnected, setWsConnected] = useState(false);
|
||||||
const [showNewListModal, setShowNewListModal] = useState(false);
|
const [showNewListModal, setShowNewListModal] = useState(false);
|
||||||
@@ -103,9 +116,13 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
// fallback
|
// fallback
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setLists(all);
|
const deduplicated = all.map((l) => ({
|
||||||
if (all.length > 0 && (!activeListId || !all.some((l) => l.id === activeListId))) {
|
...l,
|
||||||
setActiveListId(all[0].id);
|
items: deduplicateItems(l.items || []),
|
||||||
|
}));
|
||||||
|
setLists(deduplicated);
|
||||||
|
if (deduplicated.length > 0 && (!activeListId || !deduplicated.some((l) => l.id === activeListId))) {
|
||||||
|
setActiveListId(deduplicated[0].id);
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
@@ -152,11 +169,12 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
const msgListId = msg.listId || (msg as any).list_id;
|
const msgListId = msg.listId || (msg as any).list_id;
|
||||||
if (msg.event === 'ITEM_CREATED' && msgListId) {
|
if (msg.event === 'ITEM_CREATED' && msgListId) {
|
||||||
const newItem: ListItem = msg.payload;
|
const newItem: ListItem = msg.payload;
|
||||||
|
if (!newItem || !newItem.id) return;
|
||||||
setLists((prev) =>
|
setLists((prev) =>
|
||||||
prev.map((l) => {
|
prev.map((l) => {
|
||||||
if (l.id !== msgListId) return l;
|
if (l.id !== msgListId) return l;
|
||||||
if (l.items.some((i) => i.id === newItem.id)) 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) {
|
} 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_DELETED' ||
|
||||||
msg.event === 'LIST_SHARED' ||
|
msg.event === 'LIST_SHARED' ||
|
||||||
msg.event === 'LIST_UNSHARED' ||
|
msg.event === 'LIST_UNSHARED' ||
|
||||||
msg.event === 'ITEM_REORDERED'
|
msg.event === 'ITEM_REORDERED' ||
|
||||||
|
msg.event === 'LIST_UPDATED'
|
||||||
) {
|
) {
|
||||||
loadLists();
|
loadLists();
|
||||||
}
|
}
|
||||||
@@ -216,7 +235,8 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
msg.event === 'LIST_UNSHARED' ||
|
msg.event === 'LIST_UNSHARED' ||
|
||||||
msg.event === 'LIST_CREATED' ||
|
msg.event === 'LIST_CREATED' ||
|
||||||
msg.event === 'LIST_DELETED' ||
|
msg.event === 'LIST_DELETED' ||
|
||||||
msg.event === 'ITEM_REORDERED'
|
msg.event === 'ITEM_REORDERED' ||
|
||||||
|
msg.event === 'LIST_UPDATED'
|
||||||
) {
|
) {
|
||||||
loadLists();
|
loadLists();
|
||||||
}
|
}
|
||||||
@@ -227,7 +247,8 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
|
|
||||||
const handleAddItem = async (e: React.FormEvent) => {
|
const handleAddItem = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!activeList || !newItemText.trim()) return;
|
if (!activeList || !newItemText.trim() || isSubmittingItem) return;
|
||||||
|
setIsSubmittingItem(true);
|
||||||
triggerHaptic(15);
|
triggerHaptic(15);
|
||||||
|
|
||||||
setShowSuggestions(false);
|
setShowSuggestions(false);
|
||||||
@@ -239,11 +260,17 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
try {
|
try {
|
||||||
const item = await api.createListItem(activeList.id, parsed.content, parsed.category, parsed.quantity);
|
const item = await api.createListItem(activeList.id, parsed.content, parsed.category, parsed.quantity);
|
||||||
setLists((prev) =>
|
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();
|
onRefreshGlobal();
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
alert(err.message || 'Fehler beim Erstellen des Listeneintrags');
|
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 safeActiveItems = useMemo(() => deduplicateItems(activeList?.items || []), [activeList?.items]);
|
||||||
const completedItems = activeList?.items.filter((i) => i.isCompleted) || [];
|
const openItems = useMemo(() => safeActiveItems.filter((i) => !i.isCompleted), [safeActiveItems]);
|
||||||
|
const completedItems = useMemo(() => safeActiveItems.filter((i) => i.isCompleted), [safeActiveItems]);
|
||||||
|
|
||||||
const groupedOpenItems = useMemo(() => {
|
const groupedOpenItems = useMemo(() => {
|
||||||
const groupsMap = new Map<string, ListItem[]>();
|
const groupsMap = new Map<string, ListItem[]>();
|
||||||
@@ -690,7 +718,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
{/* List Tabs */}
|
{/* List Tabs */}
|
||||||
<div className="flex flex-wrap items-center gap-1">
|
<div className="flex flex-wrap items-center gap-1">
|
||||||
{lists.map((lst) => {
|
{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;
|
const isSelected = activeList?.id === lst.id;
|
||||||
return (
|
return (
|
||||||
<div key={lst.id} className="relative group flex items-center">
|
<div key={lst.id} className="relative group flex items-center">
|
||||||
@@ -909,10 +937,10 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
|
|||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -586,7 +586,7 @@ func (s *Server) handleCaptureInbox(w http.ResponseWriter, r *http.Request) {
|
|||||||
thought = converted
|
thought = converted
|
||||||
if target == "list_item" && targetListID != "" {
|
if target == "list_item" && targetListID != "" {
|
||||||
if lst, lErr := s.store.GetListByID(userID, targetListID); lErr == nil && lst != nil {
|
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 converted to a list item, broadcast via WebSocket to the space
|
||||||
if convertTo == "list_item" && targetListID != "" {
|
if convertTo == "list_item" && targetListID != "" {
|
||||||
if lst, err := s.store.GetListByID(userID, targetListID); err == nil && lst != nil {
|
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