feat: unify sharing UI, redesign lists header, fix team sharing, and add user autocomplete
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 18s

This commit is contained in:
sandrodach committed 2026-10-25 16:04:08 +01:00
1 parent 6d07352e80
commit 354129b2d4
10 files changed
+1049 -393

No files matched your search

@@ -53,6 +53,7 @@ import {
import { BlockType, Document, DocumentBlock, DocLockInfo, DocShare, TagDetails, TagSummary, Team, User } from '../../lib/types';
import { api, getToken, triggerHaptic } from '../../lib/api';
import { CodeBlockItem } from './CodeBlockItem';
import { UserAutocomplete } from '../UserAutocomplete';
interface WorkspaceViewProps {
currentUser?: User | null;
@@ -182,7 +183,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
const [shareTeams, setShareTeams] = useState<Team[]>([]);
const [addShareType, setAddShareType] = useState<'team' | 'user'>('team');
const [selectedShareTeamId, setSelectedShareTeamId] = useState('');
const [shareTargetEmailOrId, setShareTargetEmailOrId] = useState('');
const [selectedShareUser, setSelectedShareUser] = useState<User | null>(null);
const [sharePermission, setSharePermission] = useState<'read' | 'write'>('read');
const [sharingLoading, setSharingLoading] = useState(false);
const [shareError, setShareError] = useState<string | null>(null);
@@ -625,10 +626,12 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
setShowShareModal(true);
setShareError(null);
setSharingLoading(true);
setSelectedShareUser(null);
setSelectedShareTeamId('');
try {
const [sharesList, usersList, teamsList] = await Promise.all([
api.getDocShares(selectedDoc.id).catch(() => []),
api.getCollaborators().catch(() => []),
api.getUsersDirectory ? api.getUsersDirectory().catch(() => api.getCollaborators().catch(() => [])) : api.getCollaborators().catch(() => []),
api.getUserTeams().catch(() => []),
]);
setDocShares(sharesList);
@@ -657,19 +660,16 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
});
setSelectedShareTeamId('');
} else {
const target = shareTargetEmailOrId.trim();
if (!target) {
setShareError('Bitte einen Benutzer auswählen oder E-Mail eingeben');
if (!selectedShareUser) {
setShareError('Bitte einen bestehenden Benutzer auswählen');
setSharingLoading(false);
return;
}
const isId = collaborators.some((u) => u.id === target);
await api.createDocShare(selectedDoc.id, {
userId: isId ? target : undefined,
email: !isId ? target : undefined,
userId: selectedShareUser.id,
permission: sharePermission,
});
setShareTargetEmailOrId('');
setSelectedShareUser(null);
}
const sharesList = await api.getDocShares(selectedDoc.id).catch(() => []);
@@ -715,18 +715,18 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
}
};
const handleRemoveShare = async (targetUserId: string) => {
const handleRemoveShare = async (targetId: string) => {
if (!selectedDoc) return;
if (!window.confirm('Freigabe für diesen Benutzer wirklich aufheben?')) return;
if (!window.confirm('Freigabe wirklich aufheben?')) return;
setSharingLoading(true);
try {
await api.deleteDocShare(selectedDoc.id, targetUserId);
setDocShares((prev) => prev.filter((s) => s.userId !== targetUserId));
await api.deleteDocShare(selectedDoc.id, targetId);
setDocShares((prev) => prev.filter((s) => (s.targetId || s.userId || s.id) !== targetId && s.userId !== targetId));
setSelectedDoc((prev) =>
prev
? {
...prev,
sharedWith: (prev.sharedWith || []).filter((s) => s.userId !== targetUserId),
sharedWith: (prev.sharedWith || []).filter((s) => (s.targetId || s.userId || s.id) !== targetId && s.userId !== targetId),
}
: prev
);
@@ -4403,7 +4403,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
type="button"
onClick={() => {
setAddShareType('team');
setShareTargetEmailOrId('');
setSelectedShareUser(null);
}}
className={`h-7 px-2.5 text-xs rounded-btn font-medium flex items-center gap-1 border transition-colors ${
addShareType === 'team'
@@ -4456,42 +4456,28 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</select>
</div>
) : (
<div className="space-y-2">
<div className="flex flex-col sm:flex-row gap-2">
<select
value={shareTargetEmailOrId}
onChange={(e) => setShareTargetEmailOrId(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">Nutzer auswählen oder Email eingeben…</option>
{collaborators
.filter((u) => u.id !== currentUser?.id && !docShares.some((s) => s.userId === u.id))
.map((u) => (
<option key={u.id} value={u.id} className="bg-panel text-main">
👤 {u.displayName || u.email} ({u.email})
</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>
{/* Direct Email fallback if user not in list */}
{(!shareTargetEmailOrId || !collaborators.some((u) => u.id === shareTargetEmailOrId)) && (
<input
type="email"
placeholder="Oder E-Mail-Adresse direkt eingeben…"
value={shareTargetEmailOrId}
onChange={(e) => setShareTargetEmailOrId(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 w-full"
<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>
)}
@@ -4500,7 +4486,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
type="submit"
disabled={
sharingLoading ||
(addShareType === 'team' ? !selectedShareTeamId : !shareTargetEmailOrId.trim())
(addShareType === 'team' ? !selectedShareTeamId : !selectedShareUser)
}
className="btn-primary h-8 px-3 text-xs flex items-center gap-1.5"
>
@@ -4525,43 +4511,68 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
</div>
) : (
<div className="space-y-1.5 max-h-56 overflow-y-auto">
{docShares.map((share) => (
<div
key={share.id || share.userId}
className="p-2.5 bg-panel-elevated border border-border rounded-card flex items-center justify-between gap-3 text-xs"
>
<div className="min-w-0">
<div className="font-semibold text-main truncate">
{share.displayName || share.userEmail || share.userId}
{docShares.map((share) => {
const isTeam = share.targetType === 'team' || Boolean(share.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>
{share.userEmail && share.displayName && (
<div className="text-[11px] text-muted truncate">{share.userEmail}</div>
)}
</div>
<div className="flex items-center gap-2 flex-shrink-0">
<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>
<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(share.userId)}
className="btn-icon btn-icon-danger h-7 w-7"
title="Freigabe entziehen"
disabled={sharingLoading}
>
<Trash2 className="w-3.5 h-3.5" />
</button>
<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>