2 Commits

Author SHA1 Message Date
nikita f14490c076 feat: rename
ci-front / build (push) Successful in 2m24s
2026-04-05 03:59:08 +03:00
nikita 178c3b53f7 feat: remove folders & create folder 2026-04-05 03:28:31 +03:00
6 changed files with 255 additions and 45 deletions
+6
View File
@@ -65,6 +65,12 @@ export const IDE: React.FC<IDEProps> = ({
const initialize = useIDEStore((state) => state.initialize); const initialize = useIDEStore((state) => state.initialize);
const isInitialized = useIDEStore((state) => state.isInitialized); const isInitialized = useIDEStore((state) => state.isInitialized);
const fetchTree = useIDEStore((state) => state.fetchTree); const fetchTree = useIDEStore((state) => state.fetchTree);
const fetchInterpreters = useIDEStore((state) => state.fetchInterpreters);
// Загружаем интерпретаторы при инициализации
useEffect(() => {
fetchInterpreters();
}, []);
// Обработка Ctrl+S // Обработка Ctrl+S
useEffect(() => { useEffect(() => {
@@ -1,4 +1,5 @@
import { apiClient } from "@/shared/api/axios.instance"; import { apiClient } from "@/shared/api/axios.instance";
import type { Interpreter } from "../types";
export interface ScriptNodeDto { export interface ScriptNodeDto {
id: number; id: number;
@@ -32,6 +33,11 @@ export interface UpdateScriptPayload {
// apiClient уже имеет интерсептор для Authorization header // apiClient уже имеет интерсептор для Authorization header
export const scriptsApi = { export const scriptsApi = {
getInterpreters: async (): Promise<Interpreter[]> => {
const res = await apiClient.get<Interpreter[]>("/scripts/interpreters");
return res.data;
},
getTree: async (): Promise<ScriptNodeDto[]> => { getTree: async (): Promise<ScriptNodeDto[]> => {
const res = await apiClient.get<ScriptNodeDto[]>("/scripts/tree"); const res = await apiClient.get<ScriptNodeDto[]>("/scripts/tree");
return res.data; return res.data;
@@ -55,4 +61,26 @@ export const scriptsApi = {
deleteScript: async (id: number): Promise<void> => { deleteScript: async (id: number): Promise<void> => {
await apiClient.delete(`/scripts/${id}`); await apiClient.delete(`/scripts/${id}`);
}, },
createFolder: async (path: string): Promise<{ path: string }> => {
const res = await apiClient.post<{ path: string }>("/scripts/folder", {
path,
});
return res.data;
},
deleteFolder: async (path: string): Promise<void> => {
await apiClient.delete(`/scripts/folder`, { data: { path } });
},
rename: async (payload: {
old_path: string;
new_path: string;
}): Promise<{ path: string }> => {
const res = await apiClient.post<{ path: string }>(
"/scripts/rename",
payload,
);
return res.data;
},
}; };
@@ -46,6 +46,10 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({
const handleEmptyContextMenu = (e: React.MouseEvent) => { const handleEmptyContextMenu = (e: React.MouseEvent) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
// Загружаем интерпретаторы перед открытием меню
if (store.interpreters.length === 0) {
store.fetchInterpreters();
}
store.setContextMenu({ x: e.clientX, y: e.clientY, node: null }); store.setContextMenu({ x: e.clientX, y: e.clientY, node: null });
}; };
@@ -55,6 +59,13 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({
store.setContextMenu({ x: e.clientX, y: e.clientY, node }); store.setContextMenu({ x: e.clientX, y: e.clientY, node });
}; };
// Загружаем интерпретаторы при монтировании компонента
useEffect(() => {
if (store.interpreters.length === 0) {
store.fetchInterpreters();
}
}, []);
const filteredFiles = store.searchQuery const filteredFiles = store.searchQuery
? (files.children || []) ? (files.children || [])
.map((child) => filterTree(child, store.searchQuery)) .map((child) => filterTree(child, store.searchQuery))
@@ -320,8 +331,13 @@ export const FileExplorer: React.FC<FileExplorerProps> = ({
? store.dialog.node.name ? store.dialog.node.name
: "" : ""
} }
onConfirm={store.handleDialogConfirm} onConfirm={(value, interpreterId) => {
store.handleDialogConfirm(value, interpreterId);
}}
onCancel={() => store.setDialog(null)} onCancel={() => store.setDialog(null)}
interpreters={
store.dialog.type === "newFile" ? store.interpreters : undefined
}
/> />
)} )}
</div> </div>
@@ -1,10 +1,12 @@
import React, { useState, useRef, useEffect } from "react"; import React, { useState, useRef, useEffect } from "react";
import type { Interpreter } from "../types";
interface InputDialogProps { interface InputDialogProps {
title: string; title: string;
initialValue?: string; initialValue?: string;
onConfirm: (value: string) => void; onConfirm: (value: string, interpreterId?: number) => void;
onCancel: () => void; onCancel: () => void;
interpreters?: Interpreter[];
} }
export const InputDialog: React.FC<InputDialogProps> = ({ export const InputDialog: React.FC<InputDialogProps> = ({
@@ -12,8 +14,12 @@ export const InputDialog: React.FC<InputDialogProps> = ({
initialValue = "", initialValue = "",
onConfirm, onConfirm,
onCancel, onCancel,
interpreters,
}) => { }) => {
const [value, setValue] = useState(initialValue); const [value, setValue] = useState(initialValue);
const [interpreterId, setInterpreterId] = useState<number | undefined>(
interpreters?.[0]?.id,
);
const inputRef = useRef<HTMLInputElement>(null); const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => { useEffect(() => {
@@ -21,6 +27,8 @@ export const InputDialog: React.FC<InputDialogProps> = ({
inputRef.current?.select(); inputRef.current?.select();
}, []); }, []);
const showInterpreterDropdown = interpreters && interpreters.length > 0;
return ( return (
<div <div
style={{ style={{
@@ -59,7 +67,7 @@ export const InputDialog: React.FC<InputDialogProps> = ({
{title} {title}
</h3> </h3>
<p style={{ margin: "0 0 16px 0", color: "#858585", fontSize: "12px" }}> <p style={{ margin: "0 0 16px 0", color: "#858585", fontSize: "12px" }}>
Enter a new name Enter a name
</p> </p>
<input <input
ref={inputRef} ref={inputRef}
@@ -67,7 +75,9 @@ export const InputDialog: React.FC<InputDialogProps> = ({
value={value} value={value}
onChange={(e) => setValue(e.target.value)} onChange={(e) => setValue(e.target.value)}
onKeyDown={(e) => onKeyDown={(e) =>
e.key === "Enter" && value.trim() && onConfirm(value.trim()) e.key === "Enter" &&
value.trim() &&
onConfirm(value.trim(), interpreterId)
} }
style={{ style={{
width: "100%", width: "100%",
@@ -77,10 +87,48 @@ export const InputDialog: React.FC<InputDialogProps> = ({
borderRadius: "6px", borderRadius: "6px",
color: "#ccc", color: "#ccc",
fontSize: "14px", fontSize: "14px",
marginBottom: "20px", marginBottom: showInterpreterDropdown ? "12px" : "20px",
outline: "none", outline: "none",
}} }}
/> />
{/* Interpreter dropdown */}
{showInterpreterDropdown && (
<div style={{ marginBottom: "20px" }}>
<label
style={{
display: "block",
fontSize: "12px",
color: "#858585",
marginBottom: "6px",
}}
>
Interpreter
</label>
<select
value={interpreterId}
onChange={(e) => setInterpreterId(Number(e.target.value))}
style={{
width: "100%",
padding: "10px",
backgroundColor: "#3c3c3c",
border: "1px solid #3e3e42",
borderRadius: "6px",
color: "#ccc",
fontSize: "14px",
outline: "none",
cursor: "pointer",
}}
>
{interpreters.map((interp) => (
<option key={interp.id} value={interp.id}>
{interp.label}
</option>
))}
</select>
</div>
)}
<div <div
style={{ display: "flex", gap: "12px", justifyContent: "flex-end" }} style={{ display: "flex", gap: "12px", justifyContent: "flex-end" }}
> >
@@ -99,7 +147,9 @@ export const InputDialog: React.FC<InputDialogProps> = ({
Cancel Cancel
</button> </button>
<button <button
onClick={() => value.trim() && onConfirm(value.trim())} onClick={() =>
value.trim() && onConfirm(value.trim(), interpreterId)
}
style={{ style={{
padding: "6px 16px", padding: "6px 16px",
backgroundColor: "#0e639c", backgroundColor: "#0e639c",
+139 -39
View File
@@ -1,5 +1,5 @@
import { create } from "zustand"; import { create } from "zustand";
import type { FileNode } from "../types"; import type { FileNode, Interpreter, DialogState } from "../types";
import { import {
addPaths, addPaths,
getAllFolderPaths, getAllFolderPaths,
@@ -52,13 +52,11 @@ interface IDEState {
searchQuery: string; searchQuery: string;
showSearch: boolean; showSearch: boolean;
isInitialized: boolean; isInitialized: boolean;
interpreters: Interpreter[];
// Диалоги и контекстные меню // Диалоги и контекстные меню
contextMenu: { x: number; y: number; node: FileNode | null } | null; contextMenu: { x: number; y: number; node: FileNode | null } | null;
dialog: { dialog: DialogState | null;
type: "newFile" | "newFolder" | "rename";
node: FileNode | null;
} | null;
tabContextMenu: { x: number; y: number; file: FileNode } | null; tabContextMenu: { x: number; y: number; file: FileNode } | null;
// Действия с файлами // Действия с файлами
@@ -78,6 +76,9 @@ interface IDEState {
deleteRoot: () => void; deleteRoot: () => void;
createNewProject: () => void; createNewProject: () => void;
// Интерпретаторы
fetchInterpreters: () => Promise<void>;
// API методы // API методы
fetchTree: () => Promise<void>; fetchTree: () => Promise<void>;
createScript: (payload: { createScript: (payload: {
@@ -85,11 +86,13 @@ interface IDEState {
interpreter_id: number; interpreter_id: number;
path: string; path: string;
}) => Promise<void>; }) => Promise<void>;
createFolder: (path: string) => Promise<void>;
updateScript: ( updateScript: (
id: number, id: number,
payload: { content: string; interpreter_id: number; path: string }, payload: { content: string; interpreter_id: number; path: string },
) => Promise<void>; ) => Promise<void>;
deleteScript: (id: number) => Promise<void>; deleteScript: (id: number) => Promise<void>;
deleteFolder: (payload: { path: string }) => Promise<void>;
saveActiveFile: () => Promise<void>; saveActiveFile: () => Promise<void>;
// Поиск // Поиск
@@ -114,7 +117,7 @@ interface IDEState {
initialize: (initialFiles: FileNode) => void; initialize: (initialFiles: FileNode) => void;
// Диалог подтверждения // Диалог подтверждения
handleDialogConfirm: (value: string) => Promise<void>; handleDialogConfirm: (value: string, interpreterId?: number) => Promise<void>;
handleDeleteNode: (node: FileNode) => Promise<void>; handleDeleteNode: (node: FileNode) => Promise<void>;
} }
@@ -131,6 +134,7 @@ export const useIDEStore = create<IDEState>((set, get) => ({
contextMenu: null, contextMenu: null,
dialog: null, dialog: null,
tabContextMenu: null, tabContextMenu: null,
interpreters: [],
// Инициализация // Инициализация
initialize: (initialFiles: FileNode) => { initialize: (initialFiles: FileNode) => {
@@ -295,10 +299,21 @@ export const useIDEStore = create<IDEState>((set, get) => ({
}); });
}, },
// Интерпретаторы
fetchInterpreters: async () => {
try {
const interpreters = await scriptsApi.getInterpreters();
set({ interpreters });
} catch (e) {
console.error("Failed to fetch interpreters:", e);
}
},
// API: загрузка дерева с сервера // API: загрузка дерева с сервера
fetchTree: async () => { fetchTree: async () => {
try { try {
const data = await scriptsApi.getTree(); const data = await scriptsApi.getTree();
const { expandedFolders } = get();
const convertItem = (item: any): FileNode => { const convertItem = (item: any): FileNode => {
const node: FileNode = { const node: FileNode = {
@@ -332,7 +347,7 @@ export const useIDEStore = create<IDEState>((set, get) => ({
type: "folder", type: "folder",
children: roots, children: roots,
}, },
expandedFolders: new Set(), expandedFolders,
isInitialized: true, isInitialized: true,
}); });
} catch (e) { } catch (e) {
@@ -352,6 +367,37 @@ export const useIDEStore = create<IDEState>((set, get) => ({
} }
}, },
// API: создание папки
createFolder: async (path: string) => {
try {
await scriptsApi.createFolder(path);
await get().fetchTree();
} catch (e) {
console.error("Failed to create folder:", e);
throw e;
}
},
// API: удаление папки
deleteFolder: async ({ path }: { path: string }) => {
try {
const { openFiles } = get();
// Закрываем все файлы, которые находятся в удаляемой папке
const folderPathPrefix = path.endsWith("/") ? path : `${path}/`;
const filesToClose = openFiles.filter(
(f) => f.path === path || f.path?.startsWith(folderPathPrefix),
);
filesToClose.forEach((f) => get().closeFile(f));
await scriptsApi.deleteFolder(path);
await get().fetchTree();
} catch (e) {
console.error("Failed to delete folder:", e);
throw e;
}
},
// API: обновление скрипта // API: обновление скрипта
updateScript: async (id, payload) => { updateScript: async (id, payload) => {
try { try {
@@ -412,7 +458,7 @@ export const useIDEStore = create<IDEState>((set, get) => ({
setTabContextMenu: (menu) => set({ tabContextMenu: menu }), setTabContextMenu: (menu) => set({ tabContextMenu: menu }),
// Подтверждение диалога // Подтверждение диалога
handleDialogConfirm: async (value: string) => { handleDialogConfirm: async (value: string, interpreterId?: number) => {
const { dialog, files, toggleFolder, autoExpandPaths } = get(); const { dialog, files, toggleFolder, autoExpandPaths } = get();
if (!dialog) return; if (!dialog) return;
@@ -430,14 +476,45 @@ export const useIDEStore = create<IDEState>((set, get) => ({
alert(`"${value}" already exists.`); alert(`"${value}" already exists.`);
return; return;
} }
const newFiles = renameNode(
files!, const oldPath = dialog.node.path || dialog.node.name;
dialog.node.path || dialog.node.name, const newPath = parentPath ? `${parentPath}/${value}` : value;
value,
); // Сохраняем раскрытые папки
if (newFiles) { const savedExpandedFolders = new Set(get().expandedFolders);
set({ files: newFiles });
try {
await scriptsApi.rename({ old_path: oldPath, new_path: newPath });
await get().fetchTree();
// Восстанавливаем раскрытые папки
set({ expandedFolders: savedExpandedFolders });
// Раскрываем родительскую цепочку
const allParentPaths: string[] = [];
let current = parentPath;
while (current) {
allParentPaths.push(current);
const parts = current.split("/");
parts.pop();
current = parts.join("/");
}
autoExpandPaths(new Set(allParentPaths));
// Если переименованный файл был открыт — обновим его в openFiles
const { openFiles, activeFile } = get();
const updatedOpenFiles = openFiles.map((f) =>
f.path === oldPath ? { ...f, name: value, path: newPath } : f,
);
set({ openFiles: updatedOpenFiles });
if (activeFile?.path === oldPath) {
set({ activeFile: { ...activeFile, name: value, path: newPath } });
}
} catch (e) {
console.error("Failed to rename:", e);
} }
set({ dialog: null }); set({ dialog: null });
return; return;
} }
@@ -480,31 +557,52 @@ export const useIDEStore = create<IDEState>((set, get) => ({
const savedExpandedFolders = new Set(get().expandedFolders); const savedExpandedFolders = new Set(get().expandedFolders);
try { try {
const result = await scriptsApi.createScript({ // Создание папки
content: "", if (dialog.type === "newFolder" && !isFile) {
interpreter_id: 1, await scriptsApi.createFolder(fullPath);
path: fullPath, await get().fetchTree();
});
await get().fetchTree(); // Восстанавливаем раскрытые папки
set({ expandedFolders: savedExpandedFolders });
// Восстанавливаем раскрытые папки // Собираем все пути от корня до родительской папки
set({ expandedFolders: savedExpandedFolders }); const allParentPaths: string[] = [];
let current = parentPath;
while (current) {
allParentPaths.push(current);
const parts = current.split("/");
parts.pop();
current = parts.join("/");
}
// Собираем все пути от корня до родительской папки // Раскрываем родительскую цепочку
const allParentPaths: string[] = []; autoExpandPaths(new Set(allParentPaths));
let current = parentPath; } else {
while (current) { // Создание файла
allParentPaths.push(current); const result = await scriptsApi.createScript({
const parts = current.split("/"); content: "",
parts.pop(); interpreter_id: interpreterId || 0,
current = parts.join("/"); path: fullPath,
} });
// Раскрываем родительскую цепочку await get().fetchTree();
autoExpandPaths(new Set(allParentPaths));
// Восстанавливаем раскрытые папки
set({ expandedFolders: savedExpandedFolders });
// Собираем все пути от корня до родительской папки
const allParentPaths: string[] = [];
let current = parentPath;
while (current) {
allParentPaths.push(current);
const parts = current.split("/");
parts.pop();
current = parts.join("/");
}
// Раскрываем родительскую цепочку
autoExpandPaths(new Set(allParentPaths));
if (isFile) {
const createdNode: FileNode = { const createdNode: FileNode = {
id: result.id, id: result.id,
name: finalName, name: finalName,
@@ -529,12 +627,14 @@ export const useIDEStore = create<IDEState>((set, get) => ({
if (isRootNode) { if (isRootNode) {
get().deleteRoot(); get().deleteRoot();
} else if (window.confirm(`Delete "${node.name}"?`)) { } else if (window.confirm(`Delete "${node.name}"?`)) {
if (node.id) { try {
try { if (node.type === "folder") {
await get().deleteFolder({ path: node.path || node.name });
} else if (node.id) {
await get().deleteScript(node.id); await get().deleteScript(node.id);
} catch (e) {
console.error("Failed to delete:", e);
} }
} catch (e) {
console.error("Failed to delete:", e);
} }
} }
}, },
+10
View File
@@ -6,6 +6,15 @@ export interface FileNode {
path?: string; path?: string;
} }
export interface Interpreter {
id: number;
name: string;
label: string;
argv: string[];
created_at: string;
updated_at: string;
}
export interface ContextMenuState { export interface ContextMenuState {
x: number; x: number;
y: number; y: number;
@@ -15,6 +24,7 @@ export interface ContextMenuState {
export interface DialogState { export interface DialogState {
type: "newFile" | "newFolder" | "rename"; type: "newFile" | "newFolder" | "rename";
node: FileNode | null; node: FileNode | null;
interpreterId?: number;
} }
export interface TabContextMenuState { export interface TabContextMenuState {