Dev #5
No files matched your search
@@ -50,7 +50,7 @@ import {
|
||||
Radio,
|
||||
} from 'lucide-react';
|
||||
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 { UserAutocomplete } from '../UserAutocomplete';
|
||||
import { UnifiedShareModal } from '../../components/UnifiedShareModal';
|
||||
@@ -129,20 +129,10 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
const [showIconPicker, setShowIconPicker] = useState(false);
|
||||
const [showWikiLinkMenu, setShowWikiLinkMenu] = useState(false);
|
||||
|
||||
// WebSocket connection status for live indicator
|
||||
const [wsConnected, setWsConnected] = useState<boolean>(() => isWebSocketConnected());
|
||||
|
||||
useEffect(() => {
|
||||
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;
|
||||
}, []);
|
||||
// Dedicated WebSocket connection & status for live indicator
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
const wsReconnectTimerRef = useRef<any>(null);
|
||||
const [wsConnected, setWsConnected] = useState<boolean>(false);
|
||||
|
||||
// Folder state (synchronized with server)
|
||||
const [customFolders, setCustomFolders] = useState<string[]>([]);
|
||||
@@ -237,7 +227,7 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
const blockInputRefs = useRef<Record<string, HTMLTextAreaElement | HTMLInputElement | null>>({});
|
||||
|
||||
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
|
||||
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(() => {
|
||||
const handleWs = (e: any) => {
|
||||
const msg = e.detail;
|
||||
let isMounted = true;
|
||||
|
||||
const handleWsMsg = (msg: any) => {
|
||||
if (!msg || !msg.event) return;
|
||||
|
||||
if (msg.event === 'DOC_LOCKED') {
|
||||
@@ -957,8 +948,80 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('mindsift:ws', handleWs);
|
||||
return () => window.removeEventListener('mindsift:ws', handleWs);
|
||||
const connect = () => {
|
||||
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]);
|
||||
|
||||
const openShareModal = async () => {
|
||||
@@ -1278,91 +1341,24 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
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);
|
||||
|
||||
// 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);
|
||||
const res = await api.moveFolder(cleanSource, cleanTarget);
|
||||
await Promise.all([
|
||||
api.getFolders().then(setCustomFolders),
|
||||
api.getSharedFolders().then(setSharedFolders),
|
||||
loadDocs(selectedDoc?.id),
|
||||
]);
|
||||
setExpandedFolders((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.add(newFolderPath);
|
||||
if (res.newPath) next.add(res.newPath);
|
||||
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');
|
||||
console.error('Failed to move folder', err);
|
||||
alert(err.message || 'Fehler beim Verschieben des 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 {
|
||||
await api.createFolder(newFolderPath);
|
||||
const subFoldersToRecreate = customFolders.filter((cf) => cf.startsWith(cleanSource + '/'));
|
||||
for (const sf of subFoldersToRecreate) {
|
||||
const suffix = sf.slice(cleanSource.length);
|
||||
await api.createFolder(`${newFolderPath}${suffix}`);
|
||||
}
|
||||
// Persist all affected docs to backend BEFORE deleting the old empty folder
|
||||
await Promise.all(
|
||||
docsToUpdate.map((d) => {
|
||||
const f = getDocFolder(d);
|
||||
let nextFolder = newFolderPath;
|
||||
if (f.startsWith(cleanSource + '/')) {
|
||||
const suffix = f.slice(cleanSource.length);
|
||||
nextFolder = `${newFolderPath}${suffix}`;
|
||||
}
|
||||
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) => {
|
||||
@@ -4216,16 +4212,16 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
|
||||
|
||||
{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">
|
||||
<Radio className="w-4 h-4 text-emerald-500 animate-pulse flex-shrink-0" />
|
||||
<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">
|
||||
<Lock className="w-4 h-4 text-amber-500 flex-shrink-0" />
|
||||
<div>
|
||||
<span><strong>{activeLock?.userName || 'Ein anderer Benutzer'}</strong> bearbeitet diese Notiz gerade ebenfalls.</span>
|
||||
<p className="text-[11px] opacity-80 mt-0.5">Live-Zusammenarbeit aktiv – Änderungen werden automatisch Punkt für Punkt synchronisiert.</p>
|
||||
<span>Diese Notiz wird aktuell von <strong>{activeLock?.userName || 'einem anderen Benutzer'}</strong> bearbeitet.</span>
|
||||
<p className="text-[11px] opacity-80 mt-0.5">Die Bearbeitung ist vorübergehend gesperrt, um Zeilen- und Datenverlust zu vermeiden.</p>
|
||||
</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">
|
||||
<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>
|
||||
|
||||
@@ -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 ---
|
||||
async getSharedFolders(): Promise<SharedFolder[]> {
|
||||
const res = await request<SharedFolder[]>('/api/v1/workspace/folders/shared');
|
||||
|
||||
@@ -104,6 +104,7 @@ func (s *Server) Router() http.Handler {
|
||||
// Workspace Documents & Folders
|
||||
r.Get("/api/v1/folders", s.handleGetFolders)
|
||||
r.Post("/api/v1/folders", s.handleCreateFolder)
|
||||
r.Post("/api/v1/folders/move", s.handleMoveFolder)
|
||||
r.Delete("/api/v1/folders", s.handleDeleteFolder)
|
||||
|
||||
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())
|
||||
return
|
||||
}
|
||||
s.wsHub.BroadcastDocEvent(doc.ID, "DOC_CREATED", doc)
|
||||
s.broadcastDocToAuthorizedUsers(doc.ID, "DOC_CREATED")
|
||||
writeJSON(w, http.StatusCreated, doc)
|
||||
}
|
||||
@@ -835,6 +837,16 @@ func (s *Server) handleUpdateDocument(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
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 {
|
||||
Title *string `json:"title"`
|
||||
Icon *string `json:"icon"`
|
||||
@@ -854,6 +866,7 @@ func (s *Server) handleUpdateDocument(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
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")
|
||||
}
|
||||
writeJSON(w, http.StatusOK, doc)
|
||||
@@ -863,6 +876,16 @@ func (s *Server) handlePatchDocumentBlocks(w http.ResponseWriter, r *http.Reques
|
||||
userID := auth.GetUserID(r.Context())
|
||||
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
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
@@ -883,6 +906,7 @@ func (s *Server) handlePatchDocumentBlocks(w http.ResponseWriter, r *http.Reques
|
||||
DeletedIDs: req.DeletedIDs,
|
||||
BlockOrder: mergedOrder,
|
||||
}
|
||||
s.wsHub.BroadcastDocEvent(docID, "DOC_BLOCKS_PATCHED", patchEvent)
|
||||
if users, err := s.store.GetAuthorizedDocUsers(docID); err == nil {
|
||||
for _, uid := range users {
|
||||
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 ---
|
||||
|
||||
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())
|
||||
return
|
||||
}
|
||||
s.wsHub.BroadcastDocEvent(docID, "DOC_UPDATED", doc)
|
||||
s.broadcastDocToAuthorizedUsers(docID, "DOC_UPDATED")
|
||||
s.wsHub.BroadcastDocEvent("", "WORKSPACE_FOLDERS_UPDATED", nil)
|
||||
writeJSON(w, http.StatusOK, doc)
|
||||
|
||||
@@ -2807,6 +2807,138 @@ func (s *Store) MoveDocument(userID, docID, targetFolderPath string) (*domain.Do
|
||||
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.
|
||||
func (s *Store) syncSharedFolderContents(ownerID, folderID, cleanMount string) {
|
||||
if ownerID == "" || folderID == "" || cleanMount == "" {
|
||||
|
||||
@@ -665,5 +665,29 @@ func TestDatabaseFirstWorkspaceArchitecture(t *testing.T) {
|
||||
if !foundSubfolderForB {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user