diff --git a/frontend/src/pages/EndpointEditor.tsx b/frontend/src/pages/EndpointEditor.tsx index 1ffc3d8..a6faedc 100644 --- a/frontend/src/pages/EndpointEditor.tsx +++ b/frontend/src/pages/EndpointEditor.tsx @@ -146,14 +146,23 @@ export default function EndpointEditor() { } }, [storageKey, testParams, testResult]); + const [saveAndStay, setSaveAndStay] = useState(false); + const saveMutation = useMutation({ mutationFn: (data: any) => isEditing ? endpointsApi.update(id!, data) : endpointsApi.create(data), - onSuccess: () => { + onSuccess: (response) => { queryClient.invalidateQueries({ queryKey: ['endpoints'] }); queryClient.invalidateQueries({ queryKey: ['endpoint', id] }); toast.success(isEditing ? 'Эндпоинт обновлен' : 'Эндпоинт создан'); - navigate(-1); + if (saveAndStay) { + // При создании нового — переходим на страницу редактирования созданного + if (!isEditing && response.data?.id) { + navigate(`/endpoints/${response.data.id}`, { replace: true }); + } + } else { + navigate(-1); + } }, onError: () => toast.error('Не удалось сохранить эндпоинт'), }); @@ -832,8 +841,21 @@ export default function EndpointEditor() { - + diff --git a/frontend/src/pages/Folders.tsx b/frontend/src/pages/Folders.tsx index 823f674..b714151 100644 --- a/frontend/src/pages/Folders.tsx +++ b/frontend/src/pages/Folders.tsx @@ -1,9 +1,9 @@ -import { useState } from 'react'; +import { useState, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { foldersApi, endpointsApi } from '@/services/api'; import { Folder, Endpoint } from '@/types'; -import { Plus, Edit2, Trash2, Folder as FolderIcon, FolderOpen, FileCode, ChevronRight, ChevronDown } from 'lucide-react'; +import { Plus, Edit2, Trash2, Folder as FolderIcon, FolderOpen, FileCode, ChevronRight, ChevronDown, Search, ChevronsDownUp, ChevronsUpDown } from 'lucide-react'; import toast from 'react-hot-toast'; import Dialog from '@/components/Dialog'; @@ -14,6 +14,7 @@ export default function Folders() { const [editingFolder, setEditingFolder] = useState(null); const [selectedFolderId, setSelectedFolderId] = useState(null); const [expandedFolders, setExpandedFolders] = useState>(new Set()); + const [searchQuery, setSearchQuery] = useState(''); const [dialog, setDialog] = useState<{ isOpen: boolean; title: string; @@ -111,6 +112,16 @@ export default function Folders() { }); }; + const getAllFolderIds = (nodes: any[]): string[] => { + const ids: string[] = []; + const collect = (ns: any[]) => ns.forEach(n => { ids.push(n.id); if (n.children?.length) collect(n.children); }); + collect(nodes); + return ids; + }; + + const expandAll = () => setExpandedFolders(new Set(getAllFolderIds(tree))); + const collapseAll = () => setExpandedFolders(new Set()); + // Построение дерева папок const buildTree = () => { if (!folders || !endpoints) return []; @@ -143,6 +154,33 @@ export default function Folders() { const tree = buildTree(); + // Поиск + const searchResults = useMemo(() => { + if (!searchQuery.trim()) return null; + const q = searchQuery.toLowerCase(); + + // Строим карту путей папок + const folderPathMap = new Map(); + const buildPaths = (nodes: any[], prefix = '') => { + nodes.forEach(n => { + const p = prefix ? `${prefix} / ${n.name}` : n.name; + folderPathMap.set(n.id, p); + if (n.children?.length) buildPaths(n.children, p); + }); + }; + buildPaths(tree); + + const matchedEndpoints = (endpoints || []).filter(e => + e.name.toLowerCase().includes(q) || + e.path.toLowerCase().includes(q) || + e.method.toLowerCase().includes(q) + ).map(e => ({ ...e, folderPath: e.folder_id ? (folderPathMap.get(e.folder_id) || '') : '' })); + + const matchedFolders = (folders || []).filter(f => f.name.toLowerCase().includes(q)); + + return { endpoints: matchedEndpoints, folders: matchedFolders, folderPathMap }; + }, [searchQuery, endpoints, folders, tree]); + return (
@@ -169,40 +207,126 @@ export default function Folders() {
) : (
+ {/* Toolbar: search + expand/collapse */} +
+
+ + setSearchQuery(e.target.value)} + className="input w-full pl-9 text-sm" + /> +
+ + +
+
- {/* Корневые папки */} - {tree.map(folder => ( - - ))} + {searchResults ? ( + /* Результаты поиска */ + <> + {searchResults.endpoints.length === 0 && searchResults.folders.length === 0 ? ( +
+

Ничего не найдено по запросу «{searchQuery}»

+
+ ) : ( + <> + {searchResults.folders.map((folder: any) => ( +
+ + {folder.name} + {searchResults.folderPathMap.get(folder.id)} +
+ + +
+
+ ))} + {searchResults.endpoints.map((endpoint: any) => ( +
+ +
+ {endpoint.name} + {endpoint.folderPath && ( + {endpoint.folderPath} + )} +
+ {endpoint.method} + {endpoint.path} +
+ + +
+
+ ))} + + )} + + ) : ( + /* Обычное дерево */ + <> + {tree.map(folder => ( + + ))} - {/* Корневые эндпоинты (без папки) */} - {rootEndpoints.map(endpoint => ( - - ))} + {rootEndpoints.map(endpoint => ( + + ))} - {tree.length === 0 && rootEndpoints.length === 0 && ( -
-

Нет папок и эндпоинтов.

-

Создайте первую папку или эндпоинт!

-
+ {tree.length === 0 && rootEndpoints.length === 0 && ( +
+

Нет папок и эндпоинтов.

+

Создайте первую папку или эндпоинт!

+
+ )} + )}