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

@@ -16,8 +16,11 @@ import {
Folder,
Webhook,
Lock,
UserPlus,
} from 'lucide-react';
import { Thought, SubTask, TaskList, KanbanColumn } from '../../lib/types';
import { Thought, SubTask, TaskList, TaskListMember, KanbanColumn, User } from '../../lib/types';
import { UserAvatar } from '../../components/UserAvatar';
import { UserAutocomplete } from '../UserAutocomplete';
import { triggerHaptic } from '../../lib/api';
import { MarkdownViewer } from '../../lib/markdown';
@@ -34,6 +37,8 @@ interface TaskDetailModalProps {
onUpdateTaskList: (task: Thought, newListId: string) => Promise<void>;
onUpdateNotes: (task: Thought, newNotes: string) => Promise<void>;
onUpdateTags?: (task: Thought, newTags: string[]) => Promise<void>;
assignableMembers?: TaskListMember[]; // empty unless the task's list is shared
onUpdateAssignees?: (task: Thought, assignees: string[]) => Promise<void>;
onToggleSubtask: (task: Thought, subtaskId: string) => Promise<void>;
onAddSubtask: (task: Thought, title: string) => Promise<void>;
onDeleteSubtask: (task: Thought, subtaskId: string) => Promise<void>;
@@ -56,6 +61,8 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
onUpdateTaskList,
onUpdateNotes,
onUpdateTags,
assignableMembers = [],
onUpdateAssignees,
onToggleSubtask,
onAddSubtask,
onDeleteSubtask,
@@ -525,6 +532,65 @@ export const TaskDetailModal: React.FC<TaskDetailModalProps> = ({
</div>
</div>
{/* Assignees (shared lists only): chips + the shared type-to-search */}
{assignableMembers.length > 0 && (() => {
const assigned = (task.assignees || [])
.map((id) => assignableMembers.find((m) => m.userId === id))
.filter((m): m is TaskListMember => !!m);
const assignedIds = assigned.map((m) => m.userId);
const candidates: User[] = assignableMembers.map((m) => ({
id: m.userId,
email: m.email,
displayName: m.displayName,
avatarUrl: m.avatarUrl,
role: 'member',
isActive: true,
createdAt: '',
}));
return (
<div className="space-y-1.5 pt-1 border-t border-border">
<label className="block text-xs font-semibold text-main flex items-center gap-1">
<UserPlus className="w-3.5 h-3.5 text-accent" />
<span>Zugewiesen:</span>
</label>
{assigned.length > 0 ? (
<div className="flex flex-wrap items-center gap-1.5">
{assigned.map((m) => (
<span
key={m.userId}
className="inline-flex items-center gap-1.5 pl-0.5 pr-2 py-0.5 rounded-pill bg-panel border border-border text-xs text-main"
>
<UserAvatar name={m.displayName || m.email} avatarUrl={m.avatarUrl || ''} size="xxs" showTitle={false} />
<span>{m.displayName || m.email}</span>
{!readOnly && onUpdateAssignees && (
<button
type="button"
onClick={() => onUpdateAssignees(task, assignedIds.filter((id) => id !== m.userId))}
className="text-muted hover:text-status-err"
title="Zuweisung entfernen"
>
<X className="w-3 h-3" />
</button>
)}
</span>
))}
</div>
) : (
readOnly && <p className="text-xs text-muted">Niemandem zugewiesen</p>
)}
{!readOnly && onUpdateAssignees && assigned.length < assignableMembers.length && (
<UserAutocomplete
users={candidates}
selectedUser={null}
onSelectUser={(u) => u && onUpdateAssignees(task, [...assignedIds, u.id])}
excludeUserIds={assignedIds}
placeholder="Person zuweisen (Name tippen)…"
/>
)}
</div>
);
})()}
{/* Hashtags Section */}
<div className="space-y-1.5 pt-1 border-t border-border">
<label className="block text-xs font-semibold text-main flex items-center gap-1">