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
@@ -0,0 +1,356 @@
|
||||
import React, { useState } from 'react';
|
||||
import {
|
||||
Users,
|
||||
User as UserIcon,
|
||||
X,
|
||||
Plus,
|
||||
Trash2,
|
||||
Lock,
|
||||
Share2,
|
||||
Check,
|
||||
} from 'lucide-react';
|
||||
import { User, Team, UnifiedShare } from '../lib/types';
|
||||
import { UserAutocomplete } from '../features/UserAutocomplete';
|
||||
|
||||
export interface UnifiedShareModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
title?: string;
|
||||
resourceName: string;
|
||||
resourceIcon?: string | React.ReactNode;
|
||||
isOwner: boolean;
|
||||
currentUserId?: string;
|
||||
shares: UnifiedShare[];
|
||||
teams: Team[];
|
||||
collaborators: User[];
|
||||
allowRoleSelection?: boolean; // Default true (editor vs viewer)
|
||||
onAddShare: (targetType: 'team' | 'user', targetId: string, permission: 'editor' | 'viewer') => Promise<void>;
|
||||
onRemoveShare: (shareId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export const UnifiedShareModal: React.FC<UnifiedShareModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
title = 'Freigeben & Berechtigungen',
|
||||
resourceName,
|
||||
resourceIcon,
|
||||
isOwner,
|
||||
currentUserId,
|
||||
shares,
|
||||
teams,
|
||||
collaborators,
|
||||
allowRoleSelection = true,
|
||||
onAddShare,
|
||||
onRemoveShare,
|
||||
}) => {
|
||||
const [shareType, setShareType] = useState<'team' | 'user'>('team');
|
||||
const [selectedTeamId, setSelectedTeamId] = useState('');
|
||||
const [selectedUser, setSelectedUser] = useState<User | null>(null);
|
||||
const [selectedPermission, setSelectedPermission] = useState<'editor' | 'viewer'>('editor');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleAdd = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
setError(null);
|
||||
const targetId = shareType === 'team' ? selectedTeamId : selectedUser?.id;
|
||||
if (!targetId) {
|
||||
setError(shareType === 'team' ? 'Bitte eine Gruppe auswählen' : 'Bitte einen Benutzer auswählen');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
await onAddShare(shareType, targetId, selectedPermission);
|
||||
setSelectedTeamId('');
|
||||
setSelectedUser(null);
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'Fehler beim Hinzufügen der Freigabe');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRemove = async (shareId: string) => {
|
||||
if (!window.confirm('Möchtest du diese Freigabe wirklich aufheben?')) return;
|
||||
setError(null);
|
||||
setLoading(true);
|
||||
try {
|
||||
await onRemoveShare(shareId);
|
||||
} catch (err: any) {
|
||||
setError(err?.message || 'Fehler beim Entfernen der Freigabe');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const excludedUserIds = [
|
||||
...(currentUserId ? [currentUserId] : []),
|
||||
...shares.filter((s) => s.targetType === 'user').map((s) => s.targetId),
|
||||
];
|
||||
|
||||
const availableTeams = teams.filter(
|
||||
(t) => !shares.some((s) => s.targetType === 'team' && s.targetId === t.id)
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs"
|
||||
onClick={(e) => {
|
||||
if (e.target === e.currentTarget) onClose();
|
||||
}}
|
||||
>
|
||||
<div className="panel-surface w-full max-w-lg p-5 shadow-panel rounded-card border border-border space-y-4 animate-in fade-in zoom-in-95 duration-150">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between pb-3 border-b border-border">
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<div className="w-8 h-8 rounded-btn bg-accent/15 text-accent flex items-center justify-center shrink-0">
|
||||
{resourceIcon ? (
|
||||
typeof resourceIcon === 'string' ? (
|
||||
<span className="text-base">{resourceIcon}</span>
|
||||
) : (
|
||||
resourceIcon
|
||||
)
|
||||
) : (
|
||||
<Share2 className="w-4 h-4" />
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
<h3 className="text-sm font-bold text-main truncate">{title}</h3>
|
||||
<p className="text-[11px] text-muted truncate">
|
||||
Objekt: <span className="font-semibold text-main">{resourceName}</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="p-1.5 text-muted hover:text-main rounded-btn hover:bg-panel transition-colors cursor-pointer"
|
||||
title="Schließen"
|
||||
>
|
||||
<X className="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="p-2.5 bg-status-danger/10 border border-status-danger/30 text-status-danger rounded text-xs">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Add Share Section (Only if owner / manager) */}
|
||||
{isOwner ? (
|
||||
<form onSubmit={handleAdd} className="space-y-3 p-3 bg-panel-elevated border border-border rounded-card">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-xs font-semibold text-main">Freigabe hinzufügen:</span>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShareType('team');
|
||||
setSelectedUser(null);
|
||||
setError(null);
|
||||
}}
|
||||
className={`h-7 px-2.5 text-xs rounded-btn font-medium flex items-center gap-1.5 border transition-colors cursor-pointer ${
|
||||
shareType === 'team'
|
||||
? 'bg-panel border-accent text-accent font-semibold shadow-xs'
|
||||
: 'bg-canvas border-border text-muted hover:text-main'
|
||||
}`}
|
||||
>
|
||||
<Users className="w-3.5 h-3.5" />
|
||||
<span>Gruppe</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setShareType('user');
|
||||
setSelectedTeamId('');
|
||||
setError(null);
|
||||
}}
|
||||
className={`h-7 px-2.5 text-xs rounded-btn font-medium flex items-center gap-1.5 border transition-colors cursor-pointer ${
|
||||
shareType === 'user'
|
||||
? 'bg-panel border-accent text-accent font-semibold shadow-xs'
|
||||
: 'bg-canvas border-border text-muted hover:text-main'
|
||||
}`}
|
||||
>
|
||||
<UserIcon className="w-3.5 h-3.5" />
|
||||
<span>Benutzer</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{shareType === 'team' ? (
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<select
|
||||
value={selectedTeamId}
|
||||
onChange={(e) => setSelectedTeamId(e.target.value)}
|
||||
className="input-field bg-panel h-8 px-2.5 text-xs flex-1 cursor-pointer"
|
||||
disabled={loading}
|
||||
>
|
||||
<option value="" className="bg-panel text-muted">-- Gruppe auswählen --</option>
|
||||
{availableTeams.map((t) => (
|
||||
<option key={t.id} value={t.id} className="bg-panel text-main">
|
||||
👥 {t.name} ({t.members?.length || 1} Mitglieder)
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
{allowRoleSelection && (
|
||||
<select
|
||||
value={selectedPermission}
|
||||
onChange={(e) => setSelectedPermission(e.target.value as 'editor' | 'viewer')}
|
||||
className="input-field bg-panel h-8 px-2.5 text-xs w-full sm:w-36 cursor-pointer shrink-0"
|
||||
disabled={loading}
|
||||
>
|
||||
<option value="editor" className="bg-panel text-main">Bearbeiten</option>
|
||||
<option value="viewer" className="bg-panel text-main">Nur Lesen</option>
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col sm:flex-row gap-2">
|
||||
<div className="flex-1">
|
||||
<UserAutocomplete
|
||||
users={collaborators}
|
||||
selectedUser={selectedUser}
|
||||
onSelectUser={setSelectedUser}
|
||||
excludeUserIds={excludedUserIds}
|
||||
disabled={loading}
|
||||
placeholder="Benutzer suchen (Name oder @Benutzername)…"
|
||||
/>
|
||||
</div>
|
||||
{allowRoleSelection && (
|
||||
<select
|
||||
value={selectedPermission}
|
||||
onChange={(e) => setSelectedPermission(e.target.value as 'editor' | 'viewer')}
|
||||
className="input-field bg-panel h-8 px-2.5 text-xs w-full sm:w-36 cursor-pointer shrink-0"
|
||||
disabled={loading}
|
||||
>
|
||||
<option value="editor" className="bg-panel text-main">Bearbeiten</option>
|
||||
<option value="viewer" className="bg-panel text-main">Nur Lesen</option>
|
||||
</select>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end pt-1">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loading || (shareType === 'team' ? !selectedTeamId : !selectedUser)}
|
||||
className="btn-primary text-xs h-8 px-3.5 flex items-center gap-1.5 cursor-pointer disabled:opacity-50"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
<span>
|
||||
{loading
|
||||
? 'Wird gespeichert…'
|
||||
: shareType === 'team'
|
||||
? 'Gruppe freigeben'
|
||||
: 'Benutzer hinzufügen'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="p-3 bg-panel-elevated border border-border rounded-card text-xs text-muted flex items-center gap-2">
|
||||
<Lock className="w-4 h-4 text-faint shrink-0" />
|
||||
<span>Nur der Eigentümer kann neue Freigaben einrichten.</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Active Shares List */}
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between text-xs font-semibold text-muted uppercase tracking-wider px-0.5">
|
||||
<span>Aktive Freigaben</span>
|
||||
<span className="font-mono text-[11px] font-normal lowercase">
|
||||
{shares.length} {shares.length === 1 ? 'Freigabe' : 'Freigaben'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5 max-h-56 overflow-y-auto pr-0.5">
|
||||
{shares.length === 0 ? (
|
||||
<div className="p-4 bg-canvas border border-border rounded-card text-xs text-muted flex items-center gap-2.5 justify-center text-center">
|
||||
<Lock className="w-4 h-4 text-faint shrink-0" />
|
||||
<span>Dieses Element ist aktuell privat (nur für dich sichtbar).</span>
|
||||
</div>
|
||||
) : (
|
||||
shares.map((sh) => {
|
||||
const isTeam = sh.targetType === 'team';
|
||||
const roleLabel =
|
||||
sh.permission === 'viewer' || sh.role === 'viewer' ? 'Leser' : 'Bearbeiter';
|
||||
|
||||
return (
|
||||
<div
|
||||
key={sh.id}
|
||||
className="p-2.5 bg-canvas border border-border rounded-btn flex items-center justify-between gap-3 text-xs hover:border-border/80 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
<div
|
||||
className={`w-7 h-7 rounded-btn flex items-center justify-center shrink-0 ${
|
||||
isTeam
|
||||
? 'bg-accent/15 text-accent'
|
||||
: 'bg-surface-2 text-main border border-border'
|
||||
}`}
|
||||
>
|
||||
{isTeam ? <Users className="w-3.5 h-3.5" /> : <UserIcon className="w-3.5 h-3.5" />}
|
||||
</div>
|
||||
<div className="truncate">
|
||||
<div className="font-medium text-main truncate flex items-center gap-1.5">
|
||||
<span className="truncate">{sh.targetName || (isTeam ? 'Gruppe' : sh.targetEmail || 'Benutzer')}</span>
|
||||
<span
|
||||
className={`text-[9px] uppercase font-semibold px-1 py-0.2 rounded ${
|
||||
isTeam ? 'badge-neutral' : 'bg-surface-2 text-faint'
|
||||
}`}
|
||||
>
|
||||
{isTeam ? 'Gruppe' : 'Benutzer'}
|
||||
</span>
|
||||
</div>
|
||||
{sh.targetEmail && sh.targetEmail !== sh.targetName && (
|
||||
<div className="text-[11px] text-muted truncate">{sh.targetEmail}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
<span
|
||||
className={`text-[10px] px-2 py-0.5 rounded font-semibold ${
|
||||
roleLabel === 'Bearbeiter'
|
||||
? 'bg-accent/15 text-accent border border-accent/30'
|
||||
: 'bg-surface-2 text-muted border border-border'
|
||||
}`}
|
||||
>
|
||||
{roleLabel}
|
||||
</span>
|
||||
{isOwner && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleRemove(sh.id)}
|
||||
disabled={loading}
|
||||
className="p-1 text-faint hover:text-status-err rounded transition-colors cursor-pointer"
|
||||
title="Freigabe entfernen"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="pt-2 border-t border-border flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
className="btn-action text-xs h-8 px-4 cursor-pointer"
|
||||
>
|
||||
Schließen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in new issue
Block a user