feat: unify sharing modal and clean up team share duplication in lists
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 57s

This commit is contained in:
sandrodach committed 2026-10-25 17:41:18 +01:00
1 parent d45b3cd0b3
commit 177b69234c
6 files changed
+558 -767

No files matched your search

+56 -215
View File
@@ -28,6 +28,7 @@ import {
} from 'lucide-react';
import { TaskList, Thought, SubTask, Team, User, TaskListShare, KanbanColumn } from '../../lib/types';
import { api, triggerHaptic } from '../../lib/api';
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
import { TaskDetailModal } from './TaskDetailModal';
import { ManageColumnsModal, DEFAULT_KANBAN_COLUMNS } from './ManageColumnsModal';
import { UserAutocomplete } from '../UserAutocomplete';
@@ -1233,222 +1234,62 @@ export const TasksView: React.FC<TasksViewProps> = ({
</div>
)}
{/* SHARE LIST MODAL */}
{/* UNIFIED SHARE LIST MODAL */}
{showShareListModal && sharingTaskList && (
<div className="fixed inset-0 modal-overlay z-50 flex items-center justify-center p-4">
<div className="w-full max-w-md panel-surface shadow-panel p-4 space-y-4">
<div className="flex items-center justify-between border-b border-border pb-2">
<div className="flex items-center gap-2">
<span className="text-base">{sharingTaskList.icon || '📋'}</span>
<div>
<h3 className="text-sm font-semibold text-main">
Liste teilen: {sharingTaskList.title}
</h3>
<p className="text-[11px] text-muted">
{sharingTaskList.shares && sharingTaskList.shares.length > 0
? `${sharingTaskList.shares.length} Freigabe(n) aktiv`
: 'Aktuell privat (nur für dich)'}
</p>
</div>
</div>
<button
type="button"
onClick={() => {
setShowShareListModal(false);
setSharingTaskList(null);
}}
className="text-muted hover:text-main"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="space-y-4">
{/* Active Shares */}
<div>
<label className="block text-xs font-semibold text-main mb-1.5">
Aktive Freigaben:
</label>
{sharingTaskList.shares && sharingTaskList.shares.length > 0 ? (
<div className="space-y-1.5 max-h-40 overflow-y-auto pr-1">
{sharingTaskList.shares.map((sh) => (
<div
key={sh.id}
className="p-2 bg-canvas border border-border rounded-card flex items-center justify-between text-xs"
>
<div className="flex items-center gap-2 min-w-0">
<div className="w-6 h-6 rounded-pill bg-accent/20 text-accent flex items-center justify-center text-xs flex-shrink-0">
{sh.targetType === 'team' ? (
<Users className="w-3.5 h-3.5" />
) : (
<UserIcon className="w-3.5 h-3.5" />
)}
</div>
<div className="min-w-0">
<p className="font-semibold text-main truncate">
{sh.targetName || (sh.targetType === 'team' ? 'Team' : sh.email || 'Benutzer')}
</p>
{sh.email && sh.email !== sh.targetName && (
<p className="text-[10px] text-muted truncate">{sh.email}</p>
)}
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<span className="badge-neutral text-[10px]">
{sh.targetType === 'team' ? 'Team' : 'Benutzer'}
</span>
{sharingTaskList.isOwner !== false && (
<button
type="button"
onClick={() => handleRemoveShare(sh.id)}
disabled={shareLoading}
className="text-muted hover:text-status-err p-1 transition-colors"
title="Freigabe entfernen"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
)}
</div>
</div>
))}
</div>
) : (
<div className="p-2.5 bg-canvas border border-border rounded-card text-xs text-muted flex items-center gap-2">
<Lock className="w-3.5 h-3.5 text-muted flex-shrink-0" />
<span>Diese Liste ist privat. Füge unten ein Team oder einen Benutzer hinzu.</span>
</div>
)}
</div>
{/* Add New Share */}
{sharingTaskList.isOwner !== false && (
<div className="space-y-2 pt-2 border-t border-border">
<label className="block text-xs font-semibold text-main">
Neue Freigabe hinzufügen:
</label>
<div className="flex items-center gap-1.5">
<button
type="button"
onClick={() => {
setAddShareType('team');
setSelectedShareUser(null);
}}
className={`h-7 px-2.5 text-xs rounded-btn font-medium flex items-center gap-1 border transition-colors ${
addShareType === 'team'
? 'bg-panel border-accent text-main'
: 'bg-canvas border-border text-muted hover:text-main'
}`}
>
<Users className="w-3 h-3 text-accent" />
<span>Team / Gruppe</span>
</button>
<button
type="button"
onClick={() => {
setAddShareType('user');
setSelectedShareTargetId('');
}}
className={`h-7 px-2.5 text-xs rounded-btn font-medium flex items-center gap-1 border transition-colors ${
addShareType === 'user'
? 'bg-panel border-accent text-main'
: 'bg-canvas border-border text-muted hover:text-main'
}`}
>
<UserIcon className="w-3 h-3 text-accent" />
<span>Einzelner Benutzer</span>
</button>
</div>
<div className="flex items-center gap-2">
{addShareType === 'team' ? (
<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="">-- Team / Gruppe auswählen --</option>
{safeTeams
.filter((t) => !(sharingTaskList.shares || []).some((s) => s.targetType === 'team' && s.targetId === t.id))
.map((t) => (
<option key={t.id} value={t.id}>
👥 {t.name} ({t.members?.length || 1} Mitglieder)
</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={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>
</button>
</div>
</div>
)}
{/* Persons with access summary */}
{sharingTaskList.members && sharingTaskList.members.length > 0 && (
<div className="space-y-1.5 pt-2 border-t border-border">
<p className="text-xs font-semibold text-main">
Personen mit Zugriff ({sharingTaskList.members.length}):
</p>
<div className="max-h-36 overflow-y-auto space-y-1 pr-1">
{sharingTaskList.members.map((m) => (
<div
key={m.userId}
className="p-1.5 bg-canvas border border-border rounded-card flex items-center justify-between text-xs"
>
<div className="flex items-center gap-2 min-w-0">
<div className="w-5 h-5 rounded-pill bg-accent/20 text-accent font-bold text-[9px] flex items-center justify-center flex-shrink-0">
{getInitials(m.displayName || m.email)}
</div>
<span className="font-medium text-main truncate">
{m.displayName || m.email}
</span>
</div>
<span className="badge-neutral text-[10px] flex-shrink-0">
{m.role === 'owner' ? 'Eigentümer' : m.role === 'editor' ? 'Bearbeiter' : 'Mitglied'}
</span>
</div>
))}
</div>
</div>
)}
</div>
<div className="flex justify-end pt-2 border-t border-border">
<button
type="button"
onClick={() => {
setShowShareListModal(false);
setSharingTaskList(null);
}}
className="btn-action text-xs"
>
Fertig
</button>
</div>
</div>
</div>
<UnifiedShareModal
isOpen={showShareListModal}
onClose={() => {
setShowShareListModal(false);
setSharingTaskList(null);
}}
title={`Aufgabenliste teilen: ${sharingTaskList.title}`}
resourceName={sharingTaskList.title}
resourceIcon={sharingTaskList.icon || '📋'}
isOwner={sharingTaskList.isOwner !== false}
currentUserId={currentUser?.id}
shares={(sharingTaskList.shares || []).map((sh) => ({
id: sh.id,
targetType: sh.targetType,
targetId: sh.targetId,
targetName: sh.targetName || (sh.targetType === 'team' ? 'Gruppe' : sh.email || 'Benutzer'),
targetEmail: sh.email,
permission: 'editor' as const,
role: 'editor',
createdAt: sh.createdAt,
}))}
teams={safeTeams}
collaborators={safeCollaborators}
allowRoleSelection={false}
onAddShare={async (targetType, targetId) => {
await api.addTaskListShare(sharingTaskList.id, {
targetType,
targetId,
});
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, shares: freshShares });
}
await loadData();
}}
onRemoveShare={async (shareId) => {
await api.removeTaskListShare(sharingTaskList.id, shareId);
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, shares: freshShares });
}
await loadData();
}}
/>
)}
{/* MANAGE COLUMNS MODAL */}