feat(lists): drag tasks and list entries onto another list; remember open list and view; live pill in lists
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m3s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-31 10:01:41 +01:00
1 parent 6abcfa655b
commit 25bb787a8e
13 files changed
+298 -52

No files matched your search

+72 -40
View File
@@ -4,13 +4,13 @@ import {
Trash2,
Users,
Check,
Radio,
Sparkles,
X,
ShoppingCart,
} from 'lucide-react';
import { ListItem, SharedList, Team, User, WSMessage } from '../../lib/types';
import { api, triggerHaptic, createRealtimeConnection, RealtimeTransport } from '../../lib/api';
import { readViewSession, writeViewSession } from '../../lib/viewSession';
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
import { AvatarStack, AvatarPerson } from '../../components/AvatarStack';
import { CountBadge } from '../../components/CountBadge';
@@ -83,7 +83,13 @@ export function deduplicateItems(items: ListItem[]): ListItem[] {
export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGlobal, currentUser, initialListId }) => {
const { setGroups, setPrimaryAction } = useMobileActions();
const [lists, setLists] = useState<SharedList[]>([]);
const [activeListId, setActiveListId] = useState<string>(initialListId || '');
// The open list survives switching to other tabs (like the workspace's open note)
const [activeListId, setActiveListId] = useState<string>(
() => initialListId || readViewSession<{ listId: string }>('lists').listId || ''
);
useEffect(() => {
if (activeListId) writeViewSession('lists', { listId: activeListId });
}, [activeListId]);
useEffect(() => {
if (initialListId) {
@@ -116,6 +122,8 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
const [draggingItemId, setDraggingItemId] = useState<string | null>(null);
const [dragOverItemTarget, setDragOverItemTarget] = useState<{ itemId: string; position: 'before' | 'after' } | null>(null);
const [dragOverCategoryTarget, setDragOverCategoryTarget] = useState<string | null>(null);
// List tab a dragged entry hovers over (dropping moves it into that list)
const [dragOverListTab, setDragOverListTab] = useState<string | null>(null);
const realtimeConnRef = useRef<any>(null);
@@ -378,6 +386,35 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
setDraggingItemId(item.id);
};
const canWriteList = (lst: SharedList) => lst.isOwner || lst.role !== 'viewer';
// An entry dragged onto another list's tab lands at the top of that list, like a new entry
const handleDropOnListTab = async (e: React.DragEvent, target: SharedList) => {
e.preventDefault();
const itemId = e.dataTransfer.getData('application/mindsift-list-item-id') || draggingItemId;
setDraggingItemId(null);
setDragOverItemTarget(null);
setDragOverCategoryTarget(null);
setDragOverListTab(null);
const source = activeList;
const item = source?.items.find((i) => i.id === itemId);
if (!source || !item || target.id === source.id || !canWriteList(target)) return;
triggerHaptic(15);
setLists((prev) =>
prev.map((l) => {
if (l.id === source.id) return { ...l, items: l.items.filter((i) => i.id !== item.id) };
if (l.id === target.id) return { ...l, items: deduplicateItems([{ ...item, listId: target.id }, ...l.items]) };
return l;
})
);
try {
await api.moveListItem(source.id, item.id, target.id);
} catch {
loadLists();
}
onRefreshGlobal?.();
};
const handleItemDragOver = (e: React.DragEvent, targetItemId: string) => {
e.preventDefault();
e.stopPropagation();
@@ -849,6 +886,23 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
]);
}, [lists, activeList, completedItems.length, setGroups, setPrimaryAction]);
// Connection pill in the same look as the notes' header
const livePill = (
<span
className="inline-flex items-center gap-1 h-7 px-2 rounded-btn text-[11px] bg-panel text-muted"
title={
!wsConnected
? 'Keine Live-Verbindung – Änderungen anderer erscheinen nach dem Neuladen'
: realtimeTransport === 'sse'
? 'Live-Verbindung über Server-Sent Events (WebSocket wird blockiert)'
: 'Live-Verbindung über WebSocket'
}
>
<span className={`w-1.5 h-1.5 rounded-full ${wsConnected ? 'bg-status-ok' : 'bg-status-warn'}`} />
{wsConnected ? 'Live' : 'Offline'}
</span>
);
return (
<div className="w-full space-y-3">
{/* Top Header: List Tabs & Actions (Desktop) */}
@@ -861,9 +915,21 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
return (
<div
key={lst.id}
onDragOver={(e) => {
if (!draggingItemId || isSelected || !canWriteList(lst)) return;
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
if (dragOverListTab !== lst.id) setDragOverListTab(lst.id);
}}
onDragLeave={() => {
if (dragOverListTab === lst.id) setDragOverListTab(null);
}}
onDrop={(e) => handleDropOnListTab(e, lst)}
className={`relative group h-8 flex items-center border rounded-btn transition-colors ${
isSelected ? 'bg-panel border-accent text-main' : 'bg-canvas border-border text-muted hover:text-main'
} ${isSelected && lists.length > 1 ? 'pr-1' : ''}`}
} ${isSelected && lists.length > 1 ? 'pr-1' : ''} ${
dragOverListTab === lst.id ? 'ring-2 ring-accent/60 bg-accent/10 text-main' : ''
}`}
>
<button
onClick={() => setActiveListId(lst.id)}
@@ -951,25 +1017,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
);
})()}
{/* Live Sync Status */}
<div
className={`h-8 w-8 flex items-center justify-center rounded-btn border transition-colors ${
wsConnected
? realtimeTransport === 'sse'
? 'bg-teal-500/10 border-teal-500/30 text-teal-400'
: 'bg-emerald-500/10 border-emerald-500/30 text-emerald-500'
: 'bg-canvas border-border text-muted'
}`}
title={
wsConnected
? realtimeTransport === 'sse'
? 'Live synchronisiert (Server-Sent Events / Proxy-Stream)'
: 'Live synchronisiert (WebSocket online)'
: 'Offline'
}
>
<Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : ''}`} />
</div>
{livePill}
</div>
</div>
@@ -996,24 +1044,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
<Users className="w-4 h-4 text-accent" />
</button>
)}
<div
className={`h-9 w-9 flex items-center justify-center rounded-btn border transition-colors ${
wsConnected
? realtimeTransport === 'sse'
? 'bg-teal-500/10 border-teal-500/30 text-teal-400'
: 'bg-emerald-500/10 border-emerald-500/30 text-emerald-500'
: 'bg-canvas border-border text-muted'
}`}
title={
wsConnected
? realtimeTransport === 'sse'
? 'Live synchronisiert (Server-Sent Events / Proxy-Stream)'
: 'Live synchronisiert (WebSocket online)'
: 'Offline'
}
>
<Radio className={`w-3.5 h-3.5 ${wsConnected ? 'animate-pulse' : ''}`} />
</div>
{livePill}
</div>
</div>
@@ -1195,6 +1226,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
setDraggingItemId(null);
setDragOverItemTarget(null);
setDragOverCategoryTarget(null);
setDragOverListTab(null);
}}
onDragOver={(e) => handleItemDragOver(e, item.id)}
onDragLeave={(e) => handleItemDragLeave(e, item.id)}