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
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:
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">
|
||||
|
||||
Reference in new issue
Block a user