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
@@ -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) */}
|
||||
|
||||
Reference in new issue
Block a user