feat: request for tree

This commit is contained in:
nikita
2026-04-05 00:56:48 +03:00
parent 6b82c99d50
commit 07066ec8c0
5 changed files with 359 additions and 162 deletions
+166 -50
View File
@@ -8,6 +8,7 @@ import {
addNode,
renameNode,
} from "../helpers/fileTree";
import { scriptsApi } from "../api/scripts.api";
export const initialFiles: FileNode = {
name: "my-project",
@@ -38,11 +39,15 @@ export const initialFiles: FileNode = {
],
};
interface IDEFileNode extends FileNode {
dirty?: boolean;
}
interface IDEState {
// Файловая система
files: FileNode | null;
openFiles: FileNode[];
activeFile: FileNode | null;
openFiles: IDEFileNode[];
activeFile: IDEFileNode | null;
expandedFolders: Set<string>;
searchQuery: string;
showSearch: boolean;
@@ -59,6 +64,7 @@ interface IDEState {
// Действия с файлами
selectFile: (node: FileNode) => void;
updateFileContent: (content: string) => void;
saveActiveFile: () => Promise<void>;
closeFile: (file: FileNode) => void;
closeAllFiles: () => void;
closeOtherFiles: (file: FileNode) => void;
@@ -72,6 +78,20 @@ interface IDEState {
deleteRoot: () => void;
createNewProject: () => void;
// API методы
fetchTree: () => Promise<void>;
createScript: (payload: {
content: string;
interpreter_id: number;
path: string;
}) => Promise<void>;
updateScript: (
id: number,
payload: { content: string; interpreter_id: number; path: string },
) => Promise<void>;
deleteScript: (id: number) => Promise<void>;
saveActiveFile: () => Promise<void>;
// Поиск
setSearchQuery: (query: string) => void;
toggleSearch: () => void;
@@ -94,8 +114,8 @@ interface IDEState {
initialize: (initialFiles: FileNode) => void;
// Диалог подтверждения
handleDialogConfirm: (value: string) => void;
handleDeleteNode: (node: FileNode) => void;
handleDialogConfirm: (value: string) => Promise<void>;
handleDeleteNode: (node: FileNode) => Promise<void>;
}
export const useIDEStore = create<IDEState>((set, get) => ({
@@ -142,7 +162,7 @@ export const useIDEStore = create<IDEState>((set, get) => ({
updateFileContent: (content: string) => {
const { activeFile, files } = get();
if (activeFile && files) {
const updatedFile = { ...activeFile, content };
const updatedFile = { ...activeFile, content, dirty: true };
set({ activeFile: updatedFile });
set((state) => ({
openFiles: state.openFiles.map((f) =>
@@ -275,6 +295,113 @@ export const useIDEStore = create<IDEState>((set, get) => ({
});
},
// API: загрузка дерева с сервера
fetchTree: async () => {
try {
const data = await scriptsApi.getTree();
const nodeMap = new Map<string, FileNode>();
data.forEach((item) => {
nodeMap.set(item.name, {
id: item.id,
name: item.name,
type: item.type === "folder" ? "folder" : "file",
content: item.content || "",
path: item.name,
interpreter_id: item.interpreter_id,
children: item.type === "folder" ? [] : undefined,
});
});
const hasParent = new Set<string>();
data.forEach((item) => {
if (item.children) {
item.children.forEach((childName: string) => {
hasParent.add(childName);
const parent = nodeMap.get(item.name);
const child = nodeMap.get(childName);
if (parent?.children && child) {
parent.children.push(child);
}
});
}
});
const roots = [...nodeMap.entries()]
.filter(([name]) => !hasParent.has(name))
.map(([, node]) => node);
set({
files: {
name: "scripts",
type: "folder",
children: roots,
},
expandedFolders: new Set(),
isInitialized: true,
});
} catch (e) {
console.error("Failed to fetch tree:", e);
throw e;
}
},
// API: создание скрипта
createScript: async (payload) => {
try {
await scriptsApi.createScript(payload);
await get().fetchTree();
} catch (e) {
console.error("Failed to create script:", e);
throw e;
}
},
// API: обновление скрипта
updateScript: async (id, payload) => {
try {
await scriptsApi.updateScript(id, payload);
} catch (e) {
console.error("Failed to update script:", e);
throw e;
}
},
// API: удаление скрипта
deleteScript: async (id) => {
try {
await scriptsApi.deleteScript(id);
await get().fetchTree();
} catch (e) {
console.error("Failed to delete script:", e);
throw e;
}
},
// API: сохранение активного файла
saveActiveFile: async () => {
const { activeFile } = get();
if (!activeFile || !activeFile.id) return;
try {
await scriptsApi.updateScript(activeFile.id, {
content: activeFile.content || "",
interpreter_id: activeFile.interpreter_id || 0,
path: activeFile.path || "",
});
set((state) => ({
activeFile: state.activeFile
? { ...state.activeFile, dirty: false }
: null,
openFiles: state.openFiles.map((f) =>
f.path === state.activeFile?.path ? { ...f, dirty: false } : f,
),
}));
} catch (e) {
console.error("Failed to save file:", e);
}
},
// Поиск
setSearchQuery: (query: string) => {
set({ searchQuery: query });
@@ -290,9 +417,8 @@ export const useIDEStore = create<IDEState>((set, get) => ({
setTabContextMenu: (menu) => set({ tabContextMenu: menu }),
// Подтверждение диалога
handleDialogConfirm: (value: string) => {
const { dialog, files, refreshFiles, toggleFolder, autoExpandPaths } =
get();
handleDialogConfirm: async (value: string) => {
const { dialog, files, toggleFolder, autoExpandPaths } = get();
if (!dialog) return;
if (dialog.type === "rename" && dialog.node) {
@@ -315,47 +441,34 @@ export const useIDEStore = create<IDEState>((set, get) => ({
value,
);
if (newFiles) {
refreshFiles(newFiles);
set({ files: newFiles });
}
set({ dialog: null });
return;
}
let parentPath: string;
if (!dialog.node) {
parentPath = files!.path || files!.name;
parentPath = "";
} else if (dialog.node.type === "folder") {
parentPath = dialog.node.path || dialog.node.name;
} else {
const pathParts = (dialog.node.path || dialog.node.name).split("/");
pathParts.pop();
parentPath = pathParts.join("/") || files!.path || files!.name;
parentPath = pathParts.join("/");
}
const parentNode = findNode(files!, parentPath);
if (
parentNode?.children?.some(
(c) => c.name.toLowerCase() === value.toLowerCase(),
)
) {
alert(`"${value}" already exists in this folder.`);
set({ dialog: null });
return;
}
const fullPath = parentPath ? `${parentPath}/${value}` : value;
let newFiles: FileNode | null = null;
let createdNode: FileNode | null = null;
try {
const result = await scriptsApi.createScript({
content: "",
interpreter_id: 0,
path: fullPath,
});
if (dialog.type === "newFile") {
createdNode = { name: value, type: "file", content: "" };
newFiles = addNode(files!, parentPath, createdNode);
} else if (dialog.type === "newFolder") {
createdNode = { name: value, type: "folder", children: [] };
newFiles = addNode(files!, parentPath, createdNode);
}
await get().fetchTree();
if (newFiles) {
const allParentPaths: string[] = [];
let current = parentPath;
while (current) {
@@ -371,35 +484,38 @@ export const useIDEStore = create<IDEState>((set, get) => ({
});
autoExpandPaths(new Set(allParentPaths));
if (createdNode && createdNode.type === "file") {
const findAndOpen = (node: FileNode, name: string): FileNode | null => {
if (node.name === name && node.type === "file") return node;
if (node.children) {
for (const child of node.children) {
const found = findAndOpen(child, name);
if (found) return found;
}
}
return null;
if (dialog.type === "newFile") {
const createdNode: FileNode = {
id: result.id,
name: value,
type: "file",
content: result.content,
path: result.path,
interpreter_id: result.interpreter_id,
};
const openedFile = findAndOpen(newFiles, value);
refreshFiles(newFiles, openedFile || undefined);
} else {
refreshFiles(newFiles);
get().selectFile(createdNode);
}
} catch (e) {
console.error("Failed to create:", e);
}
set({ dialog: null });
},
// Удаление узла
handleDeleteNode: (node: FileNode) => {
const { files, refreshFiles } = get();
handleDeleteNode: async (node: FileNode) => {
const { files } = get();
const isRootNode = node.path === files?.path;
if (isRootNode) {
get().deleteRoot();
} else if (window.confirm(`Delete "${node.name}"?`)) {
const newFiles = deleteNode(files!, node.path || node.name);
if (newFiles) refreshFiles(newFiles);
if (node.id) {
try {
await get().deleteScript(node.id);
} catch (e) {
console.error("Failed to delete:", e);
}
}
}
},
}));