feat(sharing): remove code redeem, unify Teilen button and team/user sharing across lists and notes
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 58s

This commit is contained in:
sandrodach committed 2026-10-25 13:51:42 +01:00
1 parent 20c50aa8d6
commit 7c47f245e2
6 files changed
+440 -226

No files matched your search

+145 -52
View File
@@ -48,8 +48,9 @@ import {
Share,
Share2,
Hash,
User as UserIcon,
} from 'lucide-react';
import { BlockType, Document, DocumentBlock, DocLockInfo, DocShare, TagDetails, TagSummary, User } from '../../lib/types';
import { BlockType, Document, DocumentBlock, DocLockInfo, DocShare, TagDetails, TagSummary, Team, User } from '../../lib/types';
import { api, getToken, triggerHaptic } from '../../lib/api';
import { CodeBlockItem } from './CodeBlockItem';
@@ -165,6 +166,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
const [showShareModal, setShowShareModal] = useState(false);
const [docShares, setDocShares] = useState<DocShare[]>([]);
const [collaborators, setCollaborators] = useState<User[]>([]);
const [shareTeams, setShareTeams] = useState<Team[]>([]);
const [addShareType, setAddShareType] = useState<'team' | 'user'>('team');
const [selectedShareTeamId, setSelectedShareTeamId] = useState('');
const [shareTargetEmailOrId, setShareTargetEmailOrId] = useState('');
const [sharePermission, setSharePermission] = useState<'read' | 'write'>('read');
const [sharingLoading, setSharingLoading] = useState(false);
@@ -569,12 +573,14 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
setShareError(null);
setSharingLoading(true);
try {
const [sharesList, usersList] = await Promise.all([
const [sharesList, usersList, teamsList] = await Promise.all([
api.getDocShares(selectedDoc.id).catch(() => []),
api.getCollaborators().catch(() => []),
api.getUserTeams().catch(() => []),
]);
setDocShares(sharesList);
setCollaborators(usersList);
setShareTeams(teamsList);
} finally {
setSharingLoading(false);
}
@@ -583,27 +589,43 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
const handleAddShare = async (e: React.FormEvent) => {
e.preventDefault();
if (!selectedDoc) return;
const target = shareTargetEmailOrId.trim();
if (!target) return;
setSharingLoading(true);
setShareError(null);
try {
const isId = collaborators.some((u) => u.id === target);
const newShare = await api.createDocShare(selectedDoc.id, {
userId: isId ? target : undefined,
email: !isId ? target : undefined,
permission: sharePermission,
});
setDocShares((prev) => [...prev.filter((s) => s.userId !== newShare.userId), newShare]);
setShareTargetEmailOrId('');
if (addShareType === 'team') {
if (!selectedShareTeamId) {
setShareError('Bitte ein Team oder eine Gruppe auswählen');
setSharingLoading(false);
return;
}
await api.createDocShare(selectedDoc.id, {
teamId: selectedShareTeamId,
permission: sharePermission,
});
setSelectedShareTeamId('');
} else {
const target = shareTargetEmailOrId.trim();
if (!target) {
setShareError('Bitte einen Benutzer auswählen oder E-Mail eingeben');
setSharingLoading(false);
return;
}
const isId = collaborators.some((u) => u.id === target);
await api.createDocShare(selectedDoc.id, {
userId: isId ? target : undefined,
email: !isId ? target : undefined,
permission: sharePermission,
});
setShareTargetEmailOrId('');
}
const sharesList = await api.getDocShares(selectedDoc.id).catch(() => []);
setDocShares(sharesList);
setSelectedDoc((prev) =>
prev
? {
...prev,
sharedWith: [
...(prev.sharedWith || []).filter((s) => s.userId !== newShare.userId),
newShare,
],
sharedWith: sharesList,
}
: prev
);
@@ -3608,52 +3630,123 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{/* 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-2.5 p-3 bg-panel-elevated border border-border rounded-card">
<div className="text-xs font-semibold text-main">Neuen Nutzer einladen / berechtigen</div>
<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>
<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');
setShareTargetEmailOrId('');
}}
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>
{/* 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"
/>
{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="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>
)}
<div className="flex justify-end pt-1">
<button
type="submit"
disabled={sharingLoading || !shareTargetEmailOrId.trim()}
disabled={
sharingLoading ||
(addShareType === 'team' ? !selectedShareTeamId : !shareTargetEmailOrId.trim())
}
className="btn-primary h-8 px-3 text-xs flex items-center gap-1.5"
>
<Plus className="w-3.5 h-3.5" />
<span>Freigeben</span>
<span>
{sharingLoading
? 'Wird geteilt…'
: addShareType === 'team'
? 'Team freigeben'
: 'Benutzer freigeben'}
</span>
</button>
</div>
</form>