Dev #2
No files matched your search
@@ -101,15 +101,13 @@ 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);
|
||||||
|
|
||||||
// Folder state
|
// Folder state (synchronized with server)
|
||||||
const [customFolders, setCustomFolders] = useState<string[]>(() => {
|
const [customFolders, setCustomFolders] = useState<string[]>([]);
|
||||||
|
useEffect(() => {
|
||||||
try {
|
try {
|
||||||
const stored = localStorage.getItem('mindsift_workspace_folders');
|
localStorage.removeItem('mindsift_workspace_folders');
|
||||||
return stored ? JSON.parse(stored) : [];
|
} catch {}
|
||||||
} catch {
|
}, []);
|
||||||
return [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
||||||
const [isCreatingFolder, setIsCreatingFolder] = useState(false);
|
const [isCreatingFolder, setIsCreatingFolder] = useState(false);
|
||||||
const [newFolderParent, setNewFolderParent] = useState<string | null>(null);
|
const [newFolderParent, setNewFolderParent] = useState<string | null>(null);
|
||||||
@@ -344,8 +342,13 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const loadDocs = async (selectId?: string | null) => {
|
const loadDocs = async (selectId?: string | null) => {
|
||||||
const list = await api.getDocuments();
|
try {
|
||||||
|
const [list, serverFolders] = await Promise.all([
|
||||||
|
api.getDocuments(),
|
||||||
|
api.getFolders(),
|
||||||
|
]);
|
||||||
setDocs(list);
|
setDocs(list);
|
||||||
|
setCustomFolders(serverFolders);
|
||||||
if (selectId) {
|
if (selectId) {
|
||||||
const found = list.find((d) => d.id === selectId);
|
const found = list.find((d) => d.id === selectId);
|
||||||
if (found) {
|
if (found) {
|
||||||
@@ -359,6 +362,9 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
const updated = list.find((d) => d.id === selectedDoc.id);
|
const updated = list.find((d) => d.id === selectedDoc.id);
|
||||||
if (updated) setSelectedDoc(updated);
|
if (updated) setSelectedDoc(updated);
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error loading documents or folders:', err);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -504,6 +510,8 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
}
|
}
|
||||||
onStatsChange();
|
onStatsChange();
|
||||||
}
|
}
|
||||||
|
} else if (msg.event === 'WORKSPACE_FOLDERS_UPDATED') {
|
||||||
|
api.getFolders().then(setCustomFolders).catch(() => {});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -817,25 +825,13 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
.catch((err) => console.error('Failed to update doc folder', err));
|
.catch((err) => console.error('Failed to update doc folder', err));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Update customFolders in state & localStorage
|
// 2. Sync folder structure with server
|
||||||
const updatedCustomFolders = customFolders.map((p) => {
|
api
|
||||||
if (p === cleanSource) return newFolderPath;
|
.createFolder(newFolderPath)
|
||||||
if (p.startsWith(cleanSource + '/')) {
|
.then(() => api.deleteFolder(cleanSource))
|
||||||
return `${newFolderPath}${p.slice(cleanSource.length)}`;
|
.then(() => api.getFolders())
|
||||||
}
|
.then(setCustomFolders)
|
||||||
return p;
|
.catch((err) => console.error('Error syncing moved folders with server', err));
|
||||||
});
|
|
||||||
|
|
||||||
if (!updatedCustomFolders.includes(newFolderPath)) {
|
|
||||||
updatedCustomFolders.push(newFolderPath);
|
|
||||||
}
|
|
||||||
const sortedCustom = Array.from(new Set(updatedCustomFolders)).sort((a, b) =>
|
|
||||||
a.localeCompare(b)
|
|
||||||
);
|
|
||||||
setCustomFolders(sortedCustom);
|
|
||||||
try {
|
|
||||||
localStorage.setItem('mindsift_workspace_folders', JSON.stringify(sortedCustom));
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
// Expand the new folder path and its parent
|
// Expand the new folder path and its parent
|
||||||
setExpandedFolders((prev) => {
|
setExpandedFolders((prev) => {
|
||||||
@@ -881,20 +877,21 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfirmCreateFolder = () => {
|
const handleConfirmCreateFolder = async () => {
|
||||||
const trimmed = newFolderName.trim();
|
const trimmed = newFolderName.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
setIsCreatingFolder(false);
|
setIsCreatingFolder(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const fullPath = newFolderParent ? `${newFolderParent}/${trimmed}` : trimmed;
|
const fullPath = newFolderParent ? `${newFolderParent}/${trimmed}` : trimmed;
|
||||||
if (!customFolders.includes(fullPath)) {
|
|
||||||
const next = [...customFolders, fullPath].sort((a, b) => a.localeCompare(b));
|
|
||||||
setCustomFolders(next);
|
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('mindsift_workspace_folders', JSON.stringify(next));
|
await api.createFolder(fullPath);
|
||||||
} catch {
|
const serverFolders = await api.getFolders();
|
||||||
// ignore
|
setCustomFolders(serverFolders);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to create folder on server', err);
|
||||||
|
if (!customFolders.includes(fullPath)) {
|
||||||
|
setCustomFolders((prev) => [...prev, fullPath].sort((a, b) => a.localeCompare(b)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setExpandedFolders((prev) => new Set([...prev, fullPath]));
|
setExpandedFolders((prev) => new Set([...prev, fullPath]));
|
||||||
@@ -903,6 +900,40 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
setNewFolderParent(null);
|
setNewFolderParent(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleDeleteFolder = async (folderPath: string, folderName: string) => {
|
||||||
|
// Find all documents in this folder and subfolders
|
||||||
|
const docsInFolder = docs.filter((d) => {
|
||||||
|
const f = getDocFolder(d);
|
||||||
|
return f === folderPath || f.startsWith(`${folderPath}/`);
|
||||||
|
});
|
||||||
|
|
||||||
|
const confirmMsg =
|
||||||
|
docsInFolder.length > 0
|
||||||
|
? `Möchtest du den Ordner "${folderName}" wirklich löschen? Alle ${docsInFolder.length} darin enthaltenen Notizen werden unwiderruflich gelöscht!`
|
||||||
|
: `Möchtest du den leeren Ordner "${folderName}" wirklich löschen?`;
|
||||||
|
|
||||||
|
if (!window.confirm(confirmMsg)) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await api.deleteFolder(folderPath);
|
||||||
|
const deletedIds = new Set(res.deletedDocIds || docsInFolder.map((d) => d.id));
|
||||||
|
|
||||||
|
const nextDocs = docs.filter((d) => !deletedIds.has(d.id));
|
||||||
|
setDocs(nextDocs);
|
||||||
|
|
||||||
|
if (selectedDoc && deletedIds.has(selectedDoc.id)) {
|
||||||
|
setSelectedDoc(nextDocs.length > 0 ? nextDocs[0] : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const serverFolders = await api.getFolders();
|
||||||
|
setCustomFolders(serverFolders);
|
||||||
|
onStatsChange();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to delete folder', err);
|
||||||
|
alert('Fehler beim Löschen des Ordners');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const toggleFolder = (folderPath: string) => {
|
const toggleFolder = (folderPath: string) => {
|
||||||
setExpandedFolders((prev) => {
|
setExpandedFolders((prev) => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
@@ -1688,6 +1719,17 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
|||||||
>
|
>
|
||||||
<FolderPlus className="w-3 h-3" />
|
<FolderPlus className="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleDeleteFolder(node.path, node.name);
|
||||||
|
}}
|
||||||
|
className="p-1 hover:bg-red-500/10 text-muted hover:text-red-400 rounded-btn transition-colors"
|
||||||
|
title="Ordner löschen"
|
||||||
|
>
|
||||||
|
<Trash2 className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -368,6 +368,26 @@ export const api = {
|
|||||||
await request(`/api/v1/task-lists/${id}`, { method: 'DELETE' });
|
await request(`/api/v1/task-lists/${id}`, { method: 'DELETE' });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// --- WORKSPACE FOLDERS ---
|
||||||
|
async getFolders(): Promise<string[]> {
|
||||||
|
const res = await request<string[]>('/api/v1/folders');
|
||||||
|
return ensureArray(res);
|
||||||
|
},
|
||||||
|
|
||||||
|
async createFolder(path: string): Promise<{ path: string }> {
|
||||||
|
return request<{ path: string }>('/api/v1/folders', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ path }),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
async deleteFolder(path: string): Promise<{ deleted: boolean; deletedDocIds: string[] }> {
|
||||||
|
return request<{ deleted: boolean; deletedDocIds: string[] }>(
|
||||||
|
`/api/v1/folders?path=${encodeURIComponent(path)}`,
|
||||||
|
{ method: 'DELETE' }
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
// --- WORKSPACE DOCUMENTS ---
|
// --- WORKSPACE DOCUMENTS ---
|
||||||
async getDocuments(): Promise<Document[]> {
|
async getDocuments(): Promise<Document[]> {
|
||||||
const res = await request<Document[]>('/api/v1/documents');
|
const res = await request<Document[]>('/api/v1/documents');
|
||||||
|
|||||||
+61
-1
@@ -95,7 +95,11 @@ func (s *Server) Router() http.Handler {
|
|||||||
r.Put("/api/v1/task-lists/{id}", s.handleUpdateTaskList)
|
r.Put("/api/v1/task-lists/{id}", s.handleUpdateTaskList)
|
||||||
r.Delete("/api/v1/task-lists/{id}", s.handleDeleteTaskList)
|
r.Delete("/api/v1/task-lists/{id}", s.handleDeleteTaskList)
|
||||||
|
|
||||||
// Workspace Documents
|
// Workspace Documents & Folders
|
||||||
|
r.Get("/api/v1/folders", s.handleGetFolders)
|
||||||
|
r.Post("/api/v1/folders", s.handleCreateFolder)
|
||||||
|
r.Delete("/api/v1/folders", s.handleDeleteFolder)
|
||||||
|
|
||||||
r.Get("/api/v1/documents", s.handleGetDocuments)
|
r.Get("/api/v1/documents", s.handleGetDocuments)
|
||||||
r.Post("/api/v1/documents", s.handleCreateDocument)
|
r.Post("/api/v1/documents", s.handleCreateDocument)
|
||||||
r.Get("/api/v1/documents/{id}", s.handleGetDocument)
|
r.Get("/api/v1/documents/{id}", s.handleGetDocument)
|
||||||
@@ -811,6 +815,62 @@ func (s *Server) handleDeleteDocument(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- WORKSPACE FOLDERS ---
|
||||||
|
|
||||||
|
func (s *Server) handleGetFolders(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID := auth.GetUserID(r.Context())
|
||||||
|
folders, err := s.store.GetFolders(userID)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeJSON(w, http.StatusOK, folders)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleCreateFolder(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID := auth.GetUserID(r.Context())
|
||||||
|
var req struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cleanPath, err := s.store.CreateFolder(userID, req.Path)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
s.wsHub.BroadcastDocEvent("", "WORKSPACE_FOLDERS_UPDATED", map[string]string{"path": cleanPath})
|
||||||
|
writeJSON(w, http.StatusCreated, map[string]string{"path": cleanPath})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) handleDeleteFolder(w http.ResponseWriter, r *http.Request) {
|
||||||
|
userID := auth.GetUserID(r.Context())
|
||||||
|
folderPath := r.URL.Query().Get("path")
|
||||||
|
if folderPath == "" {
|
||||||
|
writeError(w, http.StatusBadRequest, "Pfad erforderlich")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deletedDocIDs, err := s.store.DeleteFolder(userID, folderPath)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, docID := range deletedDocIDs {
|
||||||
|
s.wsHub.ReleaseDocLock(docID, userID, true)
|
||||||
|
s.wsHub.BroadcastDocEvent(docID, "DOC_DELETED", map[string]string{"id": docID})
|
||||||
|
}
|
||||||
|
s.wsHub.BroadcastDocEvent("", "WORKSPACE_FOLDERS_UPDATED", map[string]string{"path": folderPath})
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||||
|
"deleted": true,
|
||||||
|
"deletedDocIds": deletedDocIDs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// --- DOCUMENT SHARING & COLLABORATION ---
|
// --- DOCUMENT SHARING & COLLABORATION ---
|
||||||
|
|
||||||
func (s *Server) handleGetDocShares(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleGetDocShares(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -1435,6 +1436,135 @@ func (s *Store) DeleteDocument(userID, docID string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Store) GetFolders(userID string) ([]string, error) {
|
||||||
|
folderSet := make(map[string]bool)
|
||||||
|
|
||||||
|
// 1. Collect from SQLite documents (owned by user or shared with user)
|
||||||
|
rows, err := s.db.Query(
|
||||||
|
`SELECT DISTINCT folder_path FROM documents
|
||||||
|
WHERE (user_id = ? OR id IN (SELECT doc_id FROM doc_shares WHERE user_id = ?))
|
||||||
|
AND is_archived = 0 AND folder_path != ''`,
|
||||||
|
userID, userID,
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
for rows.Next() {
|
||||||
|
var fp string
|
||||||
|
if err := rows.Scan(&fp); err == nil && fp != "" {
|
||||||
|
clean := filepath.ToSlash(filepath.Clean(fp))
|
||||||
|
parts := strings.Split(clean, "/")
|
||||||
|
var cur string
|
||||||
|
for _, p := range parts {
|
||||||
|
if p != "" && p != "." {
|
||||||
|
if cur == "" {
|
||||||
|
cur = p
|
||||||
|
} else {
|
||||||
|
cur = cur + "/" + p
|
||||||
|
}
|
||||||
|
folderSet[cur] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = rows.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Scan physical directories on disk in docDir/userID
|
||||||
|
if s.docDir != "" {
|
||||||
|
userDir := filepath.Join(s.docDir, userID)
|
||||||
|
if fi, err := os.Stat(userDir); err == nil && fi.IsDir() {
|
||||||
|
_ = filepath.Walk(userDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil || !info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if path == userDir {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
rel, err := filepath.Rel(userDir, path)
|
||||||
|
if err == nil && rel != "" && rel != "." {
|
||||||
|
clean := filepath.ToSlash(filepath.Clean(rel))
|
||||||
|
parts := strings.Split(clean, "/")
|
||||||
|
var cur string
|
||||||
|
for _, p := range parts {
|
||||||
|
if p != "" && p != "." {
|
||||||
|
if cur == "" {
|
||||||
|
cur = p
|
||||||
|
} else {
|
||||||
|
cur = cur + "/" + p
|
||||||
|
}
|
||||||
|
folderSet[cur] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]string, 0, len(folderSet))
|
||||||
|
for f := range folderSet {
|
||||||
|
result = append(result, f)
|
||||||
|
}
|
||||||
|
sort.Strings(result)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) CreateFolder(userID, folderPath string) (string, error) {
|
||||||
|
clean := filepath.ToSlash(filepath.Clean(strings.TrimSpace(folderPath)))
|
||||||
|
if clean == "" || clean == "." || strings.HasPrefix(clean, "/") || strings.Contains(clean, "..") {
|
||||||
|
return "", fmt.Errorf("ungültiger Ordnerpfad")
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.docDir != "" {
|
||||||
|
targetDir := filepath.Join(s.docDir, userID, clean)
|
||||||
|
if err := os.MkdirAll(targetDir, 0755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return clean, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) DeleteFolder(userID, folderPath string) ([]string, error) {
|
||||||
|
clean := filepath.ToSlash(filepath.Clean(strings.TrimSpace(folderPath)))
|
||||||
|
if clean == "" || clean == "." || strings.HasPrefix(clean, "/") || strings.Contains(clean, "..") {
|
||||||
|
return nil, fmt.Errorf("ungültiger Ordnerpfad")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all documents in this folder and subfolders
|
||||||
|
rows, err := s.db.Query(
|
||||||
|
`SELECT id FROM documents
|
||||||
|
WHERE user_id = ? AND (folder_path = ? OR folder_path LIKE ?)`,
|
||||||
|
userID, clean, clean+"/%",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var deletedDocIDs []string
|
||||||
|
for rows.Next() {
|
||||||
|
var id string
|
||||||
|
if err := rows.Scan(&id); err == nil {
|
||||||
|
deletedDocIDs = append(deletedDocIDs, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = rows.Close()
|
||||||
|
|
||||||
|
// Delete from SQLite, pins, shares
|
||||||
|
for _, id := range deletedDocIDs {
|
||||||
|
_, _ = s.db.Exec(`DELETE FROM doc_pins WHERE doc_id = ?`, id)
|
||||||
|
_, _ = s.db.Exec(`DELETE FROM doc_shares WHERE doc_id = ?`, id)
|
||||||
|
_, _ = s.db.Exec(`DELETE FROM documents WHERE id = ? AND user_id = ?`, id, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove physical folder and subdirectories from disk
|
||||||
|
if s.docDir != "" {
|
||||||
|
targetDir := filepath.Join(s.docDir, userID, clean)
|
||||||
|
_ = os.RemoveAll(targetDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
return deletedDocIDs, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Store) GetDocShares(docID string) ([]domain.DocShare, error) {
|
func (s *Store) GetDocShares(docID string) ([]domain.DocShare, error) {
|
||||||
rows, err := s.db.Query(
|
rows, err := s.db.Query(
|
||||||
`SELECT s.id, s.doc_id, s.user_id, u.email, u.display_name, s.permission, s.created_by, s.created_at
|
`SELECT s.id, s.doc_id, s.user_id, u.email, u.display_name, s.permission, s.created_by, s.created_at
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -317,3 +318,83 @@ func TestEnsureDemoAccountsAndLoginLookup(t *testing.T) {
|
|||||||
t.Fatalf("second EnsureDemoAccounts call failed: %v", err)
|
t.Fatalf("second EnsureDemoAccounts call failed: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFolderManagementAndDeletion(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dbPath := filepath.Join(tmpDir, "folder_test.db")
|
||||||
|
docDir := filepath.Join(tmpDir, "workspace")
|
||||||
|
|
||||||
|
store, err := NewStore(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewStore failed: %v", err)
|
||||||
|
}
|
||||||
|
defer store.Close()
|
||||||
|
|
||||||
|
user, err := store.CreateUser("folderuser@example.com", "hash", "Folder User", "member")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateUser failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Create folders on disk
|
||||||
|
f1, err := store.CreateFolder(user.ID, "Projekte/MindSift")
|
||||||
|
if err != nil || f1 != "Projekte/MindSift" {
|
||||||
|
t.Fatalf("CreateFolder failed: %v, f1=%s", err, f1)
|
||||||
|
}
|
||||||
|
f2, err := store.CreateFolder(user.ID, "Archiv")
|
||||||
|
if err != nil || f2 != "Archiv" {
|
||||||
|
t.Fatalf("CreateFolder Archiv failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Create document in subfolder
|
||||||
|
doc1, err := store.CreateDocument(user.ID, "", "Doc in MindSift", "📄", "Projekte/MindSift", nil, nil, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateDocument failed: %v", err)
|
||||||
|
}
|
||||||
|
doc2, err := store.CreateDocument(user.ID, "", "SubDoc", "📝", "Projekte/MindSift/Sub", nil, nil, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CreateDocument 2 failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. GetFolders
|
||||||
|
folders, err := store.GetFolders(user.ID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetFolders failed: %v", err)
|
||||||
|
}
|
||||||
|
expected := map[string]bool{
|
||||||
|
"Archiv": true,
|
||||||
|
"Projekte": true,
|
||||||
|
"Projekte/MindSift": true,
|
||||||
|
"Projekte/MindSift/Sub": true,
|
||||||
|
}
|
||||||
|
for _, f := range folders {
|
||||||
|
delete(expected, f)
|
||||||
|
}
|
||||||
|
if len(expected) > 0 {
|
||||||
|
t.Fatalf("GetFolders missing expected folders: %v (got %v)", expected, folders)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. DeleteFolder "Projekte/MindSift" -> should remove doc1 & doc2
|
||||||
|
deletedDocs, err := store.DeleteFolder(user.ID, "Projekte/MindSift")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("DeleteFolder failed: %v", err)
|
||||||
|
}
|
||||||
|
if len(deletedDocs) != 2 {
|
||||||
|
t.Fatalf("expected 2 deleted docs, got %d (%v)", len(deletedDocs), deletedDocs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify docs are gone from DB
|
||||||
|
_, err = store.GetDocumentByID(user.ID, doc1.ID)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected doc1 to be deleted")
|
||||||
|
}
|
||||||
|
_, err = store.GetDocumentByID(user.ID, doc2.ID)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected doc2 to be deleted")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify disk directory was deleted
|
||||||
|
diskFolder := filepath.Join(docDir, user.ID, "Projekte/MindSift")
|
||||||
|
if _, err := os.Stat(diskFolder); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("expected directory %s to be deleted on disk", diskFolder)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user