feat(tasks): assign tasks in shared lists, unified people search, list avatars
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 2m36s

- Tasks in shared task lists can be assigned to one or more people with access
  (task_assignees); assignments are pruned whenever access is lost
- UserAutocomplete: type-to-search everywhere, nothing before typing, max 3 results,
  first match preselected
- Task list and list tabs: avatar stack of members and round monochrome counter
- Shared list items remember who added them (list_items.created_by)
- Doku/ui-patterns.md documents the shared UI patterns

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-28 20:26:55 +01:00
1 parent 1bd38f4d50
commit 73fad1c5db
21 files changed
+598 -67

No files matched your search

@@ -13,6 +13,7 @@ import {
import { ListItem, SharedList, ListShare, Team, User, WSMessage } from '../../lib/types';
import { api, buildWebSocketURL, triggerHaptic, createRealtimeConnection, RealtimeTransport } from '../../lib/api';
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
import { AvatarStack, AvatarPerson } from '../../components/AvatarStack';
import { useMobileActions } from '../../context/MobileActionContext';
interface SharedListsViewProps {
@@ -144,10 +145,32 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
.catch(() => {});
}, []);
// People shown in tabs: everyone with access except me
const otherMembers = (lst: SharedList): AvatarPerson[] =>
(lst.members || [])
.filter((m) => m.userId !== currentUser?.id)
.map((m) => ({ id: m.userId, name: m.displayName || m.email, avatarUrl: m.avatarUrl }));
const activeList = useMemo(() => {
return lists.find((l) => l.id === activeListId) || lists[0] || null;
}, [lists, activeListId]);
// In shared lists every item shows who added it (older items without an author show nothing)
const isActiveListShared = (activeList?.members?.length || 0) > 1 || (activeList?.shares?.length || 0) > 0;
const renderItemAuthor = (item: ListItem) => {
if (!isActiveListShared || !item.createdBy) return null;
const member = activeList?.members?.find((m) => m.userId === item.createdBy);
const name = member?.displayName || member?.email || item.createdByName || '';
return (
<AvatarStack
people={[{ id: item.createdBy, name, avatarUrl: member?.avatarUrl }]}
size="xxs"
ringClass="ring-transparent"
title={name ? `Hinzugefügt von ${name}` : undefined}
/>
);
};
useEffect(() => {
if (!activeList?.id) return;
@@ -838,8 +861,14 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
<span>{lst.icon}</span>
<span>{lst.title}</span>
{openCount > 0 && (
<span className="ml-1 badge-neutral">{openCount}</span>
<span className="ml-1 badge-count">{openCount}</span>
)}
<AvatarStack
people={otherMembers(lst)}
size="xxs"
ringClass={isSelected ? 'ring-[var(--bg-panel)]' : 'ring-[var(--bg-app)]'}
className="ml-0.5"
/>
</button>
{isSelected && lists.length > 1 && (
<button
@@ -1267,6 +1296,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
)}
</div>
{renderItemAuthor(item)}
<button
type="button"
draggable={false}
@@ -1375,6 +1405,7 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
</div>
<div className="flex items-center gap-2">
{renderItemAuthor(item)}
{item.completedByName && (
<span className="text-xs text-faint">✓ {item.completedByName}</span>
)}