Dev #5

Merged
sandrodach merged 19 commits from dev into main 2026-10-26 13:35:00 +01:00
5 changed files with 323 additions and 108 deletions
Showing only changes of commit e088ef6974 - Show all commits

No files matched your search

+104 -108
View File
@@ -50,7 +50,7 @@ import {
Radio, Radio,
} from 'lucide-react'; } from 'lucide-react';
import { BlockType, Document, DocumentBlock, DocBlocksPatchEvent, DocLockInfo, DocShare, FolderShare, SharedFolder, TagDetails, TagSummary, Team, User } from '../../lib/types'; import { BlockType, Document, DocumentBlock, DocBlocksPatchEvent, DocLockInfo, DocShare, FolderShare, SharedFolder, TagDetails, TagSummary, Team, User } from '../../lib/types';
import { api, getToken, triggerHaptic, onWebSocketStatus, isWebSocketConnected } from '../../lib/api'; import { api, getToken, triggerHaptic, onWebSocketStatus, isWebSocketConnected, buildWebSocketURL } from '../../lib/api';
import { CodeBlockItem } from './CodeBlockItem'; import { CodeBlockItem } from './CodeBlockItem';
import { UserAutocomplete } from '../UserAutocomplete'; import { UserAutocomplete } from '../UserAutocomplete';
import { UnifiedShareModal } from '../../components/UnifiedShareModal'; import { UnifiedShareModal } from '../../components/UnifiedShareModal';
@@ -129,20 +129,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
const [showIconPicker, setShowIconPicker] = useState(false); const [showIconPicker, setShowIconPicker] = useState(false);
const [showWikiLinkMenu, setShowWikiLinkMenu] = useState(false); const [showWikiLinkMenu, setShowWikiLinkMenu] = useState(false);
// WebSocket connection status for live indicator // Dedicated WebSocket connection & status for live indicator
const [wsConnected, setWsConnected] = useState<boolean>(() => isWebSocketConnected()); const wsRef = useRef<WebSocket | null>(null);
const wsReconnectTimerRef = useRef<any>(null);
useEffect(() => { const [wsConnected, setWsConnected] = useState<boolean>(false);
const unsub = onWebSocketStatus((connected) => {
setWsConnected(connected);
if (connected) {
// Fresh re-fetch when reconnecting
api.getFolders().then(setCustomFolders).catch(() => {});
api.getSharedFolders().then(setSharedFolders).catch(() => {});
}
});
return unsub;
}, []);
// Folder state (synchronized with server) // Folder state (synchronized with server)
const [customFolders, setCustomFolders] = useState<string[]>([]); const [customFolders, setCustomFolders] = useState<string[]>([]);
@@ -237,7 +227,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
const blockInputRefs = useRef<Record<string, HTMLTextAreaElement | HTMLInputElement | null>>({}); const blockInputRefs = useRef<Record<string, HTMLTextAreaElement | HTMLInputElement | null>>({});
const isLockedByOther = !!(activeLock && activeLock.userId !== currentUser?.id); const isLockedByOther = !!(activeLock && activeLock.userId !== currentUser?.id);
const isReadOnly = selectedDoc?.permission === 'read'; const isReadOnly = selectedDoc?.permission === 'read' || isLockedByOther;
// Track server-synced blocks & metadata per document, plus local unsaved block deltas // Track server-synced blocks & metadata per document, plus local unsaved block deltas
const syncedDocMetaRef = useRef<Map<string, { title: string; icon: string; tagsJson: string; isPinned: boolean }>>(new Map()); const syncedDocMetaRef = useRef<Map<string, { title: string; icon: string; tagsJson: string; isPinned: boolean }>>(new Map());
@@ -820,10 +810,11 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
}; };
}, []); }, []);
// Real-time WebSocket listener for Document, Block Delta & Folder events // Dedicated Real-time WebSocket connection for Document, Block Delta, Lock & Folder events
useEffect(() => { useEffect(() => {
const handleWs = (e: any) => { let isMounted = true;
const msg = e.detail;
const handleWsMsg = (msg: any) => {
if (!msg || !msg.event) return; if (!msg || !msg.event) return;
if (msg.event === 'DOC_LOCKED') { if (msg.event === 'DOC_LOCKED') {
@@ -957,8 +948,80 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
} }
}; };
window.addEventListener('mindsift:ws', handleWs); const connect = () => {
return () => window.removeEventListener('mindsift:ws', handleWs); if (!isMounted) return;
const token = getToken();
if (!token) {
setWsConnected(false);
return;
}
if (wsRef.current) {
try {
wsRef.current.close();
} catch {}
wsRef.current = null;
}
try {
const url = buildWebSocketURL();
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onopen = () => {
if (!isMounted) return;
setWsConnected(true);
api.getFolders().then(setCustomFolders).catch(() => {});
api.getSharedFolders().then(setSharedFolders).catch(() => {});
};
ws.onclose = () => {
if (!isMounted) return;
setWsConnected(false);
clearTimeout(wsReconnectTimerRef.current);
wsReconnectTimerRef.current = setTimeout(connect, 2000);
};
ws.onerror = () => {
if (!isMounted) return;
setWsConnected(false);
};
ws.onmessage = (evt) => {
try {
const msg = JSON.parse(evt.data);
handleWsMsg(msg);
} catch {}
};
} catch (err) {
if (!isMounted) return;
setWsConnected(false);
clearTimeout(wsReconnectTimerRef.current);
wsReconnectTimerRef.current = setTimeout(connect, 3000);
}
};
connect();
// Fallback: also listen for window custom event 'mindsift:ws'
const handleGlobalCustomEvent = (e: any) => {
if (e.detail) {
handleWsMsg(e.detail);
}
};
window.addEventListener('mindsift:ws', handleGlobalCustomEvent);
return () => {
isMounted = false;
clearTimeout(wsReconnectTimerRef.current);
if (wsRef.current) {
try {
wsRef.current.close();
} catch {}
wsRef.current = null;
}
window.removeEventListener('mindsift:ws', handleGlobalCustomEvent);
};
}, [onStatsChange]); }, [onStatsChange]);
const openShareModal = async () => { const openShareModal = async () => {
@@ -1278,91 +1341,24 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
return; return;
} }
// Name of the folder being moved
const folderName = cleanSource.split('/').pop() || cleanSource;
const newFolderPath = cleanTarget ? `${cleanTarget}/${folderName}` : folderName;
if (newFolderPath === cleanSource) return;
triggerHaptic(15); 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);
return f === cleanSource || f.startsWith(cleanSource + '/');
});
const updatedDocsList = docs.map((d) => {
const f = getDocFolder(d);
if (f === cleanSource) {
return { ...d, folderPath: newFolderPath };
} else if (f.startsWith(cleanSource + '/')) {
const suffix = f.slice(cleanSource.length);
return { ...d, folderPath: `${newFolderPath}${suffix}` };
}
return d;
});
setDocs(updatedDocsList);
if (selectedDoc) {
const found = updatedDocsList.find((d) => d.id === selectedDoc.id);
if (found) setSelectedDoc(found);
}
try { try {
await api.createFolder(newFolderPath); const res = await api.moveFolder(cleanSource, cleanTarget);
const subFoldersToRecreate = customFolders.filter((cf) => cf.startsWith(cleanSource + '/')); await Promise.all([
for (const sf of subFoldersToRecreate) { api.getFolders().then(setCustomFolders),
const suffix = sf.slice(cleanSource.length); api.getSharedFolders().then(setSharedFolders),
await api.createFolder(`${newFolderPath}${suffix}`); loadDocs(selectedDoc?.id),
} ]);
// Persist all affected docs to backend BEFORE deleting the old empty folder setExpandedFolders((prev) => {
await Promise.all( const next = new Set(prev);
docsToUpdate.map((d) => { if (res.newPath) next.add(res.newPath);
const f = getDocFolder(d); if (cleanTarget) next.add(cleanTarget);
let nextFolder = newFolderPath; return next;
if (f.startsWith(cleanSource + '/')) { });
const suffix = f.slice(cleanSource.length); } catch (err: any) {
nextFolder = `${newFolderPath}${suffix}`; console.error('Failed to move folder', err);
} alert(err.message || 'Fehler beim Verschieben des Ordners');
return api.moveDocument(d.id, nextFolder);
})
);
await api.deleteFolder(cleanSource);
const freshFolders = await api.getFolders();
setCustomFolders(freshFolders);
} catch (err) {
console.error('Error syncing moved folders with server', err);
} }
// Expand the new folder path and its parent
setExpandedFolders((prev) => {
const next = new Set(prev);
next.add(newFolderPath);
if (cleanTarget) next.add(cleanTarget);
return next;
});
}; };
const handleDropDocToFolder = async (folderPath: string, e: React.DragEvent) => { const handleDropDocToFolder = async (folderPath: string, e: React.DragEvent) => {
@@ -4216,16 +4212,16 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
{isLockedByOther && ( {isLockedByOther && (
<div className="p-2.5 bg-emerald-500/10 border border-emerald-500/30 text-emerald-700 dark:text-emerald-300 rounded-card flex items-center gap-2.5 text-xs font-medium"> <div className="p-3 bg-amber-500/10 border border-amber-500/30 text-amber-700 dark:text-amber-300 rounded-card flex items-center gap-2.5 text-xs font-medium animate-pulse">
<Radio className="w-4 h-4 text-emerald-500 animate-pulse flex-shrink-0" /> <Lock className="w-4 h-4 text-amber-500 flex-shrink-0" />
<div> <div>
<span><strong>{activeLock?.userName || 'Ein anderer Benutzer'}</strong> bearbeitet diese Notiz gerade ebenfalls.</span> <span>Diese Notiz wird aktuell von <strong>{activeLock?.userName || 'einem anderen Benutzer'}</strong> bearbeitet.</span>
<p className="text-[11px] opacity-80 mt-0.5">Live-Zusammenarbeit aktiv – Änderungen werden automatisch Punkt für Punkt synchronisiert.</p> <p className="text-[11px] opacity-80 mt-0.5">Die Bearbeitung ist vorübergehend gesperrt, um Zeilen- und Datenverlust zu vermeiden.</p>
</div> </div>
</div> </div>
)} )}
{selectedDoc.permission === 'read' && ( {!isLockedByOther && selectedDoc.permission === 'read' && (
<div className="p-2.5 bg-blue-500/10 border border-blue-500/30 text-blue-700 dark:text-blue-300 rounded-card flex items-center gap-2 text-xs"> <div className="p-2.5 bg-blue-500/10 border border-blue-500/30 text-blue-700 dark:text-blue-300 rounded-card flex items-center gap-2 text-xs">
<Eye className="w-4 h-4 text-blue-500 flex-shrink-0" /> <Eye className="w-4 h-4 text-blue-500 flex-shrink-0" />
<span>Du hast <strong>Nur-Lesezugriff</strong> auf diese Notiz. Änderungen können nicht vorgenommen werden.</span> <span>Du hast <strong>Nur-Lesezugriff</strong> auf diese Notiz. Änderungen können nicht vorgenommen werden.</span>
+7
View File
@@ -434,6 +434,13 @@ export const api = {
); );
}, },
async moveFolder(sourcePath: string, targetParentPath: string): Promise<{ newPath: string; movedDocIds: string[] }> {
return request<{ newPath: string; movedDocIds: string[] }>('/api/v1/folders/move', {
method: 'POST',
body: JSON.stringify({ sourcePath, targetParentPath }),
});
},
// --- WORKSPACE SHARED FOLDERS --- // --- WORKSPACE SHARED FOLDERS ---
async getSharedFolders(): Promise<SharedFolder[]> { async getSharedFolders(): Promise<SharedFolder[]> {
const res = await request<SharedFolder[]>('/api/v1/workspace/folders/shared'); const res = await request<SharedFolder[]>('/api/v1/workspace/folders/shared');
+56
View File
@@ -104,6 +104,7 @@ func (s *Server) Router() http.Handler {
// Workspace Documents & Folders // Workspace Documents & Folders
r.Get("/api/v1/folders", s.handleGetFolders) r.Get("/api/v1/folders", s.handleGetFolders)
r.Post("/api/v1/folders", s.handleCreateFolder) r.Post("/api/v1/folders", s.handleCreateFolder)
r.Post("/api/v1/folders/move", s.handleMoveFolder)
r.Delete("/api/v1/folders", s.handleDeleteFolder) r.Delete("/api/v1/folders", s.handleDeleteFolder)
r.Get("/api/v1/workspace/folders/shared", s.handleGetSharedFolders) r.Get("/api/v1/workspace/folders/shared", s.handleGetSharedFolders)
@@ -816,6 +817,7 @@ func (s *Server) handleCreateDocument(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, err.Error()) writeError(w, http.StatusBadRequest, err.Error())
return return
} }
s.wsHub.BroadcastDocEvent(doc.ID, "DOC_CREATED", doc)
s.broadcastDocToAuthorizedUsers(doc.ID, "DOC_CREATED") s.broadcastDocToAuthorizedUsers(doc.ID, "DOC_CREATED")
writeJSON(w, http.StatusCreated, doc) writeJSON(w, http.StatusCreated, doc)
} }
@@ -835,6 +837,16 @@ func (s *Server) handleUpdateDocument(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context()) userID := auth.GetUserID(r.Context())
docID := chi.URLParam(r, "id") docID := chi.URLParam(r, "id")
// Check document lock
if lock := s.wsHub.GetDocLock(docID); lock != nil && lock.UserID != userID {
w.WriteHeader(http.StatusLocked)
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"error": "Dokument wird aktuell bearbeitet von " + lock.UserName,
"currentLock": lock,
})
return
}
var req struct { var req struct {
Title *string `json:"title"` Title *string `json:"title"`
Icon *string `json:"icon"` Icon *string `json:"icon"`
@@ -854,6 +866,7 @@ func (s *Server) handleUpdateDocument(w http.ResponseWriter, r *http.Request) {
return return
} }
if req.Title != nil || req.Icon != nil || req.FolderPath != nil || req.Blocks != nil || req.Tags != nil { if req.Title != nil || req.Icon != nil || req.FolderPath != nil || req.Blocks != nil || req.Tags != nil {
s.wsHub.BroadcastDocEvent(docID, "DOC_UPDATED", doc)
s.broadcastDocToAuthorizedUsers(docID, "DOC_UPDATED") s.broadcastDocToAuthorizedUsers(docID, "DOC_UPDATED")
} }
writeJSON(w, http.StatusOK, doc) writeJSON(w, http.StatusOK, doc)
@@ -863,6 +876,16 @@ func (s *Server) handlePatchDocumentBlocks(w http.ResponseWriter, r *http.Reques
userID := auth.GetUserID(r.Context()) userID := auth.GetUserID(r.Context())
docID := chi.URLParam(r, "id") docID := chi.URLParam(r, "id")
// Check document lock
if lock := s.wsHub.GetDocLock(docID); lock != nil && lock.UserID != userID {
w.WriteHeader(http.StatusLocked)
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"error": "Dokument wird aktuell bearbeitet von " + lock.UserName,
"currentLock": lock,
})
return
}
var req domain.DocBlocksPatchRequest var req domain.DocBlocksPatchRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil { if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON") writeError(w, http.StatusBadRequest, "Ungültiges JSON")
@@ -883,6 +906,7 @@ func (s *Server) handlePatchDocumentBlocks(w http.ResponseWriter, r *http.Reques
DeletedIDs: req.DeletedIDs, DeletedIDs: req.DeletedIDs,
BlockOrder: mergedOrder, BlockOrder: mergedOrder,
} }
s.wsHub.BroadcastDocEvent(docID, "DOC_BLOCKS_PATCHED", patchEvent)
if users, err := s.store.GetAuthorizedDocUsers(docID); err == nil { if users, err := s.store.GetAuthorizedDocUsers(docID); err == nil {
for _, uid := range users { for _, uid := range users {
s.wsHub.BroadcastToUser(uid, "DOC_BLOCKS_PATCHED", patchEvent) s.wsHub.BroadcastToUser(uid, "DOC_BLOCKS_PATCHED", patchEvent)
@@ -959,6 +983,37 @@ func (s *Server) handleDeleteFolder(w http.ResponseWriter, r *http.Request) {
}) })
} }
func (s *Server) handleMoveFolder(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
var req struct {
SourcePath string `json:"sourcePath"`
TargetParentPath string `json:"targetParentPath"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
newPath, movedDocIDs, err := s.store.MoveFolder(userID, req.SourcePath, req.TargetParentPath)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return
}
for _, id := range movedDocIDs {
if doc, err := s.store.GetDocumentByID(userID, id); err == nil && doc != nil {
s.wsHub.BroadcastDocEvent(id, "DOC_UPDATED", doc)
s.broadcastDocToAuthorizedUsers(id, "DOC_UPDATED")
}
}
s.wsHub.BroadcastDocEvent("", "WORKSPACE_FOLDERS_UPDATED", map[string]string{
"sourcePath": req.SourcePath,
"newPath": newPath,
})
writeJSON(w, http.StatusOK, map[string]interface{}{
"newPath": newPath,
"movedDocIds": movedDocIDs,
})
}
// --- WORKSPACE SHARED FOLDERS --- // --- WORKSPACE SHARED FOLDERS ---
func (s *Server) handleGetSharedFolders(w http.ResponseWriter, r *http.Request) { func (s *Server) handleGetSharedFolders(w http.ResponseWriter, r *http.Request) {
@@ -1107,6 +1162,7 @@ func (s *Server) handleMoveDocument(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, err.Error()) writeError(w, http.StatusBadRequest, err.Error())
return return
} }
s.wsHub.BroadcastDocEvent(docID, "DOC_UPDATED", doc)
s.broadcastDocToAuthorizedUsers(docID, "DOC_UPDATED") s.broadcastDocToAuthorizedUsers(docID, "DOC_UPDATED")
s.wsHub.BroadcastDocEvent("", "WORKSPACE_FOLDERS_UPDATED", nil) s.wsHub.BroadcastDocEvent("", "WORKSPACE_FOLDERS_UPDATED", nil)
writeJSON(w, http.StatusOK, doc) writeJSON(w, http.StatusOK, doc)
+132
View File
@@ -2807,6 +2807,138 @@ func (s *Store) MoveDocument(userID, docID, targetFolderPath string) (*domain.Do
return doc, nil return doc, nil
} }
// MoveFolder moves a folder (and all its documents/subfolders) to a new target parent path.
func (s *Store) MoveFolder(userID, sourcePath, targetParentPath string) (string, []string, error) {
cleanSource := filepath.ToSlash(filepath.Clean(strings.TrimSpace(sourcePath)))
if cleanSource == "" || cleanSource == "." || strings.HasPrefix(cleanSource, "/") || strings.Contains(cleanSource, "..") {
return "", nil, fmt.Errorf("ungültiger Quell-Ordnerpfad")
}
cleanTarget := filepath.ToSlash(filepath.Clean(strings.TrimSpace(targetParentPath)))
if cleanTarget == "." {
cleanTarget = ""
}
if cleanTarget != "" && (cleanTarget == cleanSource || strings.HasPrefix(cleanTarget, cleanSource+"/")) {
return "", nil, fmt.Errorf("ein Ordner kann nicht in sich selbst verschoben werden")
}
folderName := filepath.Base(cleanSource)
var newFolderPath string
if cleanTarget == "" {
newFolderPath = folderName
} else {
newFolderPath = cleanTarget + "/" + folderName
}
if newFolderPath == cleanSource {
return cleanSource, []string{}, nil
}
// 1. Check if sourcePath is the root mount of a shared folder
mounts, _, err := s.getUserFolderMounts(userID)
if err == nil {
for sfID, mPath := range mounts {
if mPath == cleanSource {
err := s.UpdateFolderMount(userID, sfID, newFolderPath)
if err != nil {
return "", nil, err
}
return newFolderPath, []string{}, nil
}
}
}
// 2. Move all affected documents
docs, err := s.GetDocuments(userID)
if err != nil {
return "", nil, err
}
var movedDocIDs []string
for _, doc := range docs {
if doc.FolderPath == cleanSource || strings.HasPrefix(doc.FolderPath, cleanSource+"/") {
var nextDocFolder string
if doc.FolderPath == cleanSource {
nextDocFolder = newFolderPath
} else {
suffix := strings.TrimPrefix(doc.FolderPath, cleanSource)
nextDocFolder = newFolderPath + suffix
}
_, moveErr := s.MoveDocument(userID, doc.ID, nextDocFolder)
if moveErr == nil {
movedDocIDs = append(movedDocIDs, doc.ID)
}
}
}
// 3. Re-map workspace_folders entries
targetSFID, targetSub, isTargetSF := matchMountPath(newFolderPath, mounts)
sourceSFID, sourceSub, isSourceSF := matchMountPath(cleanSource, mounts)
wfRows, qErr := s.db.Query(`SELECT id, user_id, shared_folder_id, path, sub_path FROM workspace_folders`)
if qErr == nil {
type wfRecord struct {
id, uid, sfID, path, subPath string
}
var records []wfRecord
for wfRows.Next() {
var r wfRecord
if err := wfRows.Scan(&r.id, &r.uid, &r.sfID, &r.path, &r.subPath); err == nil {
records = append(records, r)
}
}
_ = wfRows.Close()
for _, r := range records {
if isSourceSF && r.sfID == sourceSFID {
matches := false
var suffix string
if sourceSub == "" {
matches = true
suffix = "/" + r.subPath
} else if r.subPath == sourceSub {
matches = true
suffix = ""
} else if strings.HasPrefix(r.subPath, sourceSub+"/") {
matches = true
suffix = strings.TrimPrefix(r.subPath, sourceSub)
}
if matches {
var newSub string
if targetSub == "" {
newSub = strings.TrimPrefix(folderName+suffix, "/")
} else {
newSub = targetSub + "/" + folderName + suffix
}
if isTargetSF {
_, _ = s.db.Exec(`UPDATE workspace_folders SET shared_folder_id = ?, sub_path = ? WHERE id = ?`, targetSFID, newSub, r.id)
} else {
newP := newFolderPath + suffix
_, _ = s.db.Exec(`UPDATE workspace_folders SET shared_folder_id = '', sub_path = '', path = ? WHERE id = ?`, newP, r.id)
}
}
} else if r.path == cleanSource || strings.HasPrefix(r.path, cleanSource+"/") {
suffix := strings.TrimPrefix(r.path, cleanSource)
newP := newFolderPath + suffix
if isTargetSF {
var newSub string
if targetSub == "" {
newSub = strings.TrimPrefix(folderName+suffix, "/")
} else {
newSub = targetSub + "/" + folderName + suffix
}
_, _ = s.db.Exec(`UPDATE workspace_folders SET shared_folder_id = ?, sub_path = ?, path = ? WHERE id = ?`, targetSFID, newSub, newP, r.id)
} else {
_, _ = s.db.Exec(`UPDATE workspace_folders SET path = ? WHERE id = ?`, newP, r.id)
}
}
}
}
_, _ = s.CreateFolder(userID, newFolderPath)
return newFolderPath, movedDocIDs, nil
}
// syncSharedFolderContents ensures all documents and subfolders under cleanMount owned by ownerID are linked to folderID. // syncSharedFolderContents ensures all documents and subfolders under cleanMount owned by ownerID are linked to folderID.
func (s *Store) syncSharedFolderContents(ownerID, folderID, cleanMount string) { func (s *Store) syncSharedFolderContents(ownerID, folderID, cleanMount string) {
if ownerID == "" || folderID == "" || cleanMount == "" { if ownerID == "" || folderID == "" || cleanMount == "" {
+24
View File
@@ -665,5 +665,29 @@ func TestDatabaseFirstWorkspaceArchitecture(t *testing.T) {
if !foundSubfolderForB { if !foundSubfolderForB {
t.Fatalf("expected User B to see empty shared subfolder 'ExterneProjekte/TeamProjekt/Konzepte', got folders: %v", foldersB) t.Fatalf("expected User B to see empty shared subfolder 'ExterneProjekte/TeamProjekt/Konzepte', got folders: %v", foldersB)
} }
// 3. MoveFolder inside a shared folder
newPath, _, err := store.MoveFolder(userA.ID, "TeamProjekt/Konzepte", "TeamProjekt/Archiv")
if err != nil {
t.Fatalf("MoveFolder failed: %v", err)
}
if newPath != "TeamProjekt/Archiv/Konzepte" {
t.Fatalf("expected newPath 'TeamProjekt/Archiv/Konzepte', got %q", newPath)
}
// User B must now see "ExterneProjekte/TeamProjekt/Archiv/Konzepte"
foldersBAfterMove, err := store.GetFolders(userB.ID)
if err != nil {
t.Fatalf("GetFolders after MoveFolder failed: %v", err)
}
foundMovedForB := false
for _, f := range foldersBAfterMove {
if f == "ExterneProjekte/TeamProjekt/Archiv/Konzepte" {
foundMovedForB = true
}
}
if !foundMovedForB {
t.Fatalf("expected User B to see moved shared subfolder 'ExterneProjekte/TeamProjekt/Archiv/Konzepte', got %v", foldersBAfterMove)
}
} }