MindSift CI & Docker Release / Build, Test in Container & Push (push) Failing after 17s
1840 lines
75 KiB
TypeScript
1840 lines
75 KiB
TypeScript
import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
|
import {
|
|
FilePlus,
|
|
FolderPlus,
|
|
Folder,
|
|
FolderOpen,
|
|
ChevronRight,
|
|
ChevronDown,
|
|
Search,
|
|
Pin,
|
|
Trash2,
|
|
Check,
|
|
Hash,
|
|
FileText,
|
|
Users,
|
|
Plus,
|
|
X,
|
|
ExternalLink,
|
|
Sparkles,
|
|
} from 'lucide-react';
|
|
import {
|
|
Document,
|
|
DocShare,
|
|
SharedFolder,
|
|
TagDetails,
|
|
TagSummary,
|
|
Team,
|
|
UnifiedShare,
|
|
User,
|
|
} from '../../lib/types';
|
|
import { api, onWebSocketMessage, triggerHaptic } from '../../lib/api';
|
|
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
|
import { useMobileActions } from '../../context/MobileActionContext';
|
|
import { DocumentEditor } from './DocumentEditor';
|
|
|
|
interface WorkspaceViewProps {
|
|
currentUser?: User | null;
|
|
initialDocId?: string | null;
|
|
onStatsChange: () => void;
|
|
}
|
|
|
|
type WorkspaceSubView = 'editor' | 'tag_detail' | 'todos';
|
|
|
|
interface FolderTreeNode {
|
|
name: string;
|
|
fullPath: string;
|
|
subfolders: FolderTreeNode[];
|
|
docs: Document[];
|
|
sharedFolderObj?: SharedFolder;
|
|
}
|
|
|
|
interface FolderTreeItemProps {
|
|
node: FolderTreeNode;
|
|
depth: number;
|
|
expandedFolders: Record<string, boolean>;
|
|
setExpandedFolders: React.Dispatch<React.SetStateAction<Record<string, boolean>>>;
|
|
selectedDocId: string | null;
|
|
subView: WorkspaceSubView;
|
|
dragOverFolder: string | null;
|
|
setDragOverFolder: (f: string | null) => void;
|
|
handleDropOnFolder: (folderPath: string, e: React.DragEvent) => void;
|
|
handleFolderDragStart: (e: React.DragEvent, path: string) => void;
|
|
handleDocDragStart: (e: React.DragEvent, id: string) => void;
|
|
handleCreateDocument: (folderPath?: string) => void;
|
|
handleShareFolderClick: (folderPath: string, existingSf?: SharedFolder) => void;
|
|
handleDeleteFolder: (folderPath: string) => void;
|
|
onSelectDoc: (id: string) => void;
|
|
}
|
|
|
|
const FolderTreeItem: React.FC<FolderTreeItemProps> = ({
|
|
node,
|
|
depth,
|
|
expandedFolders,
|
|
setExpandedFolders,
|
|
selectedDocId,
|
|
subView,
|
|
dragOverFolder,
|
|
setDragOverFolder,
|
|
handleDropOnFolder,
|
|
handleFolderDragStart,
|
|
handleDocDragStart,
|
|
handleCreateDocument,
|
|
handleShareFolderClick,
|
|
handleDeleteFolder,
|
|
onSelectDoc,
|
|
}) => {
|
|
const isExpanded = Boolean(expandedFolders[node.fullPath]);
|
|
const isDragOver = dragOverFolder === node.fullPath;
|
|
const hasContent = node.subfolders.length > 0 || node.docs.length > 0;
|
|
|
|
return (
|
|
<div className="space-y-0.5">
|
|
<div
|
|
draggable
|
|
onDragStart={(e) => handleFolderDragStart(e, node.fullPath)}
|
|
onDragOver={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setDragOverFolder(node.fullPath);
|
|
}}
|
|
onDragLeave={(e) => {
|
|
e.preventDefault();
|
|
if (dragOverFolder === node.fullPath) setDragOverFolder(null);
|
|
}}
|
|
onDrop={(e) => handleDropOnFolder(node.fullPath, e)}
|
|
style={{ paddingLeft: `${depth * 12 + 8}px` }}
|
|
className={`group flex items-center justify-between pr-2 py-1 rounded-btn text-xs cursor-pointer transition-colors ${
|
|
isDragOver
|
|
? 'bg-accent/20 border-l-2 border-accent text-main font-semibold'
|
|
: 'hover:bg-panel-elevated text-muted'
|
|
}`}
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
setExpandedFolders((prev) => ({
|
|
...prev,
|
|
[node.fullPath]: !prev[node.fullPath],
|
|
}))
|
|
}
|
|
className="flex items-center gap-1.5 flex-1 min-w-0 text-left cursor-pointer"
|
|
>
|
|
{hasContent ? (
|
|
isExpanded ? (
|
|
<ChevronDown className="w-3.5 h-3.5 text-faint shrink-0" />
|
|
) : (
|
|
<ChevronRight className="w-3.5 h-3.5 text-faint shrink-0" />
|
|
)
|
|
) : (
|
|
<span className="w-3.5 shrink-0" />
|
|
)}
|
|
{isExpanded ? (
|
|
<FolderOpen className="w-3.5 h-3.5 text-accent shrink-0" />
|
|
) : (
|
|
<Folder className="w-3.5 h-3.5 text-accent shrink-0" />
|
|
)}
|
|
<span className="truncate">{node.name}</span>
|
|
{node.sharedFolderObj && (
|
|
<Users className="w-3 h-3 text-status-ok shrink-0" />
|
|
)}
|
|
<span className="text-[10px] text-faint ml-auto pr-1">
|
|
{node.docs.length}
|
|
</span>
|
|
</button>
|
|
|
|
<div className="hidden group-hover:flex items-center gap-0.5">
|
|
<button
|
|
type="button"
|
|
onClick={() => handleCreateDocument(node.fullPath)}
|
|
className="p-1 rounded-btn hover:bg-canvas text-muted hover:text-main cursor-pointer"
|
|
title="Notiz in diesem Ordner erstellen"
|
|
>
|
|
<Plus className="w-3 h-3" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleShareFolderClick(node.fullPath, node.sharedFolderObj)}
|
|
className={`p-1 rounded-btn hover:bg-canvas cursor-pointer ${
|
|
node.sharedFolderObj ? 'text-accent' : 'text-faint hover:text-accent'
|
|
}`}
|
|
title="Ordner teilen und Berechtigungen"
|
|
>
|
|
<Users className="w-3 h-3" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleDeleteFolder(node.fullPath)}
|
|
className="p-1 rounded-btn hover:bg-status-err/20 text-faint hover:text-status-err cursor-pointer"
|
|
title="Ordner löschen"
|
|
>
|
|
<Trash2 className="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{isExpanded && (
|
|
<div className="space-y-0.5">
|
|
{/* Subfolders in this folder */}
|
|
{node.subfolders.map((childNode) => (
|
|
<FolderTreeItem
|
|
key={childNode.fullPath}
|
|
node={childNode}
|
|
depth={depth + 1}
|
|
expandedFolders={expandedFolders}
|
|
setExpandedFolders={setExpandedFolders}
|
|
selectedDocId={selectedDocId}
|
|
subView={subView}
|
|
dragOverFolder={dragOverFolder}
|
|
setDragOverFolder={setDragOverFolder}
|
|
handleDropOnFolder={handleDropOnFolder}
|
|
handleFolderDragStart={handleFolderDragStart}
|
|
handleDocDragStart={handleDocDragStart}
|
|
handleCreateDocument={handleCreateDocument}
|
|
handleShareFolderClick={handleShareFolderClick}
|
|
handleDeleteFolder={handleDeleteFolder}
|
|
onSelectDoc={onSelectDoc}
|
|
/>
|
|
))}
|
|
|
|
{/* Notes directly in this folder */}
|
|
{node.docs.map((d) => (
|
|
<div
|
|
key={d.id}
|
|
draggable
|
|
onDragStart={(e) => handleDocDragStart(e, d.id)}
|
|
onClick={() => onSelectDoc(d.id)}
|
|
style={{ paddingLeft: `${(depth + 1) * 12 + 16}px` }}
|
|
className={`w-full flex items-center gap-2 pr-2 py-1.5 rounded-btn text-left text-xs cursor-pointer transition-colors ${
|
|
selectedDocId === d.id && subView === 'editor'
|
|
? 'bg-accent text-on-accent font-semibold'
|
|
: 'text-main hover:bg-panel-elevated'
|
|
}`}
|
|
>
|
|
<span className="shrink-0">{d.icon || '📝'}</span>
|
|
<span className="truncate flex-1">{d.title || 'Unbenannte Notiz'}</span>
|
|
</div>
|
|
))}
|
|
|
|
{!hasContent && (
|
|
<div
|
|
style={{ paddingLeft: `${(depth + 1) * 12 + 16}px` }}
|
|
className="py-1 text-[10px] text-faint italic"
|
|
>
|
|
Leer
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|
currentUser,
|
|
initialDocId,
|
|
onStatsChange,
|
|
}) => {
|
|
const [docs, setDocs] = useState<Document[]>([]);
|
|
const [folders, setFolders] = useState<string[]>([]);
|
|
const [sharedFolders, setSharedFolders] = useState<SharedFolder[]>([]);
|
|
const [tagsSummary, setTagsSummary] = useState<TagSummary[]>([]);
|
|
|
|
const [selectedDocId, setSelectedDocId] = useState<string | null>(initialDocId || null);
|
|
const [targetBlockId, setTargetBlockId] = useState<string | null>(null);
|
|
const [newlyCreatedDocId, setNewlyCreatedDocId] = useState<string | null>(null);
|
|
const [subView, setSubView] = useState<WorkspaceSubView>('editor');
|
|
const [selectedTag, setSelectedTag] = useState<string | null>(null);
|
|
const [tagDetails, setTagDetails] = useState<TagDetails | null>(null);
|
|
const [tagNoteDraft, setTagNoteDraft] = useState<string>('');
|
|
|
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
|
const [expandedFolders, setExpandedFolders] = useState<Record<string, boolean>>({});
|
|
const [showNewFolderModal, setShowNewFolderModal] = useState<boolean>(false);
|
|
const [newFolderName, setNewFolderName] = useState<string>('');
|
|
const [newFolderIsShared, setNewFolderIsShared] = useState<boolean>(false);
|
|
const [mobileShowDetail, setMobileShowDetail] = useState<boolean>(Boolean(initialDocId));
|
|
|
|
// Drag and Drop state
|
|
const [draggingDocId, setDraggingDocId] = useState<string | null>(null);
|
|
const [draggingFolderPath, setDraggingFolderPath] = useState<string | null>(null);
|
|
const [dragOverFolder, setDragOverFolder] = useState<string | null>(null);
|
|
|
|
// Rollup drilldown & View Mode
|
|
const [todoFolderDrilldown, setTodoFolderDrilldown] = useState<string | null>(null);
|
|
const [todoGrouping, setTodoGrouping] = useState<'folder_note' | 'note' | 'folder'>(() => {
|
|
return (localStorage.getItem('mindsift_todo_grouping') as any) || 'folder_note';
|
|
});
|
|
const [collapsedTodoFolders, setCollapsedTodoFolders] = useState<Set<string>>(new Set());
|
|
const [collapsedTodoNotes, setCollapsedTodoNotes] = useState<Set<string>>(new Set());
|
|
|
|
// Sharing Modal State
|
|
const [shareModalDoc, setShareModalDoc] = useState<Document | null>(null);
|
|
const [shareModalFolder, setShareModalFolder] = useState<SharedFolder | null>(null);
|
|
const [activeDocShares, setActiveDocShares] = useState<DocShare[]>([]);
|
|
const [teams, setTeams] = useState<Team[]>([]);
|
|
const [collaborators, setCollaborators] = useState<User[]>([]);
|
|
|
|
const isCreatingRef = useRef<boolean>(false);
|
|
const { setGroups, setPrimaryAction } = useMobileActions();
|
|
|
|
const loadWorkspaceData = useCallback(async () => {
|
|
try {
|
|
const [docsRes, foldersRes, sharedFoldersRes, tagsRes] = await Promise.all([
|
|
api.getDocuments(),
|
|
api.getFolders(),
|
|
api.getSharedFolders().catch(() => []),
|
|
api.getWorkspaceTags().catch(() => []),
|
|
]);
|
|
setDocs(docsRes);
|
|
setFolders(foldersRes);
|
|
setSharedFolders(sharedFoldersRes);
|
|
setTagsSummary(tagsRes);
|
|
|
|
setSelectedDocId((prevId) => {
|
|
if (prevId && docsRes.some((d) => d.id === prevId)) {
|
|
return prevId;
|
|
}
|
|
return docsRes.length > 0 ? docsRes[0].id : null;
|
|
});
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
loadWorkspaceData();
|
|
}, [loadWorkspaceData]);
|
|
|
|
useEffect(() => {
|
|
if (initialDocId) {
|
|
setSelectedDocId(initialDocId);
|
|
setSubView('editor');
|
|
setMobileShowDetail(true);
|
|
}
|
|
}, [initialDocId]);
|
|
|
|
// Global WebSocket listener with strict isolation & deduplication
|
|
useEffect(() => {
|
|
const unsub = onWebSocketMessage((msg) => {
|
|
if (msg.event === 'DOC_CREATED' && msg.payload) {
|
|
const created = msg.payload as Document;
|
|
// Strict Privacy Check: Only add if created by or shared with current user
|
|
if (currentUser) {
|
|
const isOwner = created.userId === currentUser.id;
|
|
const isShared =
|
|
created.sharedWith?.some(
|
|
(s) => s.userId === currentUser.id || s.targetId === currentUser.id
|
|
) || false;
|
|
const isFolderShared = Boolean(created.sharedFolderId || created.isSharedFolder);
|
|
if (!isOwner && !isShared && !isFolderShared) {
|
|
return;
|
|
}
|
|
}
|
|
setDocs((prev) => (prev.some((d) => d.id === created.id) ? prev : [created, ...prev]));
|
|
} else if (msg.event === 'DOC_UPDATED' && msg.payload) {
|
|
const updated = msg.payload as Document;
|
|
setDocs((prev) => prev.map((d) => (d.id === updated.id ? { ...d, ...updated } : d)));
|
|
} else if (msg.event === 'DOC_DELETED' && msg.payload?.id) {
|
|
const delId = msg.payload.id as string;
|
|
setDocs((prev) => prev.filter((d) => d.id !== delId));
|
|
setSelectedDocId((prev) => (prev === delId ? null : prev));
|
|
} else if (msg.event === 'WORKSPACE_FOLDERS_UPDATED') {
|
|
api.getFolders().then(setFolders).catch(() => {});
|
|
api.getSharedFolders().then(setSharedFolders).catch(() => {});
|
|
api.getDocuments().then(setDocs).catch(() => {});
|
|
}
|
|
});
|
|
return unsub;
|
|
}, [currentUser]);
|
|
|
|
const handleCreateDocument = useCallback(
|
|
async (folderPath = '') => {
|
|
if (isCreatingRef.current) return;
|
|
isCreatingRef.current = true;
|
|
triggerHaptic(10);
|
|
try {
|
|
const created = await api.createDocument({
|
|
title: 'Neue Notiz',
|
|
icon: '📝',
|
|
folderPath,
|
|
blocks: [
|
|
{
|
|
id: `blk_${Date.now().toString(36)}_0`,
|
|
type: 'text',
|
|
content: '',
|
|
level: 0,
|
|
position: 1000,
|
|
},
|
|
],
|
|
});
|
|
setDocs((prev) => (prev.some((d) => d.id === created.id) ? prev : [created, ...prev]));
|
|
setNewlyCreatedDocId(created.id);
|
|
setTargetBlockId(null);
|
|
setSelectedDocId(created.id);
|
|
setSubView('editor');
|
|
setMobileShowDetail(true);
|
|
if (folderPath) {
|
|
setExpandedFolders((prev) => ({ ...prev, [folderPath]: true }));
|
|
}
|
|
onStatsChange();
|
|
} catch {
|
|
// ignore
|
|
} finally {
|
|
setTimeout(() => {
|
|
isCreatingRef.current = false;
|
|
}, 300);
|
|
}
|
|
},
|
|
[onStatsChange]
|
|
);
|
|
|
|
const handleJumpToDocBlock = useCallback((docId: string, blockId?: string) => {
|
|
setSelectedDocId(docId);
|
|
setTargetBlockId(blockId || null);
|
|
setNewlyCreatedDocId(null);
|
|
setSubView('editor');
|
|
setMobileShowDetail(true);
|
|
}, []);
|
|
|
|
// Register mobile FAB actions
|
|
useEffect(() => {
|
|
setPrimaryAction(() => handleCreateDocument(''));
|
|
setGroups([
|
|
{
|
|
id: 'workspace-actions',
|
|
title: 'Notizen & Ordner',
|
|
items: [
|
|
{
|
|
id: 'new-note',
|
|
label: 'Neue Notiz',
|
|
icon: <FilePlus className="w-4 h-4" />,
|
|
variant: 'accent',
|
|
onClick: () => handleCreateDocument(''),
|
|
},
|
|
{
|
|
id: 'new-folder',
|
|
label: 'Neuer Ordner',
|
|
icon: <FolderPlus className="w-4 h-4" />,
|
|
onClick: () => setShowNewFolderModal(true),
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
return () => {
|
|
setGroups([]);
|
|
setPrimaryAction(undefined);
|
|
};
|
|
}, [handleCreateDocument, setGroups, setPrimaryAction]);
|
|
|
|
const handleDeleteDocument = useCallback(
|
|
async (docId: string) => {
|
|
try {
|
|
await api.deleteDocument(docId);
|
|
setDocs((prev) => {
|
|
const remaining = prev.filter((d) => d.id !== docId);
|
|
if (selectedDocId === docId) {
|
|
setSelectedDocId(remaining.length > 0 ? remaining[0].id : null);
|
|
}
|
|
return remaining;
|
|
});
|
|
setMobileShowDetail(false);
|
|
onStatsChange();
|
|
} catch {
|
|
// ignore
|
|
}
|
|
},
|
|
[onStatsChange, selectedDocId]
|
|
);
|
|
|
|
const handleDocMetadataChange = useCallback((updated: Document) => {
|
|
setDocs((prev) => prev.map((d) => (d.id === updated.id ? { ...d, ...updated } : d)));
|
|
}, []);
|
|
|
|
const handleCreateFolderSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
const clean = newFolderName.trim().replace(/^\/+|\/+$/g, '');
|
|
if (!clean) return;
|
|
try {
|
|
if (newFolderIsShared) {
|
|
const sf = await api.createSharedFolder({ name: clean, mountPath: clean });
|
|
setSharedFolders((prev) => [...prev, sf]);
|
|
}
|
|
const res = await api.createFolder(clean);
|
|
setFolders((prev) => (prev.includes(res.path) ? prev : [...prev, res.path].sort()));
|
|
const parts = res.path.split('/');
|
|
let acc = '';
|
|
const expansions: Record<string, boolean> = {};
|
|
for (const part of parts) {
|
|
acc = acc ? `${acc}/${part}` : part;
|
|
expansions[acc] = true;
|
|
}
|
|
setExpandedFolders((prev) => ({ ...prev, ...expansions }));
|
|
setNewFolderName('');
|
|
setNewFolderIsShared(false);
|
|
setShowNewFolderModal(false);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
};
|
|
|
|
const handleDeleteFolder = async (folderPath: string) => {
|
|
try {
|
|
const res = await api.deleteFolder(folderPath);
|
|
setFolders((prev) =>
|
|
prev.filter((f) => f !== folderPath && !f.startsWith(`${folderPath}/`))
|
|
);
|
|
if (res.deletedDocIds && res.deletedDocIds.length > 0) {
|
|
setDocs((prev) => prev.filter((d) => !res.deletedDocIds.includes(d.id)));
|
|
if (selectedDocId && res.deletedDocIds.includes(selectedDocId)) {
|
|
setSelectedDocId(null);
|
|
}
|
|
}
|
|
onStatsChange();
|
|
} catch {
|
|
// ignore
|
|
}
|
|
};
|
|
|
|
// HTML5 Drag & Drop handlers for moving documents into folders
|
|
const handleDocDragStart = (e: React.DragEvent, docId: string) => {
|
|
e.stopPropagation();
|
|
e.dataTransfer.setData('application/mindsift-doc-id', docId);
|
|
e.dataTransfer.setData('text/plain', docId);
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
setDraggingDocId(docId);
|
|
};
|
|
|
|
const handleFolderDragStart = (e: React.DragEvent, path: string) => {
|
|
e.stopPropagation();
|
|
e.dataTransfer.setData('application/mindsift-folder-path', path);
|
|
e.dataTransfer.setData('text/plain', path);
|
|
e.dataTransfer.effectAllowed = 'move';
|
|
setDraggingFolderPath(path);
|
|
};
|
|
|
|
const handleDropOnFolder = async (targetFolder: string, e: React.DragEvent) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setDragOverFolder(null);
|
|
|
|
const docId = e.dataTransfer.getData('application/mindsift-doc-id') || draggingDocId;
|
|
const folderPath = e.dataTransfer.getData('application/mindsift-folder-path') || draggingFolderPath;
|
|
|
|
setDraggingDocId(null);
|
|
setDraggingFolderPath(null);
|
|
|
|
if (docId) {
|
|
triggerHaptic(15);
|
|
try {
|
|
const updatedDoc = await api.moveDocument(docId, targetFolder);
|
|
setDocs((prev) =>
|
|
prev.map((d) => (d.id === docId ? { ...d, folderPath: updatedDoc.folderPath } : d))
|
|
);
|
|
if (targetFolder) {
|
|
const parts = targetFolder.split('/');
|
|
let acc = '';
|
|
const expansions: Record<string, boolean> = {};
|
|
for (const part of parts) {
|
|
acc = acc ? `${acc}/${part}` : part;
|
|
expansions[acc] = true;
|
|
}
|
|
setExpandedFolders((prev) => ({ ...prev, ...expansions }));
|
|
}
|
|
} catch {
|
|
// ignore
|
|
}
|
|
} else if (folderPath && folderPath !== targetFolder && !targetFolder.startsWith(`${folderPath}/`)) {
|
|
triggerHaptic(15);
|
|
try {
|
|
await api.moveFolder(folderPath, targetFolder);
|
|
if (targetFolder) {
|
|
const parts = targetFolder.split('/');
|
|
let acc = '';
|
|
const expansions: Record<string, boolean> = {};
|
|
for (const part of parts) {
|
|
acc = acc ? `${acc}/${part}` : part;
|
|
expansions[acc] = true;
|
|
}
|
|
setExpandedFolders((prev) => ({ ...prev, ...expansions }));
|
|
}
|
|
loadWorkspaceData();
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
};
|
|
|
|
const handleSelectTag = useCallback(async (tag: string) => {
|
|
const cleanTag = tag.replace(/^#/, '').toLowerCase();
|
|
setSelectedTag(cleanTag);
|
|
setSubView('tag_detail');
|
|
setMobileShowDetail(true);
|
|
try {
|
|
const details = await api.getTagDetails(cleanTag);
|
|
setTagDetails(details);
|
|
setTagNoteDraft(details.noteContent || '');
|
|
} catch {
|
|
setTagDetails(null);
|
|
}
|
|
}, []);
|
|
|
|
const handleSaveTagNote = async () => {
|
|
if (!selectedTag) return;
|
|
try {
|
|
await api.updateTagPageNote(selectedTag, tagNoteDraft);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
};
|
|
|
|
const handleWikiLinkClick = useCallback(
|
|
(pageTitle: string) => {
|
|
const clean = pageTitle.trim().toLowerCase();
|
|
const match = docs.find((d) => d.title.trim().toLowerCase() === clean);
|
|
if (match) {
|
|
setSelectedDocId(match.id);
|
|
setSubView('editor');
|
|
setMobileShowDetail(true);
|
|
} else {
|
|
handleCreateDocument('');
|
|
}
|
|
},
|
|
[docs, handleCreateDocument]
|
|
);
|
|
|
|
const openDocShareModal = useCallback(async (doc: Document) => {
|
|
setShareModalDoc(doc);
|
|
setShareModalFolder(null);
|
|
try {
|
|
const [sharesRes, teamsRes, collabRes] = await Promise.all([
|
|
api.getDocShares(doc.id).catch(() => []),
|
|
api.getUserTeams().catch(() => []),
|
|
api.getUsersDirectory().catch(() => []),
|
|
]);
|
|
setActiveDocShares(sharesRes);
|
|
setTeams(teamsRes);
|
|
setCollaborators(collabRes);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}, []);
|
|
|
|
const openFolderShareModal = useCallback(async (sf: SharedFolder) => {
|
|
setShareModalFolder(sf);
|
|
setShareModalDoc(null);
|
|
try {
|
|
const [teamsRes, collabRes, freshSf] = await Promise.all([
|
|
api.getUserTeams().catch(() => []),
|
|
api.getUsersDirectory().catch(() => []),
|
|
api.getSharedFolder(sf.id).catch(() => sf),
|
|
]);
|
|
setShareModalFolder(freshSf);
|
|
setTeams(teamsRes);
|
|
setCollaborators(collabRes);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}, []);
|
|
|
|
const handleShareFolderClick = useCallback(
|
|
async (folderPath: string, existingSf?: SharedFolder) => {
|
|
if (existingSf) {
|
|
openFolderShareModal(existingSf);
|
|
return;
|
|
}
|
|
try {
|
|
const clean = folderPath.trim().replace(/^\/+|\/+$/g, '');
|
|
const sf = await api.createSharedFolder({ name: clean, mountPath: clean });
|
|
setSharedFolders((prev) => [...prev.filter((f) => f.id !== sf.id), sf]);
|
|
openFolderShareModal(sf);
|
|
} catch {
|
|
// ignore
|
|
}
|
|
},
|
|
[openFolderShareModal]
|
|
);
|
|
|
|
const handleToggleRollupTodo = async (doc: Document, blockId: string, currentChecked: boolean) => {
|
|
triggerHaptic(10);
|
|
const updatedBlocks = (doc.blocks || []).map((b) =>
|
|
b.id === blockId ? { ...b, checked: !currentChecked } : b
|
|
);
|
|
setDocs((prev) =>
|
|
prev.map((d) => (d.id === doc.id ? { ...d, blocks: updatedBlocks } : d))
|
|
);
|
|
try {
|
|
await api.patchDocumentBlocks(doc.id, {
|
|
upserted: [{ id: blockId, checked: !currentChecked, type: 'todo', content: '' }],
|
|
});
|
|
} catch {
|
|
loadWorkspaceData();
|
|
}
|
|
};
|
|
|
|
const allFolderPaths = useMemo(() => {
|
|
const set = new Set<string>(folders);
|
|
for (const d of docs) {
|
|
if (d.folderPath) set.add(d.folderPath);
|
|
}
|
|
for (const sf of sharedFolders) {
|
|
if (sf.mountPath) set.add(sf.mountPath);
|
|
}
|
|
return Array.from(set).sort();
|
|
}, [docs, folders, sharedFolders]);
|
|
|
|
const folderTree = useMemo(() => {
|
|
const rootNodes: FolderTreeNode[] = [];
|
|
const nodeMap = new Map<string, FolderTreeNode>();
|
|
|
|
const allPathsSet = new Set<string>();
|
|
for (const p of allFolderPaths) {
|
|
const parts = p.split('/');
|
|
let current = '';
|
|
for (const part of parts) {
|
|
current = current ? `${current}/${part}` : part;
|
|
allPathsSet.add(current);
|
|
}
|
|
}
|
|
|
|
const sortedPaths = Array.from(allPathsSet).sort((a, b) => {
|
|
const depthA = a.split('/').length;
|
|
const depthB = b.split('/').length;
|
|
if (depthA !== depthB) return depthA - depthB;
|
|
return a.localeCompare(b);
|
|
});
|
|
|
|
for (const p of sortedPaths) {
|
|
const parts = p.split('/');
|
|
const leafName = parts[parts.length - 1];
|
|
const sharedFolderObj = sharedFolders.find(
|
|
(sf) => sf.mountPath === p || sf.name === p
|
|
);
|
|
const folderDocs = filteredDocs.filter((d) => d.folderPath === p);
|
|
|
|
const node: FolderTreeNode = {
|
|
name: leafName,
|
|
fullPath: p,
|
|
subfolders: [],
|
|
docs: folderDocs,
|
|
sharedFolderObj,
|
|
};
|
|
nodeMap.set(p, node);
|
|
|
|
if (parts.length === 1) {
|
|
rootNodes.push(node);
|
|
} else {
|
|
const parentPath = parts.slice(0, -1).join('/');
|
|
const parentNode = nodeMap.get(parentPath);
|
|
if (parentNode) {
|
|
parentNode.subfolders.push(node);
|
|
} else {
|
|
rootNodes.push(node);
|
|
}
|
|
}
|
|
}
|
|
|
|
return rootNodes;
|
|
}, [allFolderPaths, filteredDocs, sharedFolders]);
|
|
|
|
const filteredDocs = useMemo(() => {
|
|
const q = searchQuery.trim().toLowerCase();
|
|
if (!q) return docs;
|
|
return docs.filter(
|
|
(d) =>
|
|
d.title.toLowerCase().includes(q) ||
|
|
(d.content && d.content.toLowerCase().includes(q)) ||
|
|
(d.tags && d.tags.some((t) => t.toLowerCase().includes(q)))
|
|
);
|
|
}, [docs, searchQuery]);
|
|
|
|
const pinnedDocs = useMemo(
|
|
() => filteredDocs.filter((d) => d.isPinned),
|
|
[filteredDocs]
|
|
);
|
|
|
|
const rootDocs = useMemo(
|
|
() => filteredDocs.filter((d) => !d.folderPath),
|
|
[filteredDocs]
|
|
);
|
|
|
|
// Global To-Dos rollup collections
|
|
const allRollupTodos = useMemo(() => {
|
|
const list: { doc: Document; blockId: string; content: string; checked: boolean }[] = [];
|
|
for (const d of docs) {
|
|
const fName = d.folderPath || 'Hauptebene';
|
|
if (todoFolderDrilldown && fName !== todoFolderDrilldown) continue;
|
|
for (const b of d.blocks || []) {
|
|
if (b.type === 'todo' && b.content.trim() !== '') {
|
|
list.push({ doc: d, blockId: b.id, content: b.content, checked: Boolean(b.checked) });
|
|
}
|
|
}
|
|
}
|
|
return list;
|
|
}, [docs, todoFolderDrilldown]);
|
|
|
|
const rollupTodosByFolder = useMemo(() => {
|
|
const map = new Map<string, { doc: Document; blockId: string; content: string; checked: boolean }[]>();
|
|
for (const item of allRollupTodos) {
|
|
const fName = item.doc.folderPath || 'Hauptebene';
|
|
const arr = map.get(fName) || [];
|
|
arr.push(item);
|
|
map.set(fName, arr);
|
|
}
|
|
return map;
|
|
}, [allRollupTodos]);
|
|
|
|
const rollupTodosByNote = useMemo(() => {
|
|
const map = new Map<
|
|
string,
|
|
{ doc: Document; items: { doc: Document; blockId: string; content: string; checked: boolean }[] }
|
|
>();
|
|
for (const item of allRollupTodos) {
|
|
const existing = map.get(item.doc.id);
|
|
if (existing) {
|
|
existing.items.push(item);
|
|
} else {
|
|
map.set(item.doc.id, { doc: item.doc, items: [item] });
|
|
}
|
|
}
|
|
return map;
|
|
}, [allRollupTodos]);
|
|
|
|
const totalOpenTodos = useMemo(() => {
|
|
let count = 0;
|
|
for (const d of docs) {
|
|
for (const b of d.blocks || []) {
|
|
if (b.type === 'todo' && !b.checked && b.content.trim() !== '') {
|
|
count++;
|
|
}
|
|
}
|
|
}
|
|
return count;
|
|
}, [docs]);
|
|
|
|
const selectedDocObj = useMemo(
|
|
() => docs.find((d) => d.id === selectedDocId) || null,
|
|
[docs, selectedDocId]
|
|
);
|
|
|
|
const unifiedShares: UnifiedShare[] = useMemo(() => {
|
|
if (shareModalDoc) {
|
|
return activeDocShares.map((s) => ({
|
|
id: s.userId || s.teamId || s.id,
|
|
targetType: s.targetType || (s.teamId ? 'team' : 'user'),
|
|
targetId: s.targetId || s.teamId || s.userId || '',
|
|
targetName: s.targetName || s.displayName || s.userEmail || 'Mitglied',
|
|
targetEmail: s.userEmail,
|
|
permission: s.permission === 'write' ? 'editor' : 'viewer',
|
|
createdAt: s.createdAt,
|
|
}));
|
|
}
|
|
if (shareModalFolder) {
|
|
return (shareModalFolder.shares || []).map((s) => ({
|
|
id: s.id,
|
|
targetType: s.targetType,
|
|
targetId: s.targetId,
|
|
targetName: s.targetName || s.email || 'Mitglied',
|
|
targetEmail: s.email,
|
|
permission: s.permission === 'write' ? 'editor' : 'viewer',
|
|
createdAt: s.createdAt,
|
|
}));
|
|
}
|
|
return [];
|
|
}, [activeDocShares, shareModalDoc, shareModalFolder]);
|
|
|
|
return (
|
|
<div className="flex h-[calc(100vh-4rem)] bg-canvas rounded-card border border-border overflow-hidden shadow-panel">
|
|
{/* LEFT MASTER SIDEBAR */}
|
|
<aside
|
|
className={`${
|
|
mobileShowDetail ? 'hidden lg:flex' : 'flex'
|
|
} w-full lg:w-72 xl:w-80 shrink-0 flex-col bg-canvas border-r border-border`}
|
|
>
|
|
{/* Sidebar Header & Actions */}
|
|
<div className="p-3 border-b border-border space-y-2">
|
|
<div className="flex items-center justify-between">
|
|
<h2 className="text-xs font-bold text-main uppercase tracking-wider">
|
|
Notizen & Outliner
|
|
</h2>
|
|
<div className="flex items-center gap-1">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowNewFolderModal(true)}
|
|
className="btn-action p-1 text-muted hover:text-main"
|
|
title="Neuer Ordner"
|
|
>
|
|
<FolderPlus className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleCreateDocument('')}
|
|
className="btn-primary inline-flex items-center gap-1 px-2 py-1 text-xs font-semibold"
|
|
>
|
|
<Plus className="w-3.5 h-3.5" />
|
|
<span>Notiz</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Search Input */}
|
|
<div className="relative">
|
|
<Search className="w-3.5 h-3.5 text-faint absolute left-2.5 top-1/2 -translate-y-1/2" />
|
|
<input
|
|
type="text"
|
|
value={searchQuery}
|
|
onChange={(e) => setSearchQuery(e.target.value)}
|
|
placeholder="Notizen oder #Tags suchen…"
|
|
className="input-field w-full pl-8 pr-6 py-1 text-xs"
|
|
/>
|
|
{searchQuery && (
|
|
<button
|
|
type="button"
|
|
onClick={() => setSearchQuery('')}
|
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-faint hover:text-main"
|
|
>
|
|
<X className="w-3.5 h-3.5" />
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Sub-view Switcher: Notizen vs. To-Dos */}
|
|
<div className="grid grid-cols-2 gap-1 p-0.5 rounded-btn bg-canvas border border-border text-xs">
|
|
<button
|
|
type="button"
|
|
onClick={() => setSubView('editor')}
|
|
className={`py-1 rounded-btn font-medium transition-colors ${
|
|
subView === 'editor'
|
|
? 'bg-panel-elevated text-main font-semibold shadow-sm'
|
|
: 'text-muted hover:text-main'
|
|
}`}
|
|
>
|
|
Dokumente ({docs.length})
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setSubView('todos');
|
|
setMobileShowDetail(true);
|
|
}}
|
|
className={`py-1 rounded-btn font-medium transition-colors ${
|
|
subView === 'todos'
|
|
? 'bg-panel-elevated text-main font-semibold shadow-sm'
|
|
: 'text-muted hover:text-main'
|
|
}`}
|
|
>
|
|
To-Dos ({totalOpenTodos})
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Scrollable Navigation Tree */}
|
|
<div className="flex-1 overflow-y-auto p-2 space-y-3">
|
|
{/* Drop target for moving docs or folders to root */}
|
|
{(draggingDocId || draggingFolderPath) && (
|
|
<div
|
|
onDragOver={(e) => {
|
|
e.preventDefault();
|
|
setDragOverFolder('__root__');
|
|
}}
|
|
onDragLeave={() => setDragOverFolder(null)}
|
|
onDrop={(e) => handleDropOnFolder('', e)}
|
|
className={`p-2 border-2 border-dashed rounded-btn text-center text-xs transition-colors ${
|
|
dragOverFolder === '__root__'
|
|
? 'border-accent bg-accent/20 text-main font-semibold'
|
|
: 'border-border text-muted bg-canvas'
|
|
}`}
|
|
>
|
|
📁 In Hauptebene ablegen
|
|
</div>
|
|
)}
|
|
|
|
{/* Pinned Notes */}
|
|
{pinnedDocs.length > 0 && !searchQuery && (
|
|
<div>
|
|
<div className="px-2 mb-1 flex items-center gap-1 text-[10px] font-bold uppercase tracking-wider text-faint">
|
|
<Pin className="w-3 h-3 text-status-warn" />
|
|
<span>Angeheftet</span>
|
|
</div>
|
|
<div className="space-y-0.5">
|
|
{pinnedDocs.map((d) => (
|
|
<div
|
|
key={`pin-${d.id}`}
|
|
draggable
|
|
onDragStart={(e) => handleDocDragStart(e, d.id)}
|
|
onClick={() => {
|
|
setSelectedDocId(d.id);
|
|
setSubView('editor');
|
|
setMobileShowDetail(true);
|
|
}}
|
|
className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-btn text-left text-xs cursor-pointer transition-colors ${
|
|
selectedDocId === d.id && subView === 'editor'
|
|
? 'bg-accent text-on-accent font-semibold'
|
|
: 'text-main hover:bg-panel-elevated'
|
|
}`}
|
|
>
|
|
<span>{d.icon || '📝'}</span>
|
|
<span className="truncate flex-1">{d.title || 'Unbenannte Notiz'}</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Folders Tree with Recursive Hierarchical Nesting */}
|
|
{folderTree.length > 0 && (
|
|
<div>
|
|
<div className="px-2 mb-1 text-[10px] font-bold uppercase tracking-wider text-faint">
|
|
Ordner
|
|
</div>
|
|
<div className="space-y-0.5">
|
|
{folderTree.map((node) => (
|
|
<FolderTreeItem
|
|
key={node.fullPath}
|
|
node={node}
|
|
depth={0}
|
|
expandedFolders={expandedFolders}
|
|
setExpandedFolders={setExpandedFolders}
|
|
selectedDocId={selectedDocId}
|
|
subView={subView}
|
|
dragOverFolder={dragOverFolder}
|
|
setDragOverFolder={setDragOverFolder}
|
|
handleDropOnFolder={handleDropOnFolder}
|
|
handleFolderDragStart={handleFolderDragStart}
|
|
handleDocDragStart={handleDocDragStart}
|
|
handleCreateDocument={handleCreateDocument}
|
|
handleShareFolderClick={handleShareFolderClick}
|
|
handleDeleteFolder={handleDeleteFolder}
|
|
onSelectDoc={(id) => {
|
|
setSelectedDocId(id);
|
|
setTargetBlockId(null);
|
|
setNewlyCreatedDocId(null);
|
|
setSubView('editor');
|
|
setMobileShowDetail(true);
|
|
}}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Root Documents */}
|
|
<div>
|
|
<div className="px-2 mb-1 text-[10px] font-bold uppercase tracking-wider text-faint">
|
|
Notizen (Hauptebene)
|
|
</div>
|
|
{rootDocs.length === 0 ? (
|
|
<div className="px-2 py-2 text-xs text-faint italic">
|
|
Keine Notizen auf der Hauptebene.
|
|
</div>
|
|
) : (
|
|
<div className="space-y-0.5">
|
|
{rootDocs.map((d) => (
|
|
<div
|
|
key={d.id}
|
|
draggable
|
|
onDragStart={(e) => handleDocDragStart(e, d.id)}
|
|
onClick={() => {
|
|
setSelectedDocId(d.id);
|
|
setSubView('editor');
|
|
setMobileShowDetail(true);
|
|
}}
|
|
className={`w-full flex items-center gap-2 px-2 py-1.5 rounded-btn text-left text-xs cursor-pointer transition-colors ${
|
|
selectedDocId === d.id && subView === 'editor'
|
|
? 'bg-accent text-on-accent font-semibold'
|
|
: 'text-main hover:bg-panel-elevated'
|
|
}`}
|
|
>
|
|
<span>{d.icon || '📝'}</span>
|
|
<span className="truncate flex-1">{d.title || 'Unbenannte Notiz'}</span>
|
|
{d.sharedWith && d.sharedWith.length > 0 && (
|
|
<Users className="w-3 h-3 text-status-ok shrink-0" />
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{/* Tags Explorer */}
|
|
{tagsSummary.length > 0 && (
|
|
<div>
|
|
<div className="px-2 mb-1 text-[10px] font-bold uppercase tracking-wider text-faint">
|
|
Hashtags (#)
|
|
</div>
|
|
<div className="flex flex-wrap gap-1 px-1">
|
|
{tagsSummary.map((t) => (
|
|
<button
|
|
key={t.tag}
|
|
type="button"
|
|
onClick={() => handleSelectTag(t.tag)}
|
|
className={`inline-flex items-center gap-0.5 px-2 py-0.5 rounded-btn text-[10px] font-medium transition-colors ${
|
|
subView === 'tag_detail' && selectedTag === t.tag
|
|
? 'bg-accent text-on-accent'
|
|
: 'bg-panel-elevated text-muted hover:text-accent'
|
|
}`}
|
|
>
|
|
<span>#{t.tag}</span>
|
|
<span className="opacity-60 font-mono text-[9px]">{t.count}</span>
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</aside>
|
|
|
|
{/* RIGHT DETAIL AREA (STRICTLY ISOLATED BY KEY) */}
|
|
<main
|
|
className={`${
|
|
mobileShowDetail ? 'flex' : 'hidden lg:flex'
|
|
} flex-1 flex-col min-w-0 h-full overflow-hidden bg-canvas`}
|
|
>
|
|
{subView === 'editor' && selectedDocId ? (
|
|
<DocumentEditor
|
|
key={selectedDocId}
|
|
docId={selectedDocId}
|
|
currentUser={currentUser}
|
|
initialDoc={selectedDocObj}
|
|
availableFolders={allFolderPaths}
|
|
autoFocusTitle={newlyCreatedDocId === selectedDocId}
|
|
initialFocusedBlockId={targetBlockId}
|
|
onDocMetadataChange={handleDocMetadataChange}
|
|
onDeleteDoc={handleDeleteDocument}
|
|
onOpenShareModal={openDocShareModal}
|
|
onSelectTag={handleSelectTag}
|
|
onWikiLinkClick={handleWikiLinkClick}
|
|
onBackMobile={() => setMobileShowDetail(false)}
|
|
/>
|
|
) : subView === 'tag_detail' && selectedTag ? (
|
|
<div className="flex-1 overflow-y-auto p-4 sm:p-8 bg-canvas">
|
|
<div className="max-w-3xl mx-auto space-y-4">
|
|
<div className="flex items-center justify-between border-b border-border pb-3">
|
|
<div className="flex items-center gap-2">
|
|
<Hash className="w-5 h-5 text-accent" />
|
|
<h1 className="text-xl font-bold text-main">
|
|
#{selectedTag}
|
|
</h1>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setSubView('editor')}
|
|
className="btn-action px-3 py-1 text-xs text-main cursor-pointer"
|
|
>
|
|
Zurück zum Editor
|
|
</button>
|
|
</div>
|
|
|
|
{/* Tag Context Note */}
|
|
<div className="space-y-1">
|
|
<label className="text-[10px] font-bold uppercase tracking-wider text-faint">
|
|
Tag-Notiz
|
|
</label>
|
|
<textarea
|
|
rows={3}
|
|
value={tagNoteDraft}
|
|
onChange={(e) => setTagNoteDraft(e.target.value)}
|
|
onBlur={handleSaveTagNote}
|
|
placeholder={`Notizen zu #${selectedTag}…`}
|
|
className="input-field w-full p-2.5 text-xs text-main"
|
|
/>
|
|
</div>
|
|
|
|
{/* Matched Document Blocks */}
|
|
<div className="space-y-3">
|
|
<h3 className="text-[10px] font-bold uppercase tracking-wider text-faint">
|
|
Verknüpfte Outliner-Blöcke & Unterpunkte ({tagDetails?.totalMatches || 0})
|
|
</h3>
|
|
{tagDetails?.documents.map((docGroup) => (
|
|
<div
|
|
key={docGroup.docId}
|
|
className="p-3 rounded-card border border-border bg-canvas space-y-2"
|
|
>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleJumpToDocBlock(docGroup.docId)}
|
|
className="flex items-center gap-2 text-xs font-semibold text-accent hover:underline cursor-pointer"
|
|
>
|
|
<span>{docGroup.icon || '📝'}</span>
|
|
<span>{docGroup.title}</span>
|
|
{docGroup.folderPath && (
|
|
<span className="badge-neutral text-[10px] font-normal">
|
|
📁 {docGroup.folderPath}
|
|
</span>
|
|
)}
|
|
</button>
|
|
<div className="space-y-2 pl-3 border-l-2 border-border/70">
|
|
{docGroup.matchedBlocks.map((mb) => (
|
|
<div key={mb.rootBlock.id} className="space-y-1">
|
|
{/* Root Bullet Point containing the hashtag */}
|
|
<div
|
|
onClick={() => handleJumpToDocBlock(docGroup.docId, mb.rootBlock.id)}
|
|
className="flex items-start gap-1.5 text-xs text-main font-medium group cursor-pointer hover:bg-panel-elevated p-1 rounded-btn transition-colors"
|
|
title="Klick: Zu dieser Zeile springen"
|
|
>
|
|
<span className="text-accent font-bold select-none">•</span>
|
|
<span className="flex-1 break-words">{mb.rootBlock.content}</span>
|
|
</div>
|
|
|
|
{/* All Children indented */}
|
|
{mb.children && mb.children.length > 0 && (
|
|
<div className="space-y-0.5">
|
|
{mb.children.map((ch) => {
|
|
const relLevel = Math.max(
|
|
1,
|
|
(ch.level || 0) - (mb.rootBlock.level || 0)
|
|
);
|
|
return (
|
|
<div
|
|
key={ch.id}
|
|
onClick={() => handleJumpToDocBlock(docGroup.docId, ch.id)}
|
|
style={{ paddingLeft: `${relLevel * 16}px` }}
|
|
className="flex items-start gap-1.5 text-xs text-muted hover:text-main group cursor-pointer hover:bg-panel-elevated p-1 rounded-btn transition-colors"
|
|
title="Klick: Zu dieser Zeile springen"
|
|
>
|
|
{ch.type === 'todo' ? (
|
|
<span
|
|
className={`w-3.5 h-3.5 mt-0.5 checkbox-square flex items-center justify-center shrink-0 border ${
|
|
ch.checked
|
|
? 'bg-status-ok border-status-ok text-on-accent'
|
|
: 'border-strong text-transparent'
|
|
}`}
|
|
>
|
|
<Check className="w-2.5 h-2.5 stroke-[3]" />
|
|
</span>
|
|
) : (
|
|
<span className="text-faint group-hover:text-accent font-bold select-none">
|
|
◦
|
|
</span>
|
|
)}
|
|
<span
|
|
className={`flex-1 break-words ${
|
|
ch.checked ? 'line-through text-faint' : ''
|
|
}`}
|
|
>
|
|
{ch.content}
|
|
</span>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
) : subView === 'todos' ? (
|
|
<div className="flex-1 overflow-y-auto p-4 sm:p-8 bg-canvas">
|
|
<div className="max-w-3xl mx-auto space-y-4">
|
|
<div className="flex flex-col sm:flex-row sm:items-center justify-between border-b border-border pb-3 gap-2">
|
|
<div>
|
|
<h2 className="text-lg font-bold text-main">
|
|
Globaler To-Do Rollup ({totalOpenTodos} offen)
|
|
</h2>
|
|
<p className="text-xs text-muted">
|
|
Alle Aufgaben aus deinen Notizen im gesamten Workspace
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
{/* View Grouping Selector */}
|
|
<div className="flex items-center rounded-btn bg-panel p-0.5 border border-border shadow-xs">
|
|
<span className="text-[10px] uppercase font-bold text-faint px-1.5 hidden sm:inline">
|
|
Ansicht:
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setTodoGrouping('folder_note');
|
|
localStorage.setItem('mindsift_todo_grouping', 'folder_note');
|
|
}}
|
|
className={`px-2 py-1 text-[11px] font-semibold rounded-btn transition-colors cursor-pointer flex items-center gap-1 ${
|
|
todoGrouping === 'folder_note'
|
|
? 'bg-panel-elevated text-main shadow-xs'
|
|
: 'text-muted hover:text-main'
|
|
}`}
|
|
title="Erst nach Ordner, dann nach Notiz gruppieren"
|
|
>
|
|
<Folder className="w-3 h-3 text-accent" />
|
|
<span>+</span>
|
|
<FileText className="w-3 h-3 text-accent" />
|
|
<span>Ordner & Notiz</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setTodoGrouping('note');
|
|
localStorage.setItem('mindsift_todo_grouping', 'note');
|
|
}}
|
|
className={`px-2 py-1 text-[11px] font-semibold rounded-btn transition-colors cursor-pointer flex items-center gap-1 ${
|
|
todoGrouping === 'note'
|
|
? 'bg-panel-elevated text-main shadow-xs'
|
|
: 'text-muted hover:text-main'
|
|
}`}
|
|
title="Direkt nach Notiz gruppieren"
|
|
>
|
|
<FileText className="w-3 h-3 text-accent" />
|
|
<span>Notiz</span>
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
setTodoGrouping('folder');
|
|
localStorage.setItem('mindsift_todo_grouping', 'folder');
|
|
}}
|
|
className={`px-2 py-1 text-[11px] font-semibold rounded-btn transition-colors cursor-pointer flex items-center gap-1 ${
|
|
todoGrouping === 'folder'
|
|
? 'bg-panel-elevated text-main shadow-xs'
|
|
: 'text-muted hover:text-main'
|
|
}`}
|
|
title="Nur nach Ordner gruppieren"
|
|
>
|
|
<Folder className="w-3 h-3 text-accent" />
|
|
<span>Ordner</span>
|
|
</button>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => setSubView('editor')}
|
|
className="btn-action px-3 py-1 text-xs text-main cursor-pointer"
|
|
>
|
|
Zum Editor
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Folder Drilldown Tag */}
|
|
{todoFolderDrilldown && (
|
|
<div className="flex items-center justify-between p-2 rounded-btn bg-accent/10 border border-accent text-xs">
|
|
<span>
|
|
Gefiltert nach Ordner: <strong>{todoFolderDrilldown}</strong>
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => setTodoFolderDrilldown(null)}
|
|
className="text-accent hover:underline font-semibold cursor-pointer"
|
|
>
|
|
Filter aufheben
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Grouped Folders & To-Dos */}
|
|
{allRollupTodos.length === 0 ? (
|
|
<div className="p-8 text-center text-muted text-xs space-y-1">
|
|
<Sparkles className="w-6 h-6 text-accent mx-auto mb-1" />
|
|
<p className="font-semibold text-main">Keine To-Dos in Notizen gefunden!</p>
|
|
<p className="text-faint">
|
|
Tippe in einem beliebigen Dokument <code>/todo</code> oder <code>[] </code> ein.
|
|
</p>
|
|
</div>
|
|
) : todoGrouping === 'note' ? (
|
|
/* Grouped by Note directly */
|
|
<div className="space-y-3">
|
|
{Array.from(rollupTodosByNote.entries()).map(([docId, { doc, items }]) => {
|
|
const isCollapsed = collapsedTodoNotes.has(docId);
|
|
const openCount = items.filter((i) => !i.checked).length;
|
|
|
|
return (
|
|
<div
|
|
key={docId}
|
|
className="border border-border rounded-card bg-canvas overflow-hidden"
|
|
>
|
|
<div className="p-2 bg-canvas border-b border-border flex items-center justify-between">
|
|
<div
|
|
onClick={() => {
|
|
setCollapsedTodoNotes((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(docId)) next.delete(docId);
|
|
else next.add(docId);
|
|
return next;
|
|
});
|
|
}}
|
|
className="flex items-center gap-2 cursor-pointer select-none"
|
|
>
|
|
{isCollapsed ? (
|
|
<ChevronRight className="w-3.5 h-3.5 text-faint" />
|
|
) : (
|
|
<ChevronDown className="w-3.5 h-3.5 text-accent" />
|
|
)}
|
|
<span className="text-sm">{doc.icon || '📝'}</span>
|
|
<span className="font-bold text-xs text-main">{doc.title}</span>
|
|
{doc.folderPath && (
|
|
<span className="badge-neutral text-[10px]">📁 {doc.folderPath}</span>
|
|
)}
|
|
<span className="badge-neutral text-[10px] font-mono">
|
|
{openCount} offen
|
|
</span>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => handleJumpToDocBlock(doc.id)}
|
|
className="text-[10px] text-accent hover:underline px-1.5 py-0.5 rounded-btn hover:bg-canvas cursor-pointer flex items-center gap-1"
|
|
title="Zur Notiz springen"
|
|
>
|
|
<span>Notiz öffnen</span>
|
|
<ExternalLink className="w-2.5 h-2.5" />
|
|
</button>
|
|
</div>
|
|
|
|
{!isCollapsed && (
|
|
<div className="p-2 space-y-1">
|
|
{items.map((item) => (
|
|
<div
|
|
key={`${item.doc.id}-${item.blockId}`}
|
|
className="flex items-center justify-between gap-2 p-1.5 rounded-btn hover:bg-panel-elevated text-xs transition-colors group cursor-pointer"
|
|
>
|
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleToggleRollupTodo(item.doc, item.blockId, item.checked);
|
|
}}
|
|
className={`w-3.5 h-3.5 checkbox-square flex items-center justify-center transition-colors border cursor-pointer ${
|
|
item.checked
|
|
? 'bg-status-ok border-status-ok text-on-accent'
|
|
: 'border-strong hover:border-accent text-transparent'
|
|
}`}
|
|
>
|
|
<Check className="w-2.5 h-2.5 stroke-[3]" />
|
|
</button>
|
|
<span
|
|
onClick={() => handleJumpToDocBlock(item.doc.id, item.blockId)}
|
|
className={`truncate flex-1 ${
|
|
item.checked
|
|
? 'line-through text-faint'
|
|
: 'text-main font-medium group-hover:text-accent'
|
|
}`}
|
|
>
|
|
{item.content}
|
|
</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleJumpToDocBlock(item.doc.id, item.blockId)}
|
|
className="text-faint hover:text-accent opacity-0 group-hover:opacity-100 transition-opacity p-1"
|
|
title="Zu dieser Zeile springen"
|
|
>
|
|
<ExternalLink className="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : todoGrouping === 'folder_note' ? (
|
|
/* Grouped by Folder -> grouped by Note */
|
|
<div className="space-y-3">
|
|
{Array.from(rollupTodosByFolder.entries()).map(([folderName, items]) => {
|
|
const isCollapsed = collapsedTodoFolders.has(folderName);
|
|
const openCount = items.filter((i) => !i.checked).length;
|
|
|
|
// Subgroup by doc
|
|
const docGroupsMap = new Map<string, { doc: Document; items: typeof items }>();
|
|
for (const it of items) {
|
|
const ex = docGroupsMap.get(it.doc.id);
|
|
if (ex) ex.items.push(it);
|
|
else docGroupsMap.set(it.doc.id, { doc: it.doc, items: [it] });
|
|
}
|
|
|
|
return (
|
|
<div
|
|
key={folderName}
|
|
className="border border-border rounded-card bg-canvas overflow-hidden"
|
|
>
|
|
{/* Folder Header */}
|
|
<div className="p-2 bg-canvas border-b border-border flex items-center justify-between">
|
|
<div
|
|
onClick={() => {
|
|
setCollapsedTodoFolders((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(folderName)) next.delete(folderName);
|
|
else next.add(folderName);
|
|
return next;
|
|
});
|
|
}}
|
|
className="flex items-center gap-2 cursor-pointer select-none"
|
|
>
|
|
{isCollapsed ? (
|
|
<ChevronRight className="w-3.5 h-3.5 text-faint" />
|
|
) : (
|
|
<ChevronDown className="w-3.5 h-3.5 text-accent" />
|
|
)}
|
|
<Folder className="w-3.5 h-3.5 text-accent" />
|
|
<span className="font-bold text-xs text-main">{folderName}</span>
|
|
<span className="badge-neutral text-[10px] font-mono">
|
|
{openCount} offen
|
|
</span>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => setTodoFolderDrilldown(folderName)}
|
|
className="text-[10px] text-muted hover:text-main px-1.5 py-0.5 rounded-btn hover:bg-canvas cursor-pointer"
|
|
title="Diesen Ordner isolieren"
|
|
>
|
|
Nur diesen Ordner
|
|
</button>
|
|
</div>
|
|
|
|
{/* Folder Notes Subgroups */}
|
|
{!isCollapsed && (
|
|
<div className="p-2 space-y-3">
|
|
{Array.from(docGroupsMap.values()).map(({ doc, items: docItems }) => {
|
|
const docOpenCount = docItems.filter((i) => !i.checked).length;
|
|
const isDocCollapsed = collapsedTodoNotes.has(doc.id);
|
|
return (
|
|
<div
|
|
key={doc.id}
|
|
className="border border-border/60 rounded-btn p-2 bg-panel-elevated/30 space-y-1.5"
|
|
>
|
|
<div className="flex items-center justify-between pb-1 border-b border-border/40">
|
|
<div
|
|
onClick={() => {
|
|
setCollapsedTodoNotes((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(doc.id)) next.delete(doc.id);
|
|
else next.add(doc.id);
|
|
return next;
|
|
});
|
|
}}
|
|
className="flex items-center gap-1.5 text-xs font-semibold text-main hover:text-accent cursor-pointer select-none"
|
|
>
|
|
{isDocCollapsed ? (
|
|
<ChevronRight className="w-3 h-3 text-faint" />
|
|
) : (
|
|
<ChevronDown className="w-3 h-3 text-accent" />
|
|
)}
|
|
<span>{doc.icon || '📝'}</span>
|
|
<span>{doc.title}</span>
|
|
<span className="badge-neutral text-[9px] font-mono">
|
|
{docOpenCount} offen
|
|
</span>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => handleJumpToDocBlock(doc.id)}
|
|
className="text-[10px] text-accent hover:underline px-1.5 py-0.5 rounded-btn hover:bg-canvas cursor-pointer flex items-center gap-1"
|
|
title="Notiz öffnen"
|
|
>
|
|
<span>Notiz öffnen</span>
|
|
<ExternalLink className="w-2.5 h-2.5" />
|
|
</button>
|
|
</div>
|
|
|
|
{!isDocCollapsed && (
|
|
<div className="space-y-0.5">
|
|
{docItems.map((item) => (
|
|
<div
|
|
key={`${item.doc.id}-${item.blockId}`}
|
|
className="flex items-center justify-between gap-2 p-1 rounded-btn hover:bg-canvas text-xs transition-colors group cursor-pointer"
|
|
>
|
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleToggleRollupTodo(
|
|
item.doc,
|
|
item.blockId,
|
|
item.checked
|
|
);
|
|
}}
|
|
className={`w-3.5 h-3.5 checkbox-square flex items-center justify-center transition-colors border cursor-pointer ${
|
|
item.checked
|
|
? 'bg-status-ok border-status-ok text-on-accent'
|
|
: 'border-strong hover:border-accent text-transparent'
|
|
}`}
|
|
>
|
|
<Check className="w-2.5 h-2.5 stroke-[3]" />
|
|
</button>
|
|
<span
|
|
onClick={() =>
|
|
handleJumpToDocBlock(item.doc.id, item.blockId)
|
|
}
|
|
className={`truncate flex-1 ${
|
|
item.checked
|
|
? 'line-through text-faint'
|
|
: 'text-main font-medium group-hover:text-accent'
|
|
}`}
|
|
>
|
|
{item.content}
|
|
</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() =>
|
|
handleJumpToDocBlock(item.doc.id, item.blockId)
|
|
}
|
|
className="text-faint hover:text-accent opacity-0 group-hover:opacity-100 transition-opacity p-1"
|
|
title="Zu dieser Zeile springen"
|
|
>
|
|
<ExternalLink className="w-3 h-3" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
) : (
|
|
/* Grouped by Folder (Flat tasks inside folder) */
|
|
<div className="space-y-3">
|
|
{Array.from(rollupTodosByFolder.entries()).map(([folderName, items]) => {
|
|
const isCollapsed = collapsedTodoFolders.has(folderName);
|
|
const openCount = items.filter((i) => !i.checked).length;
|
|
|
|
return (
|
|
<div
|
|
key={folderName}
|
|
className="border border-border rounded-card bg-canvas overflow-hidden"
|
|
>
|
|
{/* Folder Header */}
|
|
<div className="p-2 bg-canvas border-b border-border flex items-center justify-between">
|
|
<div
|
|
onClick={() => {
|
|
setCollapsedTodoFolders((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(folderName)) next.delete(folderName);
|
|
else next.add(folderName);
|
|
return next;
|
|
});
|
|
}}
|
|
className="flex items-center gap-2 cursor-pointer select-none"
|
|
>
|
|
{isCollapsed ? (
|
|
<ChevronRight className="w-3.5 h-3.5 text-faint" />
|
|
) : (
|
|
<ChevronDown className="w-3.5 h-3.5 text-accent" />
|
|
)}
|
|
<Folder className="w-3.5 h-3.5 text-accent" />
|
|
<span className="font-bold text-xs text-main">{folderName}</span>
|
|
<span className="badge-neutral text-[10px] font-mono">
|
|
{openCount} offen
|
|
</span>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={() => setTodoFolderDrilldown(folderName)}
|
|
className="text-[10px] text-muted hover:text-main px-1.5 py-0.5 rounded-btn hover:bg-canvas cursor-pointer"
|
|
title="Diesen Ordner isolieren"
|
|
>
|
|
Nur diesen Ordner
|
|
</button>
|
|
</div>
|
|
|
|
{/* Folder Tasks */}
|
|
{!isCollapsed && (
|
|
<div className="p-2 space-y-1">
|
|
{items.map((item) => (
|
|
<div
|
|
key={`${item.doc.id}-${item.blockId}`}
|
|
className="flex items-center justify-between gap-2 p-1.5 rounded-btn hover:bg-panel-elevated text-xs transition-colors group cursor-pointer"
|
|
>
|
|
<div className="flex items-center gap-2 min-w-0 flex-1">
|
|
<button
|
|
type="button"
|
|
onClick={(e) => {
|
|
e.stopPropagation();
|
|
handleToggleRollupTodo(item.doc, item.blockId, item.checked);
|
|
}}
|
|
className={`w-3.5 h-3.5 checkbox-square flex items-center justify-center transition-colors border cursor-pointer ${
|
|
item.checked
|
|
? 'bg-status-ok border-status-ok text-on-accent'
|
|
: 'border-strong hover:border-accent text-transparent'
|
|
}`}
|
|
>
|
|
<Check className="w-2.5 h-2.5 stroke-[3]" />
|
|
</button>
|
|
<span
|
|
onClick={() => handleJumpToDocBlock(item.doc.id, item.blockId)}
|
|
className={`truncate flex-1 ${
|
|
item.checked
|
|
? 'line-through text-faint'
|
|
: 'text-main font-medium group-hover:text-accent'
|
|
}`}
|
|
>
|
|
{item.content}
|
|
</span>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleJumpToDocBlock(item.doc.id, item.blockId)}
|
|
className="text-[11px] text-accent hover:underline flex items-center gap-1 shrink-0 cursor-pointer"
|
|
title="Zu dieser Zeile springen"
|
|
>
|
|
<span>{item.doc.icon || '📝'}</span>
|
|
<span className="truncate max-w-[120px]">{item.doc.title}</span>
|
|
<ExternalLink className="w-2.5 h-2.5 opacity-60" />
|
|
</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex-1 flex flex-col items-center justify-center p-12 text-center bg-canvas">
|
|
<FileText className="w-10 h-10 text-faint mb-2" />
|
|
<h3 className="text-sm font-semibold text-main mb-1">
|
|
Keine Notiz ausgewählt
|
|
</h3>
|
|
<p className="text-xs text-muted max-w-sm mb-3">
|
|
Wähle links in der Seitenleiste eine bestehende Notiz aus oder erstelle eine neue.
|
|
</p>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleCreateDocument('')}
|
|
className="btn-primary inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-semibold"
|
|
>
|
|
<Plus className="w-3.5 h-3.5" />
|
|
<span>Notiz erstellen</span>
|
|
</button>
|
|
</div>
|
|
)}
|
|
</main>
|
|
|
|
{/* New Folder Modal */}
|
|
{showNewFolderModal && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-overlay backdrop-blur-sm p-4">
|
|
<form
|
|
onSubmit={handleCreateFolderSubmit}
|
|
className="w-full max-w-md panel-surface shadow-panel border border-border rounded-card p-4 space-y-3"
|
|
>
|
|
<div className="flex items-center justify-between">
|
|
<h3 className="text-sm font-bold text-main">
|
|
Neuen Ordner anlegen
|
|
</h3>
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowNewFolderModal(false)}
|
|
className="p-1 rounded-btn text-faint hover:text-main"
|
|
>
|
|
<X className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="space-y-1">
|
|
<label className="text-[11px] font-medium text-muted">
|
|
Ordnerpfad (z.B. <code>Projekte/2026</code>)
|
|
</label>
|
|
<input
|
|
type="text"
|
|
autoFocus
|
|
value={newFolderName}
|
|
onChange={(e) => setNewFolderName(e.target.value)}
|
|
placeholder="Projekte"
|
|
className="input-field w-full px-2.5 py-1.5 text-xs"
|
|
/>
|
|
</div>
|
|
|
|
<label className="flex items-center gap-2 text-xs text-muted cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
checked={newFolderIsShared}
|
|
onChange={(e) => setNewFolderIsShared(e.target.checked)}
|
|
className="checkbox-square"
|
|
/>
|
|
<span>Als geteilten Team-Ordner freigeben</span>
|
|
</label>
|
|
|
|
<div className="flex justify-end gap-2 pt-1">
|
|
<button
|
|
type="button"
|
|
onClick={() => setShowNewFolderModal(false)}
|
|
className="btn-action px-3 py-1 text-xs"
|
|
>
|
|
Abbrechen
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="btn-primary px-3 py-1 text-xs font-semibold"
|
|
>
|
|
Erstellen
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
{/* Unified Sharing Modal */}
|
|
{(shareModalDoc || shareModalFolder) && (
|
|
<UnifiedShareModal
|
|
isOpen={true}
|
|
onClose={() => {
|
|
setShareModalDoc(null);
|
|
setShareModalFolder(null);
|
|
}}
|
|
resourceName={shareModalDoc ? shareModalDoc.title : shareModalFolder?.name || ''}
|
|
resourceIcon={shareModalDoc ? shareModalDoc.icon : '📁'}
|
|
isOwner={
|
|
shareModalDoc
|
|
? (shareModalDoc.isOwner || shareModalDoc.permission === 'owner')
|
|
: Boolean(shareModalFolder?.isOwner)
|
|
}
|
|
currentUserId={currentUser?.id}
|
|
shares={unifiedShares}
|
|
teams={teams}
|
|
collaborators={collaborators}
|
|
onAddShare={async (targetType, targetId, permission) => {
|
|
const perm = permission === 'editor' ? 'write' : 'read';
|
|
if (shareModalDoc) {
|
|
await api.createDocShare(
|
|
shareModalDoc.id,
|
|
targetType === 'team'
|
|
? { teamId: targetId, permission: perm }
|
|
: { userId: targetId, permission: perm }
|
|
);
|
|
const updatedShares = await api.getDocShares(shareModalDoc.id);
|
|
setActiveDocShares(updatedShares);
|
|
} else if (shareModalFolder) {
|
|
await api.createFolderShare(shareModalFolder.id, {
|
|
targetType,
|
|
targetId,
|
|
permission: perm,
|
|
});
|
|
const updatedFolder = await api.getSharedFolder(shareModalFolder.id);
|
|
setShareModalFolder(updatedFolder);
|
|
}
|
|
}}
|
|
onRemoveShare={async (shareId) => {
|
|
if (shareModalDoc) {
|
|
await api.deleteDocShare(shareModalDoc.id, shareId);
|
|
const updatedShares = await api.getDocShares(shareModalDoc.id);
|
|
setActiveDocShares(updatedShares);
|
|
} else if (shareModalFolder) {
|
|
await api.deleteFolderShare(shareModalFolder.id, shareId);
|
|
const updatedFolder = await api.getSharedFolder(shareModalFolder.id);
|
|
setShareModalFolder(updatedFolder);
|
|
}
|
|
}}
|
|
/>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|