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
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:
1 parent
6abcfa655b
commit
25bb787a8e
13 files changed
+298
-52
No files matched your search
@@ -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)}
|
||||
|
||||
Reference in new issue
Block a user