feat(workspace): synchronize folders with server and add folder deletion
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 58s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 58s
This commit is contained in:
1 parent
1be0909344
commit
c4b2a00348
5 files changed
+382
-49
No files matched your search
@@ -101,15 +101,13 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
const [showIconPicker, setShowIconPicker] = useState(false);
|
||||
const [showWikiLinkMenu, setShowWikiLinkMenu] = useState(false);
|
||||
|
||||
// Folder state
|
||||
const [customFolders, setCustomFolders] = useState<string[]>(() => {
|
||||
// Folder state (synchronized with server)
|
||||
const [customFolders, setCustomFolders] = useState<string[]>([]);
|
||||
useEffect(() => {
|
||||
try {
|
||||
const stored = localStorage.getItem('mindsift_workspace_folders');
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
});
|
||||
localStorage.removeItem('mindsift_workspace_folders');
|
||||
} catch {}
|
||||
}, []);
|
||||
const [expandedFolders, setExpandedFolders] = useState<Set<string>>(new Set());
|
||||
const [isCreatingFolder, setIsCreatingFolder] = useState(false);
|
||||
const [newFolderParent, setNewFolderParent] = useState<string | null>(null);
|
||||
@@ -344,20 +342,28 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
};
|
||||
|
||||
const loadDocs = async (selectId?: string | null) => {
|
||||
const list = await api.getDocuments();
|
||||
setDocs(list);
|
||||
if (selectId) {
|
||||
const found = list.find((d) => d.id === selectId);
|
||||
if (found) {
|
||||
setSelectedDoc(found);
|
||||
return;
|
||||
try {
|
||||
const [list, serverFolders] = await Promise.all([
|
||||
api.getDocuments(),
|
||||
api.getFolders(),
|
||||
]);
|
||||
setDocs(list);
|
||||
setCustomFolders(serverFolders);
|
||||
if (selectId) {
|
||||
const found = list.find((d) => d.id === selectId);
|
||||
if (found) {
|
||||
setSelectedDoc(found);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!selectedDoc && list.length > 0) {
|
||||
setSelectedDoc(list[0]);
|
||||
} else if (selectedDoc) {
|
||||
const updated = list.find((d) => d.id === selectedDoc.id);
|
||||
if (updated) setSelectedDoc(updated);
|
||||
if (!selectedDoc && list.length > 0) {
|
||||
setSelectedDoc(list[0]);
|
||||
} else if (selectedDoc) {
|
||||
const updated = list.find((d) => d.id === selectedDoc.id);
|
||||
if (updated) setSelectedDoc(updated);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading documents or folders:', err);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -504,6 +510,8 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
}
|
||||
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));
|
||||
}
|
||||
|
||||
// 2. Update customFolders in state & localStorage
|
||||
const updatedCustomFolders = customFolders.map((p) => {
|
||||
if (p === cleanSource) return newFolderPath;
|
||||
if (p.startsWith(cleanSource + '/')) {
|
||||
return `${newFolderPath}${p.slice(cleanSource.length)}`;
|
||||
}
|
||||
return p;
|
||||
});
|
||||
|
||||
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 {}
|
||||
// 2. Sync folder structure with server
|
||||
api
|
||||
.createFolder(newFolderPath)
|
||||
.then(() => api.deleteFolder(cleanSource))
|
||||
.then(() => api.getFolders())
|
||||
.then(setCustomFolders)
|
||||
.catch((err) => console.error('Error syncing moved folders with server', err));
|
||||
|
||||
// Expand the new folder path and its parent
|
||||
setExpandedFolders((prev) => {
|
||||
@@ -881,20 +877,21 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmCreateFolder = () => {
|
||||
const handleConfirmCreateFolder = async () => {
|
||||
const trimmed = newFolderName.trim();
|
||||
if (!trimmed) {
|
||||
setIsCreatingFolder(false);
|
||||
return;
|
||||
}
|
||||
const fullPath = newFolderParent ? `${newFolderParent}/${trimmed}` : trimmed;
|
||||
if (!customFolders.includes(fullPath)) {
|
||||
const next = [...customFolders, fullPath].sort((a, b) => a.localeCompare(b));
|
||||
setCustomFolders(next);
|
||||
try {
|
||||
localStorage.setItem('mindsift_workspace_folders', JSON.stringify(next));
|
||||
} catch {
|
||||
// ignore
|
||||
try {
|
||||
await api.createFolder(fullPath);
|
||||
const serverFolders = await api.getFolders();
|
||||
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]));
|
||||
@@ -903,6 +900,40 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
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) => {
|
||||
setExpandedFolders((prev) => {
|
||||
const next = new Set(prev);
|
||||
@@ -1688,6 +1719,17 @@ export const WorkspaceView: React.FC<WorkspaceViewProps> = ({
|
||||
>
|
||||
<FolderPlus className="w-3 h-3" />
|
||||
</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>
|
||||
|
||||
|
||||
@@ -368,6 +368,26 @@ export const api = {
|
||||
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 ---
|
||||
async getDocuments(): Promise<Document[]> {
|
||||
const res = await request<Document[]>('/api/v1/documents');
|
||||
|
||||
Reference in new issue
Block a user