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
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 57s
This commit is contained in:
1 parent
d45b3cd0b3
commit
177b69234c
6 files changed
+558
-767
No files matched your search
@@ -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 */}
|
||||
|
||||
Reference in new issue
Block a user