feat(workspace): überarbeite geteilte Notizen und ganze Ordner teilen
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m1s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m1s
This commit is contained in:
1 parent
3fcd164fd8
commit
35fd682fcd
6 files changed
+1213
-93
No files matched your search
@@ -48,7 +48,7 @@ import {
|
||||
Hash,
|
||||
User as UserIcon,
|
||||
} from 'lucide-react';
|
||||
import { BlockType, Document, DocumentBlock, DocLockInfo, DocShare, TagDetails, TagSummary, Team, User } from '../../lib/types';
|
||||
import { BlockType, Document, DocumentBlock, DocLockInfo, DocShare, FolderShare, SharedFolder, TagDetails, TagSummary, Team, User } from '../../lib/types';
|
||||
import { api, getToken, triggerHaptic } from '../../lib/api';
|
||||
import { CodeBlockItem } from './CodeBlockItem';
|
||||
import { UserAutocomplete } from '../UserAutocomplete';
|
||||
@@ -69,6 +69,9 @@ interface FolderTreeItem {
|
||||
depth: number;
|
||||
subfolders: FolderTreeItem[];
|
||||
docs: Document[];
|
||||
sharedFolder?: SharedFolder;
|
||||
isShared?: boolean;
|
||||
isSharedRoot?: boolean;
|
||||
}
|
||||
|
||||
interface SlashCommandItem {
|
||||
@@ -199,6 +202,12 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
const [sharingLoading, setSharingLoading] = useState(false);
|
||||
const [shareError, setShareError] = useState<string | null>(null);
|
||||
|
||||
// --- FOLDER SHARING STATE ---
|
||||
const [sharedFolders, setSharedFolders] = useState<SharedFolder[]>([]);
|
||||
const [sharingFolder, setSharingFolder] = useState<SharedFolder | null>(null);
|
||||
const [showFolderShareModal, setShowFolderShareModal] = useState(false);
|
||||
const [folderShares, setFolderShares] = useState<FolderShare[]>([]);
|
||||
|
||||
// --- DOCUMENT LOCKING STATE ---
|
||||
const [activeLock, setActiveLock] = useState<DocLockInfo | null>(null);
|
||||
const [holdsLock, setHoldsLock] = useState(false);
|
||||
@@ -411,14 +420,16 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
|
||||
const loadDocs = async (selectId?: string | null) => {
|
||||
try {
|
||||
const [list, serverFolders, tags] = await Promise.all([
|
||||
const [list, serverFolders, tags, sFolders] = await Promise.all([
|
||||
api.getDocuments(),
|
||||
api.getFolders(),
|
||||
api.getWorkspaceTags().catch(() => []),
|
||||
api.getSharedFolders().catch(() => []),
|
||||
]);
|
||||
setDocs(list);
|
||||
setCustomFolders(serverFolders);
|
||||
setWorkspaceTags(tags);
|
||||
setSharedFolders(sFolders);
|
||||
if (selectId) {
|
||||
const found = list.find((d) => d.id === selectId);
|
||||
if (found) {
|
||||
@@ -637,6 +648,8 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
}
|
||||
} else if (msg.event === 'WORKSPACE_FOLDERS_UPDATED') {
|
||||
api.getFolders().then(setCustomFolders).catch(() => {});
|
||||
api.getSharedFolders().then(setSharedFolders).catch(() => {});
|
||||
api.getDocuments().then(setDocs).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -665,6 +678,30 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const openFolderShareModal = async (node: FolderTreeItem) => {
|
||||
try {
|
||||
let sf = node.sharedFolder;
|
||||
if (!sf) {
|
||||
sf = await api.createSharedFolder({ name: node.name, mountPath: node.path });
|
||||
const sfList = await api.getSharedFolders();
|
||||
setSharedFolders(sfList);
|
||||
}
|
||||
const [shares, usersList, teamsList] = await Promise.all([
|
||||
api.getFolderShares(sf.id).catch(() => []),
|
||||
api.getUsersDirectory ? api.getUsersDirectory().catch(() => api.getCollaborators().catch(() => [])) : api.getCollaborators().catch(() => []),
|
||||
api.getUserTeams().catch(() => []),
|
||||
]);
|
||||
setFolderShares(shares);
|
||||
setCollaborators(usersList);
|
||||
setShareTeams(teamsList);
|
||||
setSharingFolder(sf);
|
||||
setShowFolderShareModal(true);
|
||||
} catch (err) {
|
||||
console.error('Failed to open folder share modal', err);
|
||||
alert('Fehler beim Öffnen der Ordnerfreigabe');
|
||||
}
|
||||
};
|
||||
|
||||
const handleAddShare = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (!selectedDoc) return;
|
||||
@@ -796,6 +833,16 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
// Compute all unique folder paths
|
||||
const allFolderPaths = useMemo(() => {
|
||||
const set = new Set<string>(customFolders);
|
||||
sharedFolders.forEach((sf) => {
|
||||
if (sf.mountPath) {
|
||||
const parts = sf.mountPath.split('/');
|
||||
let cur = '';
|
||||
for (const p of parts) {
|
||||
cur = cur ? `${cur}/${p}` : p;
|
||||
set.add(cur);
|
||||
}
|
||||
}
|
||||
});
|
||||
docs.forEach((doc) => {
|
||||
const f = getDocFolder(doc);
|
||||
if (f) {
|
||||
@@ -808,7 +855,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
}
|
||||
});
|
||||
return Array.from(set).sort((a, b) => a.localeCompare(b));
|
||||
}, [customFolders, docs]);
|
||||
}, [customFolders, sharedFolders, docs]);
|
||||
|
||||
// Keep folders expanded by default
|
||||
useEffect(() => {
|
||||
@@ -919,6 +966,27 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
|
||||
triggerHaptic(15);
|
||||
|
||||
// Check if sourcePath is the root mount of a shared folder
|
||||
const matchingSF = sharedFolders.find((sf) => sf.mountPath === cleanSource);
|
||||
if (matchingSF) {
|
||||
try {
|
||||
await api.updateFolderMount(matchingSF.id, newFolderPath);
|
||||
const sfList = await api.getSharedFolders();
|
||||
setSharedFolders(sfList);
|
||||
await loadDocs(selectedDoc?.id);
|
||||
setExpandedFolders((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(newFolderPath);
|
||||
if (cleanTarget) next.add(cleanTarget);
|
||||
return next;
|
||||
});
|
||||
} catch (err: any) {
|
||||
console.error('Failed to move shared folder mount', err);
|
||||
alert(err.message || 'Fehler beim Verschieben des geteilten Ordners');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. Update all documents that are in cleanSource or any subfolder of cleanSource
|
||||
const docsToUpdate = docs.filter((d) => {
|
||||
const f = getDocFolder(d);
|
||||
@@ -948,7 +1016,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
if (found) setSelectedDoc(found);
|
||||
}
|
||||
|
||||
// Persist all affected docs to backend
|
||||
// Persist all affected docs to backend using api.moveDocument
|
||||
for (const d of docsToUpdate) {
|
||||
const f = getDocFolder(d);
|
||||
let nextFolder = newFolderPath;
|
||||
@@ -956,15 +1024,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
const suffix = f.slice(cleanSource.length);
|
||||
nextFolder = `${newFolderPath}${suffix}`;
|
||||
}
|
||||
const updatedTags = (d.tags || []).filter((t) => !t.startsWith('folder:'));
|
||||
if (nextFolder) updatedTags.push(`folder:${nextFolder}`);
|
||||
|
||||
api
|
||||
.updateDocument(d.id, {
|
||||
folderPath: nextFolder,
|
||||
tags: updatedTags,
|
||||
})
|
||||
.catch((err) => console.error('Failed to update doc folder', err));
|
||||
api.moveDocument(d.id, nextFolder).catch((err) => console.error('Failed to move doc', err));
|
||||
}
|
||||
|
||||
// 2. Sync folder structure with server
|
||||
@@ -995,18 +1055,41 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
if (getDocFolder(doc) === cleanPath) return;
|
||||
|
||||
triggerHaptic(15);
|
||||
const updatedTags = (doc.tags || []).filter((t) => !t.startsWith('folder:'));
|
||||
if (cleanPath) {
|
||||
updatedTags.push(`folder:${cleanPath}`);
|
||||
}
|
||||
const updatedDoc: Document = {
|
||||
...doc,
|
||||
folderPath: cleanPath,
|
||||
tags: updatedTags,
|
||||
};
|
||||
await persistDocument(updatedDoc, true);
|
||||
if (cleanPath) {
|
||||
setExpandedFolders((prev) => new Set([...prev, cleanPath]));
|
||||
try {
|
||||
const updatedDoc = await api.moveDocument(doc.id, cleanPath);
|
||||
setDocs((prev) =>
|
||||
prev.map((d) =>
|
||||
d.id === doc.id
|
||||
? {
|
||||
...d,
|
||||
folderPath: updatedDoc.folderPath,
|
||||
sharedFolderId: updatedDoc.sharedFolderId,
|
||||
subFolderPath: updatedDoc.subFolderPath,
|
||||
isSharedFolder: updatedDoc.isSharedFolder,
|
||||
}
|
||||
: d
|
||||
)
|
||||
);
|
||||
if (selectedDoc?.id === doc.id) {
|
||||
setSelectedDoc((prev) =>
|
||||
prev
|
||||
? {
|
||||
...prev,
|
||||
folderPath: updatedDoc.folderPath,
|
||||
sharedFolderId: updatedDoc.sharedFolderId,
|
||||
subFolderPath: updatedDoc.subFolderPath,
|
||||
isSharedFolder: updatedDoc.isSharedFolder,
|
||||
}
|
||||
: null
|
||||
);
|
||||
}
|
||||
if (cleanPath) {
|
||||
setExpandedFolders((prev) => new Set([...prev, cleanPath]));
|
||||
}
|
||||
api.getFolders().then(setCustomFolders).catch(() => {});
|
||||
} catch (err: any) {
|
||||
console.error('Failed to move doc', err);
|
||||
alert(err.message || 'Fehler beim Verschieben der Notiz');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1043,6 +1126,30 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
};
|
||||
|
||||
const handleDeleteFolder = async (folderPath: string, folderName: string) => {
|
||||
const matchingSF = sharedFolders.find((sf) => sf.mountPath === folderPath);
|
||||
if (matchingSF) {
|
||||
if (matchingSF.isOwner) {
|
||||
if (!window.confirm(`Möchtest du den geteilten Ordner "${folderName}" wirklich löschen? Die Freigabe wird für alle Teilnehmer aufgehoben.`)) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (!window.confirm(`Möchtest du den geteilten Ordner "${folderName}" aus deinem Workspace entfernen? (Die Quell-Dateien des Eigentümers bleiben erhalten).`)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
await api.deleteSharedFolder(matchingSF.id);
|
||||
const sfList = await api.getSharedFolders();
|
||||
setSharedFolders(sfList);
|
||||
await loadDocs(selectedDoc?.id);
|
||||
return;
|
||||
} catch (err: any) {
|
||||
console.error('Failed to delete shared folder', err);
|
||||
alert(err.message || 'Fehler beim Löschen des geteilten Ordners');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Find all documents in this folder and subfolders
|
||||
const docsInFolder = docs.filter((d) => {
|
||||
const f = getDocFolder(d);
|
||||
@@ -2787,12 +2894,30 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
allFolderPaths.forEach((path) => {
|
||||
const parts = path.split('/');
|
||||
const name = parts[parts.length - 1];
|
||||
|
||||
let matchingSF: SharedFolder | undefined;
|
||||
let isRoot = false;
|
||||
for (const sf of sharedFolders) {
|
||||
if (sf.mountPath === path) {
|
||||
matchingSF = sf;
|
||||
isRoot = true;
|
||||
break;
|
||||
} else if (path.startsWith(sf.mountPath + '/')) {
|
||||
matchingSF = sf;
|
||||
isRoot = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
folderMap.set(path, {
|
||||
path,
|
||||
name,
|
||||
depth: parts.length - 1,
|
||||
subfolders: [],
|
||||
docs: [],
|
||||
sharedFolder: matchingSF,
|
||||
isShared: !!matchingSF,
|
||||
isSharedRoot: isRoot,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2822,7 +2947,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
|
||||
const rootOnlyDocs = filteredDocs.filter((d) => !getDocFolder(d));
|
||||
return { rootTree: rootFolders, rootDocs: rootOnlyDocs };
|
||||
}, [allFolderPaths, filteredDocs]);
|
||||
}, [allFolderPaths, filteredDocs, sharedFolders]);
|
||||
|
||||
const pinnedDocs = useMemo(() => {
|
||||
return docs.filter((d) => d.isPinned);
|
||||
@@ -3091,10 +3216,29 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
<Folder className="w-4 h-4 sm:w-3.5 sm:h-3.5 text-muted flex-shrink-0" />
|
||||
)}
|
||||
<span className="font-medium text-sm sm:text-xs text-main truncate">{node.name}</span>
|
||||
{node.isShared && (
|
||||
<span
|
||||
title={node.sharedFolder?.isOwner ? 'Geteilter Ordner (Eigentümer)' : 'Geteilter Ordner'}
|
||||
className="text-accent flex-shrink-0"
|
||||
>
|
||||
<Users className="w-3.5 h-3.5 sm:w-3 sm:h-3" />
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[10px] text-faint font-mono">({totalItems})</span>
|
||||
</div>
|
||||
|
||||
<div className="opacity-100 sm:opacity-0 sm:group-hover:opacity-100 flex items-center gap-1 transition-opacity">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
openFolderShareModal(node);
|
||||
}}
|
||||
className="p-1.5 sm:p-1 hover:bg-panel-elevated text-muted hover:text-accent rounded-btn"
|
||||
title="Ordner freigeben / teilen"
|
||||
>
|
||||
<Users className="w-3.5 h-3.5 sm:w-3 sm:h-3" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
@@ -4926,6 +5070,57 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* UNIFIED FOLDER SHARING MODAL */}
|
||||
{showFolderShareModal && sharingFolder && (
|
||||
<UnifiedShareModal
|
||||
isOpen={showFolderShareModal}
|
||||
onClose={() => {
|
||||
setShowFolderShareModal(false);
|
||||
setSharingFolder(null);
|
||||
}}
|
||||
title={`Ordner teilen: ${sharingFolder.name}`}
|
||||
resourceName={sharingFolder.name}
|
||||
resourceIcon="📁"
|
||||
isOwner={sharingFolder.isOwner}
|
||||
currentUserId={currentUser?.id}
|
||||
shares={(folderShares || []).map((sh) => ({
|
||||
id: sh.id,
|
||||
targetType: sh.targetType,
|
||||
targetId: sh.targetId,
|
||||
targetName: sh.targetName || (sh.targetType === 'team' ? 'Gruppe' : sh.email || 'Benutzer'),
|
||||
targetEmail: sh.email,
|
||||
avatarUrl: sh.avatarUrl,
|
||||
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';
|
||||
await api.createFolderShare(sharingFolder.id, {
|
||||
targetType,
|
||||
targetId,
|
||||
permission: perm,
|
||||
});
|
||||
const updated = await api.getFolderShares(sharingFolder.id);
|
||||
setFolderShares(updated);
|
||||
const sfList = await api.getSharedFolders();
|
||||
setSharedFolders(sfList);
|
||||
await loadDocs(selectedDoc?.id);
|
||||
}}
|
||||
onRemoveShare={async (shareId) => {
|
||||
await api.deleteFolderShare(sharingFolder.id, shareId);
|
||||
const updated = await api.getFolderShares(sharingFolder.id);
|
||||
setFolderShares(updated);
|
||||
const sfList = await api.getSharedFolders();
|
||||
setSharedFolders(sfList);
|
||||
await loadDocs(selectedDoc?.id);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* LINK INSERTION MODAL (/link, /notiz, /web) */}
|
||||
{linkModalBlockId && (
|
||||
<div
|
||||
|
||||
Reference in new issue
Block a user