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

+75 -248
View File
@@ -16,6 +16,7 @@ import {
import { ListItem, SharedList, Space, Team, User, WSMessage } from '../../lib/types';
import { api, buildWebSocketURL, triggerHaptic } from '../../lib/api';
import { UserAutocomplete } from '../UserAutocomplete';
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
interface SharedListsViewProps {
onRefreshGlobal: () => void;
@@ -738,255 +739,81 @@ export const SharedListsView: React.FC<SharedListsViewProps> = ({ onRefreshGloba
</div>
) : null}
{/* Space & Shopping List Sharing Modal */}
{/* Unified Space & Shopping List Sharing Modal */}
{showShareModal && activeSpace && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs">
<div className="panel-surface w-full max-w-lg p-5 shadow-panel rounded-card border border-border space-y-4">
{/* Modal Header */}
<div className="flex items-center justify-between pb-2 border-b border-border">
<div className="flex items-center gap-2">
<Share2 className="w-5 h-5 text-accent" />
<h3 className="text-base font-bold text-main">
Einkaufsliste teilen: {activeSpace.name}
</h3>
</div>
<button
type="button"
onClick={() => {
setShowShareModal(false);
setShareError(null);
}}
className="btn-icon"
title="Schließen"
>
<X className="w-4 h-4" />
</button>
</div>
{shareError && (
<div className="p-2.5 bg-status-danger/10 border border-status-danger/30 text-status-danger rounded text-xs">
{shareError}
</div>
)}
{/* Sharing Form (Team / Gruppe vs. Einzelner Benutzer) */}
<form onSubmit={handleAddShare} 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={() => {
setAddShareType('team');
setInviteTarget('');
}}
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');
setSelectedTeamId('');
}}
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>
{addShareType === 'team' ? (
<div className="flex flex-col sm:flex-row gap-2">
<select
value={selectedTeamId}
onChange={(e) => setSelectedTeamId(e.target.value)}
className="input-field h-8 px-2.5 bg-input text-main border border-border rounded-btn focus:outline-none focus:border-accent text-xs flex-1 cursor-pointer"
>
<option value="" className="bg-panel text-muted">-- Team / Gruppe auswählen --</option>
{teams.map((t) => (
<option key={t.id} value={t.id} className="bg-panel text-main">
👥 {t.name} ({t.members?.length || 1} Mitglieder)
</option>
))}
</select>
<select
value={inviteRole}
onChange={(e) => setInviteRole(e.target.value as 'editor' | 'viewer')}
className="input-field h-8 px-2.5 bg-input text-main border border-border rounded-btn focus:outline-none focus:border-accent text-xs w-full sm:w-36 cursor-pointer"
>
<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={selectedShareUser}
onSelectUser={setSelectedShareUser}
excludeUserIds={[
currentUser?.id || '',
...(activeSpace.members || []).map((m) => m.userId),
...(activeSpace.shares || [])
.filter((s) => s.targetType === 'user')
.map((s) => s.targetId),
].filter(Boolean)}
disabled={invitingLoading}
placeholder="Benutzer suchen (Name oder @Benutzername)…"
/>
</div>
<select
value={inviteRole}
onChange={(e) => setInviteRole(e.target.value as 'editor' | 'viewer')}
className="input-field h-8 px-2.5 bg-input text-main border border-border rounded-btn focus:outline-none focus:border-accent text-xs w-full sm:w-36 cursor-pointer shrink-0"
>
<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={
invitingLoading ||
(addShareType === 'team' ? !selectedTeamId : !selectedShareUser)
}
className="btn-primary text-xs h-8 px-4 flex items-center gap-1.5"
>
<Plus className="w-3.5 h-3.5" />
<span>
{invitingLoading
? 'Wird geteilt…'
: addShareType === 'team'
? 'Team freigeben'
: 'Benutzer einladen'}
</span>
</button>
</div>
</form>
{/* Current Space Members & Team Shares List */}
<div className="space-y-2">
<div className="text-xs font-semibold text-muted uppercase tracking-wider">
Aktive Freigaben & Mitglieder (
{((activeSpace.shares || []).filter((s) => s.targetType === 'team').length) +
(activeSpace.members?.length || 1)}
)
</div>
<div className="space-y-1.5 max-h-56 overflow-y-auto">
{/* Team Shares */}
{(activeSpace.shares || [])
.filter((s) => s.targetType === 'team')
.map((sh) => {
const canRemove =
activeSpace.ownerId === currentUser?.id || currentUser?.role === 'admin';
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"
>
<div className="flex items-center gap-2 min-w-0">
<div className="w-7 h-7 rounded-btn bg-accent/20 text-accent flex items-center justify-center shrink-0">
<Users 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>{sh.targetName || 'Team'}</span>
<span className="badge-neutral text-[9px] uppercase px-1 py-0.2">Gruppe</span>
</div>
<div className="text-[11px] text-muted truncate">Freigabe für die Gruppe</div>
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<span className="text-[10px] px-2 py-0.5 rounded font-semibold bg-surface-2 text-muted">
{sh.role === 'editor' ? 'Bearbeiter' : 'Leser'}
</span>
{canRemove && (
<button
type="button"
onClick={() => handleRemoveMember(sh.id)}
disabled={invitingLoading}
className="btn-icon text-muted hover:text-status-danger p-1"
title="Team-Freigabe entfernen"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
)}
</div>
</div>
);
})}
{/* Individual Members */}
{(activeSpace.members || []).map((m) => {
const isOwner = m.role === 'owner' || m.userId === activeSpace.ownerId;
const isMe = m.userId === currentUser?.id;
const canRemove =
(activeSpace.ownerId === currentUser?.id || currentUser?.role === 'admin') &&
!isOwner;
return (
<div
key={m.userId}
className="p-2.5 bg-canvas border border-border rounded-btn flex items-center justify-between gap-3 text-xs"
>
<div className="flex items-center gap-2 min-w-0">
<div className="w-7 h-7 rounded-full bg-surface-2 flex items-center justify-center text-xs font-bold text-accent shrink-0">
{(m.displayName || m.email || '?')[0].toUpperCase()}
</div>
<div className="truncate">
<div className="font-medium text-main truncate">
{m.displayName || m.email} {isMe && <span className="text-muted">(Du)</span>}
</div>
<div className="text-[11px] text-muted truncate">{m.email}</div>
</div>
</div>
<div className="flex items-center gap-2 shrink-0">
<span
className={`text-[10px] px-2 py-0.5 rounded font-semibold ${
isOwner
? 'bg-accent/15 text-accent'
: 'bg-surface-2 text-muted'
}`}
>
{isOwner ? 'Eigentümer' : m.role === 'editor' ? 'Bearbeiter' : 'Leser'}
</span>
{canRemove && (
<button
type="button"
onClick={() => handleRemoveMember(m.userId)}
disabled={invitingLoading}
className="btn-icon text-muted hover:text-status-danger p-1"
title="Freigabe entfernen"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
)}
</div>
</div>
);
})}
</div>
</div>
</div>
</div>
<UnifiedShareModal
isOpen={showShareModal}
onClose={() => setShowShareModal(false)}
title={`Einkaufsliste teilen: ${activeSpace.name}`}
resourceName={activeSpace.name}
resourceIcon={activeSpace.icon || '🛒'}
isOwner={activeSpace.ownerId === currentUser?.id || currentUser?.role === 'admin'}
currentUserId={currentUser?.id}
shares={[
...(activeSpace.shares || [])
.filter((s) => s.targetType === 'team')
.map((s) => ({
id: s.id,
targetType: 'team' as const,
targetId: s.targetId,
targetName: s.targetName || 'Gruppe',
permission: s.role === 'viewer' ? ('viewer' as const) : ('editor' as const),
role: s.role,
createdAt: s.createdAt,
})),
...(activeSpace.members || [])
.filter((m) => m.userId !== activeSpace.ownerId)
.map((m) => ({
id: m.userId,
targetType: 'user' as const,
targetId: m.userId,
targetName: m.displayName || m.email,
targetEmail: m.email,
permission: m.role === 'viewer' ? ('viewer' as const) : ('editor' as const),
role: m.role,
createdAt: m.joinedAt,
})),
]}
teams={teams}
collaborators={collaborators}
allowRoleSelection={true}
onAddShare={async (targetType, targetId, permission) => {
if (targetType === 'team') {
await api.addSpaceMember(activeSpace.id, {
teamId: targetId,
role: permission,
});
} else {
await api.addSpaceMember(activeSpace.id, {
userId: targetId,
role: permission,
});
}
const [sp, freshShares] = await Promise.all([
api.getSpaces(),
api.getSpaceShares ? api.getSpaceShares(activeSpace.id).catch(() => []) : Promise.resolve([]),
]);
setSpaces(sp);
const updated = sp.find((s) => s.id === activeSpace.id);
if (updated) {
setActiveSpace({ ...updated, shares: freshShares });
}
onRefreshGlobal();
}}
onRemoveShare={async (shareId) => {
await api.removeSpaceMember(activeSpace.id, shareId);
const [sp, freshShares] = await Promise.all([
api.getSpaces(),
api.getSpaceShares ? api.getSpaceShares(activeSpace.id).catch(() => []) : Promise.resolve([]),
]);
setSpaces(sp);
const updated = sp.find((s) => s.id === activeSpace.id);
if (updated) {
setActiveSpace({ ...updated, shares: freshShares });
}
onRefreshGlobal();
}}
/>
)}
</div>
);
+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 */}
+47 -284
View File
@@ -54,6 +54,7 @@ import { BlockType, Document, DocumentBlock, DocLockInfo, DocShare, TagDetails,
import { api, getToken, triggerHaptic } from '../../lib/api';
import { CodeBlockItem } from './CodeBlockItem';
import { UserAutocomplete } from '../UserAutocomplete';
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
interface WorkspaceViewProps {
currentUser?: User | null;
@@ -4328,291 +4329,53 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</div>
)}
{/* ===================================================================== */}
{/* DOCUMENT SHARING MODAL */}
{/* ===================================================================== */}
{/* UNIFIED DOCUMENT SHARING MODAL */}
{showShareModal && selectedDoc && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs">
<div className="panel-surface w-full max-w-lg p-5 shadow-panel rounded-card border border-border space-y-4">
{/* Modal Header */}
<div className="flex items-center justify-between pb-2 border-b border-border">
<div className="flex items-center gap-2">
<Users className="w-5 h-5 text-accent" />
<h3 className="text-base font-bold text-main">Notiz teilen & Berechtigungen</h3>
</div>
<button
type="button"
onClick={() => setShowShareModal(false)}
className="btn-icon"
title="Schließen"
>
<X className="w-4 h-4" />
</button>
</div>
<div className="text-xs text-muted">
Dokument: <span className="font-semibold text-main">{selectedDoc.title || 'Unbenannte Notiz'}</span>
</div>
{shareError && (
<div className="p-2.5 bg-status-danger/10 border border-status-danger/30 text-status-danger rounded text-xs">
{shareError}
</div>
)}
{/* If current user is owner: can add shares and manage permissions */}
{selectedDoc.isOwner !== false ? (
<div className="space-y-4">
<form onSubmit={handleAddShare} 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={() => {
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');
setSelectedShareTeamId('');
}}
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>
{addShareType === 'team' ? (
<div className="flex flex-col sm:flex-row gap-2">
<select
value={selectedShareTeamId}
onChange={(e) => setSelectedShareTeamId(e.target.value)}
className="input-field h-8 px-2.5 bg-input text-main border border-border rounded-btn focus:outline-none focus:border-accent text-xs flex-1 cursor-pointer"
>
<option value="" className="bg-panel text-muted">-- Team / Gruppe auswählen --</option>
{shareTeams.map((t) => (
<option key={t.id} value={t.id} className="bg-panel text-main">
👥 {t.name} ({t.members?.length || 1} Mitglieder)
</option>
))}
</select>
<select
value={sharePermission}
onChange={(e) => setSharePermission(e.target.value as 'read' | 'write')}
className="input-field h-8 px-2.5 bg-input text-main border border-border rounded-btn focus:outline-none focus:border-accent text-xs w-full sm:w-40 cursor-pointer"
>
<option value="read" className="bg-panel text-main">Nur Lesen (read)</option>
<option value="write" className="bg-panel text-main">Schreiben (write)</option>
</select>
</div>
) : (
<div className="flex flex-col sm:flex-row gap-2">
<div className="flex-1">
<UserAutocomplete
users={collaborators}
selectedUser={selectedShareUser}
onSelectUser={setSelectedShareUser}
excludeUserIds={[
currentUser?.id || '',
...docShares.map((s) => s.targetId || s.userId).filter(Boolean),
]}
disabled={sharingLoading}
placeholder="Benutzer suchen (Name oder @Benutzername)…"
/>
</div>
<select
value={sharePermission}
onChange={(e) => setSharePermission(e.target.value as 'read' | 'write')}
className="input-field h-8 px-2.5 bg-input text-main border border-border rounded-btn focus:outline-none focus:border-accent text-xs w-full sm:w-40 cursor-pointer shrink-0"
>
<option value="read" className="bg-panel text-main">Nur Lesen (read)</option>
<option value="write" className="bg-panel text-main">Schreiben (write)</option>
</select>
</div>
)}
<div className="flex justify-end pt-1">
<button
type="submit"
disabled={
sharingLoading ||
(addShareType === 'team' ? !selectedShareTeamId : !selectedShareUser)
}
className="btn-primary h-8 px-3 text-xs flex items-center gap-1.5"
>
<Plus className="w-3.5 h-3.5" />
<span>
{sharingLoading
? 'Wird geteilt…'
: addShareType === 'team'
? 'Team freigeben'
: 'Benutzer freigeben'}
</span>
</button>
</div>
</form>
{/* List of current shares */}
<div className="space-y-2">
<div className="text-xs font-semibold text-main">Aktuelle Freigaben ({docShares.length})</div>
{docShares.length === 0 ? (
<div className="p-3 bg-panel-elevated border border-border rounded-card text-xs text-muted text-center">
Diese Notiz ist bisher noch mit niemandem geteilt.
</div>
) : (
<div className="space-y-1.5 max-h-56 overflow-y-auto">
{docShares.map((share) => {
const isTeam = share.targetType === 'team' || Boolean((share as any).teamId) || String(share.targetId || '').startsWith('team_');
const targetKey = share.targetId || share.userId || share.id;
return (
<div
key={share.id || targetKey}
className="p-2.5 bg-panel-elevated border border-border rounded-card flex items-center justify-between gap-3 text-xs"
>
<div className="flex items-center gap-2 min-w-0">
{isTeam ? (
<div className="w-6 h-6 rounded-btn bg-accent/20 text-accent flex items-center justify-center shrink-0">
<Users className="w-3.5 h-3.5" />
</div>
) : (
<div className="w-6 h-6 rounded-full bg-accent/20 text-accent font-bold text-[10px] flex items-center justify-center shrink-0">
{(share.displayName || share.userEmail || share.targetName || '?')[0].toUpperCase()}
</div>
)}
<div className="min-w-0">
<div className="font-semibold text-main truncate flex items-center gap-1.5">
<span>{share.displayName || share.targetName || share.userEmail || share.userId}</span>
{isTeam && (
<span className="badge-neutral text-[9px] uppercase px-1 py-0.2">Gruppe</span>
)}
</div>
{!isTeam && share.userEmail && share.displayName && (
<div className="text-[11px] text-muted truncate">{share.userEmail}</div>
)}
</div>
</div>
<div className="flex items-center gap-2 flex-shrink-0">
{!isTeam && (
<select
value={share.permission}
onChange={(e) => handleUpdateSharePerm(share.userId, e.target.value as 'read' | 'write')}
className="input-field h-7 px-2 bg-input text-main border border-border rounded-btn focus:outline-none focus:border-accent text-[11px] cursor-pointer"
disabled={sharingLoading}
>
<option value="read" className="bg-panel text-main">Nur Lesen</option>
<option value="write" className="bg-panel text-main">Schreiben</option>
</select>
)}
{isTeam && (
<span className="badge-neutral text-[10px]">
{share.permission === 'write' ? 'Schreiben' : 'Nur Lesen'}
</span>
)}
<button
type="button"
onClick={() => handleRemoveShare(targetKey)}
className="btn-icon btn-icon-danger h-7 w-7"
title="Freigabe entziehen"
disabled={sharingLoading}
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
</div>
);
})}
</div>
)}
</div>
</div>
) : (
/* If current user is a collaborator */
<div className="space-y-3">
{/* Document & Owner Card */}
<div className="p-3 bg-panel-elevated border border-border rounded-card space-y-2">
<div className="flex items-center justify-between text-xs">
<span className="text-muted">Eigentümer der Notiz:</span>
<span className="font-semibold text-main flex items-center gap-1.5">
<span className="w-5 h-5 rounded-full bg-accent/20 text-accent text-[10px] font-bold flex items-center justify-center">
{(selectedDoc.ownerName || 'U').slice(0, 1).toUpperCase()}
</span>
<span>{selectedDoc.ownerName || 'Unbekannt'}</span>
</span>
</div>
</div>
{/* Permission Card */}
{selectedDoc.permission === 'write' ? (
<div className="p-3 bg-emerald-500/10 border border-emerald-500/30 rounded-card flex items-start gap-3">
<div className="w-8 h-8 rounded-btn bg-emerald-500/20 text-emerald-600 dark:text-emerald-400 flex items-center justify-center flex-shrink-0 mt-0.5">
<Edit3 className="w-4 h-4" />
</div>
<div className="text-xs space-y-0.5">
<div className="font-semibold text-emerald-600 dark:text-emerald-400">Schreibzugriff aktiv</div>
<div className="text-muted">
Du hast Bearbeitungsrechte für diese Notiz und kannst Inhalte in Echtzeit anpassen.
</div>
</div>
</div>
) : (
<div className="p-3 bg-accent/10 border border-accent/30 rounded-card flex items-start gap-3">
<div className="w-8 h-8 rounded-btn bg-accent/20 text-accent flex items-center justify-center flex-shrink-0 mt-0.5">
<Eye className="w-4 h-4" />
</div>
<div className="text-xs space-y-0.5">
<div className="font-semibold text-accent">Nur Lesezugriff</div>
<div className="text-muted">
Du kannst diese Notiz ansehen und durchsuchen, aber keine Änderungen an den Inhalten vornehmen.
</div>
</div>
</div>
)}
{/* Action: Leave Share */}
<div className="pt-2">
<button
type="button"
onClick={handleLeaveShare}
className="w-full btn-action h-8 px-3 text-xs text-status-err hover:bg-status-err-bg border border-status-err-border rounded-btn flex items-center justify-center gap-1.5 transition-colors"
>
<Trash2 className="w-3.5 h-3.5" />
<span>Notiz aus meinem Workspace entfernen</span>
</button>
</div>
</div>
)}
<div className="flex justify-end pt-2 border-t border-border">
<button
type="button"
onClick={() => setShowShareModal(false)}
className="btn-action h-8 px-4 text-xs font-medium"
>
Schließen
</button>
</div>
</div>
</div>
<UnifiedShareModal
isOpen={showShareModal}
onClose={() => setShowShareModal(false)}
title={`Notiz teilen: ${selectedDoc.title || 'Unbenannte Notiz'}`}
resourceName={selectedDoc.title || 'Unbenannte Notiz'}
resourceIcon={selectedDoc.icon || '📄'}
isOwner={selectedDoc.isOwner !== false}
currentUserId={currentUser?.id}
shares={(docShares || []).map((sh) => ({
id: sh.id,
targetType: sh.targetType || (sh.teamId ? 'team' : 'user'),
targetId: sh.targetId || sh.teamId || sh.userId || '',
targetName: sh.targetName || sh.displayName || (sh.targetType === 'team' || sh.teamId ? 'Gruppe' : sh.userEmail || 'Benutzer'),
targetEmail: sh.userEmail,
permission: sh.permission === 'read' ? ('viewer' as const) : ('editor' as const),
role: sh.permission === 'read' ? 'viewer' : 'editor',
createdAt: sh.createdAt,
}))}
teams={shareTeams}
collaborators={collaborators}
allowRoleSelection={true}
onAddShare={async (targetType, targetId, permission) => {
const perm = permission === 'viewer' ? 'read' : 'write';
if (targetType === 'team') {
await api.createDocShare(selectedDoc.id, {
teamId: targetId,
permission: perm,
});
} else {
await api.createDocShare(selectedDoc.id, {
userId: targetId,
permission: perm,
});
}
const shares = await api.getDocShares(selectedDoc.id);
setDocShares(shares);
await loadDocs(selectedDoc.id);
}}
onRemoveShare={async (shareId) => {
await api.deleteDocShare(selectedDoc.id, shareId);
const shares = await api.getDocShares(selectedDoc.id);
setDocShares(shares);
await loadDocs(selectedDoc.id);
}}
/>
)}
{/* LINK INSERTION MODAL (/link, /notiz, /web) */}