feat: unify sharing UI, redesign lists header, fix team sharing, and add user autocomplete
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 18s

This commit is contained in:
sandrodach committed 2026-10-25 16:04:08 +01:00
1 parent 6d07352e80
commit 354129b2d4
10 files changed
+1049 -393

No files matched your search

+119 -81
View File
@@ -13,6 +13,7 @@ import {
Edit3,
X,
Layers,
Share,
Share2,
Users,
CheckSquare,
@@ -29,6 +30,7 @@ import { TaskList, Thought, SubTask, Team, User, TaskListShare, KanbanColumn } f
import { api, triggerHaptic } from '../../lib/api';
import { TaskDetailModal } from './TaskDetailModal';
import { ManageColumnsModal, DEFAULT_KANBAN_COLUMNS } from './ManageColumnsModal';
import { UserAutocomplete } from '../UserAutocomplete';
const getInitials = (name?: string): string => {
if (!name) return '?';
@@ -40,12 +42,14 @@ const getInitials = (name?: string): string => {
};
interface TasksViewProps {
currentUser?: User | null;
onRefreshStats: () => void;
onOpenDocument: (docId: string) => void;
refreshTrigger?: number;
}
export const TasksView: React.FC<TasksViewProps> = ({
currentUser,
onRefreshStats,
onOpenDocument,
refreshTrigger = 0,
@@ -75,6 +79,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
const [sharingTaskList, setSharingTaskList] = useState<TaskList | null>(null);
const [addShareType, setAddShareType] = useState<'team' | 'user'>('team');
const [selectedShareTargetId, setSelectedShareTargetId] = useState('');
const [selectedShareUser, setSelectedShareUser] = useState<User | null>(null);
const [shareLoading, setShareLoading] = useState(false);
// Detail modal state
@@ -577,26 +582,47 @@ export const TasksView: React.FC<TasksViewProps> = ({
}
};
const handleOpenShareModal = (tl: TaskList) => {
const handleOpenShareModal = async (tl: TaskList) => {
setSharingTaskList(tl);
setSelectedShareTargetId('');
setSelectedShareUser(null);
setShowShareListModal(true);
try {
const [freshTeams, freshUsers, freshShares] = await Promise.all([
api.getUserTeams().catch(() => []),
api.getUsersDirectory ? api.getUsersDirectory().catch(() => api.getCollaborators().catch(() => [])) : api.getCollaborators().catch(() => []),
api.getTaskListShares(tl.id).catch(() => []),
]);
setTeams(freshTeams);
setCollaborators(freshUsers);
setSharingTaskList((prev) => (prev && prev.id === tl.id ? { ...prev, shares: freshShares } : prev));
} catch {
// ignore
}
};
const handleAddShare = async () => {
if (!sharingTaskList || !selectedShareTargetId) return;
if (!sharingTaskList) return;
const targetId = addShareType === 'team' ? selectedShareTargetId : selectedShareUser?.id;
if (!targetId) return;
setShareLoading(true);
triggerHaptic(15);
try {
await api.addTaskListShare(sharingTaskList.id, {
targetType: addShareType,
targetId: selectedShareTargetId,
targetId: targetId,
});
const freshLists = await api.getTaskLists();
const [freshLists, freshShares] = await Promise.all([
api.getTaskLists(),
api.getTaskListShares(sharingTaskList.id).catch(() => []),
]);
setTaskLists(freshLists);
const updated = freshLists.find((l) => l.id === sharingTaskList.id);
if (updated) setSharingTaskList(updated);
if (updated) {
setSharingTaskList({ ...updated, shares: freshShares });
}
setSelectedShareTargetId('');
setSelectedShareUser(null);
await loadData();
} catch (err: any) {
alert(err.message || 'Fehler beim Hinzufügen der Freigabe');
@@ -607,14 +633,20 @@ export const TasksView: React.FC<TasksViewProps> = ({
const handleRemoveShare = async (shareId: string) => {
if (!sharingTaskList) return;
if (!window.confirm('Freigabe wirklich aufheben?')) return;
setShareLoading(true);
triggerHaptic(15);
try {
await api.removeTaskListShare(sharingTaskList.id, shareId);
const freshLists = await api.getTaskLists();
const [freshLists, freshShares] = await Promise.all([
api.getTaskLists(),
api.getTaskListShares(sharingTaskList.id).catch(() => []),
]);
setTaskLists(freshLists);
const updated = freshLists.find((l) => l.id === sharingTaskList.id);
if (updated) setSharingTaskList(updated);
if (updated) {
setSharingTaskList({ ...updated, shares: freshShares });
}
await loadData();
} catch (err: any) {
alert(err.message || 'Fehler beim Entfernen der Freigabe');
@@ -869,19 +901,27 @@ export const TasksView: React.FC<TasksViewProps> = ({
{tl.tasksCount !== undefined && tl.tasksCount > 0 && (
<span className="ml-1 badge-neutral">{tl.tasksCount}</span>
)}
{tl.members && tl.members.length > 0 ? (
<div className="flex items-center -space-x-1 ml-0.5" title={`Geteilt (${tl.members.length} Mitglieder)`}>
{tl.members.slice(0, 2).map((m) => (
<span key={m.userId} className="w-3.5 h-3.5 rounded-pill bg-accent/20 border border-panel text-[8px] font-bold text-accent flex items-center justify-center">
{getInitials(m.displayName || m.email)[0]}
</span>
))}
</div>
) : (tl.teamId || (tl.shares && tl.shares.length > 0)) ? (
<span title="Geteilt mit Team oder Benutzern">
<Share2 className="w-2.5 h-2.5 text-accent ml-0.5" />
</span>
) : null}
{(() => {
const isShared = (tl.shares && tl.shares.length > 0) || Boolean(tl.teamId) || (tl.members && tl.members.length > 1);
if (!isShared) return null;
const otherMembers = (tl.members || []).filter((m) => m.userId !== currentUser?.id);
if (otherMembers.length > 0) {
return (
<div className="flex items-center -space-x-1 ml-0.5" title={`Geteilt (${tl.members?.length || 1} Mitglieder)`}>
{otherMembers.slice(0, 2).map((m) => (
<span key={m.userId} className="w-3.5 h-3.5 rounded-pill bg-accent/20 border border-panel text-[8px] font-bold text-accent flex items-center justify-center">
{getInitials(m.displayName || m.email)[0]}
</span>
))}
</div>
);
}
return (
<span title="Geteilt mit Team oder Benutzern">
<Users className="w-2.5 h-2.5 text-accent ml-0.5" />
</span>
);
})()}
</button>
{selectedListId === tl.id && (
<button
@@ -905,55 +945,52 @@ export const TasksView: React.FC<TasksViewProps> = ({
</button>
</div>
{/* Right Header Section: Sharing, Avatar Stack & View Switcher */}
{/* Right Header Section: Sharing Badge, Icon-Only Button & View Switcher */}
<div className="flex items-center gap-2">
{currentSelectedList && (
<div className="flex items-center gap-1.5">
<button
type="button"
onClick={() => handleOpenShareModal(currentSelectedList)}
className="btn-action h-8 px-2.5 flex items-center gap-1.5 text-xs"
title={`Liste '${currentSelectedList.title}' mit Team oder Nutzern teilen`}
>
<Users className="w-3.5 h-3.5 text-accent" />
<span>Teilen</span>
</button>
{currentSelectedList && (() => {
const isOwner = !currentSelectedList.ownerId || currentSelectedList.ownerId === currentUser?.id;
const sharesCount = (currentSelectedList.shares?.length || 0) > 0
? currentSelectedList.shares.length
: (currentSelectedList.members?.filter((m) => m.userId !== currentSelectedList.ownerId).length || 0);
const isShared = sharesCount > 0 || Boolean(currentSelectedList.teamId) || ((currentSelectedList.members?.length || 0) > 1);
{/* Avatar Stack */}
{currentSelectedList.members && currentSelectedList.members.length > 0 ? (
<div
onClick={() => handleOpenShareModal(currentSelectedList)}
className="flex items-center -space-x-1.5 cursor-pointer hover:opacity-90 transition-opacity pl-0.5"
title={`Geteilt mit ${currentSelectedList.members.length} Mitgliedern (${currentSelectedList.teamName || 'Team'}). Klick zum Verwalten.`}
>
{currentSelectedList.members.slice(0, 3).map((m) => (
<div
key={m.userId}
className="w-7 h-7 rounded-pill border-2 border-panel bg-accent/20 text-accent flex items-center justify-center text-[10px] font-bold shadow-sm"
title={`${m.displayName || m.email} (${m.role})`}
return (
<div className="flex items-center gap-1.5">
{isShared && (
isOwner ? (
<button
type="button"
onClick={() => handleOpenShareModal(currentSelectedList)}
className="px-2 py-0.5 rounded text-[10px] font-semibold bg-accent/15 text-accent border border-accent/30 flex items-center gap-1 hover:bg-accent/25 hover:border-accent/50 transition-colors cursor-pointer"
title="Teilen-Menü öffnen: Geteilt mit Nutzern/Gruppen anzeigen & verwalten"
>
{getInitials(m.displayName || m.email)}
</div>
))}
{currentSelectedList.members.length > 3 && (
<div className="w-7 h-7 rounded-pill border-2 border-panel bg-accent text-on-accent flex items-center justify-center text-[10px] font-bold shadow-sm">
+{currentSelectedList.members.length - 3}
</div>
)}
</div>
) : (
<div
<Users className="w-2.5 h-2.5" />
<span>Geteilt ({sharesCount > 0 ? sharesCount : currentSelectedList.members?.length || 1})</span>
</button>
) : (
<button
type="button"
onClick={() => handleOpenShareModal(currentSelectedList)}
className="px-2 py-0.5 rounded text-[10px] font-medium border border-border text-muted flex items-center gap-1 bg-surface-subtle hover:bg-panel-elevated hover:text-main transition-colors cursor-pointer"
title="Teilen & Berechtigungen öffnen"
>
<Users className="w-2.5 h-2.5" />
<span>Geteilt von {currentSelectedList.ownerName || 'Eigentümer'}</span>
</button>
)
)}
<button
type="button"
onClick={() => handleOpenShareModal(currentSelectedList)}
className="flex items-center cursor-pointer pl-0.5"
title="Privat (nur du). Klick zum Teilen."
className="btn-icon"
title="Aufgabenliste teilen und Berechtigungen"
>
<div className="w-7 h-7 rounded-pill border border-dashed border-border bg-canvas text-muted hover:text-accent hover:border-accent flex items-center justify-center text-[11px] font-bold transition-colors">
🔒
</div>
</div>
)}
</div>
)}
<Share className="w-3.5 h-3.5" />
</button>
</div>
);
})()}
{/* View Switcher: List vs Kanban */}
<div className="flex border border-border rounded-btn bg-canvas overflow-hidden">
@@ -1291,7 +1328,7 @@ export const TasksView: React.FC<TasksViewProps> = ({
type="button"
onClick={() => {
setAddShareType('team');
setSelectedShareTargetId('');
setSelectedShareUser(null);
}}
className={`h-7 px-2.5 text-xs rounded-btn font-medium flex items-center gap-1 border transition-colors ${
addShareType === 'team'
@@ -1336,26 +1373,27 @@ export const TasksView: React.FC<TasksViewProps> = ({
))}
</select>
) : (
<select
value={selectedShareTargetId}
onChange={(e) => setSelectedShareTargetId(e.target.value)}
className="flex-1 h-8 px-2.5 input-field text-xs bg-panel"
>
<option value="">-- Benutzer auswählen --</option>
{safeCollaborators
.filter((c) => !(sharingTaskList.shares || []).some((s) => s.targetType === 'user' && s.targetId === c.id))
.map((c) => (
<option key={c.id} value={c.id}>
👤 {c.displayName || c.email} ({c.email})
</option>
))}
</select>
<div className="flex-1">
<UserAutocomplete
users={safeCollaborators}
selectedUser={selectedShareUser}
onSelectUser={setSelectedShareUser}
excludeUserIds={[
currentUser?.id || '',
...(sharingTaskList.shares || [])
.filter((s) => s.targetType === 'user')
.map((s) => s.targetId),
].filter(Boolean)}
disabled={shareLoading}
placeholder="Benutzer suchen (Name oder @Benutzername)…"
/>
</div>
)}
<button
type="button"
onClick={handleAddShare}
disabled={!selectedShareTargetId || shareLoading}
className="btn-primary h-8 px-3 text-xs flex items-center gap-1 disabled:opacity-50"
disabled={shareLoading || (addShareType === 'team' ? !selectedShareTargetId : !selectedShareUser)}
className="btn-primary h-8 px-3 text-xs flex items-center gap-1 disabled:opacity-50 shrink-0"
>
<Plus className="w-3.5 h-3.5" />
<span>Freigeben</span>