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
MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 18s
This commit is contained in:
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>
|
||||
|
||||
Reference in new issue
Block a user