This commit is contained in:
@@ -83,4 +83,12 @@ export const scriptsApi = {
|
|||||||
);
|
);
|
||||||
return res.data;
|
return res.data;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
run: async (path: string): Promise<{ id: number; status: string }> => {
|
||||||
|
const res = await apiClient.post<{ id: number; status: string }>(
|
||||||
|
"/scripts/run",
|
||||||
|
{ path },
|
||||||
|
);
|
||||||
|
return res.data;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,20 +5,21 @@ import { useFilePickerStore } from "../store/useFilePickerStore";
|
|||||||
|
|
||||||
interface FilePickerProps {
|
interface FilePickerProps {
|
||||||
files: FileNode;
|
files: FileNode;
|
||||||
|
onRun?: (path: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FilePickerTree: React.FC<{ node: FileNode; level: number }> = ({
|
const FilePickerTree: React.FC<{
|
||||||
node,
|
node: FileNode;
|
||||||
level,
|
level: number;
|
||||||
}) => {
|
onRun?: (path: string) => void;
|
||||||
|
}> = ({ node, level, onRun }) => {
|
||||||
const expandedFolders = useFilePickerStore((s) => s.expandedFolders);
|
const expandedFolders = useFilePickerStore((s) => s.expandedFolders);
|
||||||
const selectedPaths = useFilePickerStore((s) => s.selectedPaths);
|
const runningScripts = useFilePickerStore((s) => s.runningScripts);
|
||||||
const toggleSelection = useFilePickerStore((s) => s.toggleSelection);
|
|
||||||
const toggleFolder = useFilePickerStore((s) => s.toggleFolder);
|
const toggleFolder = useFilePickerStore((s) => s.toggleFolder);
|
||||||
|
|
||||||
const nodePath = node.path || node.name;
|
const nodePath = node.path || node.name;
|
||||||
const isExpanded = expandedFolders.has(nodePath);
|
const isExpanded = expandedFolders.has(nodePath);
|
||||||
const isSelected = node.type === "file" && selectedPaths.has(nodePath);
|
const isRunning = node.type === "file" && runningScripts.has(nodePath);
|
||||||
|
|
||||||
if (node.type === "file") {
|
if (node.type === "file") {
|
||||||
return (
|
return (
|
||||||
@@ -26,9 +27,9 @@ const FilePickerTree: React.FC<{ node: FileNode; level: number }> = ({
|
|||||||
name={node.name}
|
name={node.name}
|
||||||
type="file"
|
type="file"
|
||||||
path={nodePath}
|
path={nodePath}
|
||||||
isSelected={isSelected}
|
isSelected={isRunning}
|
||||||
level={level}
|
level={level}
|
||||||
onToggleSelect={toggleSelection}
|
onRun={onRun}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -44,14 +45,19 @@ const FilePickerTree: React.FC<{ node: FileNode; level: number }> = ({
|
|||||||
onToggleFolder={toggleFolder}
|
onToggleFolder={toggleFolder}
|
||||||
>
|
>
|
||||||
{node.children?.map((child, idx) => (
|
{node.children?.map((child, idx) => (
|
||||||
<FilePickerTree key={idx} node={child} level={level + 1} />
|
<FilePickerTree
|
||||||
|
key={idx}
|
||||||
|
node={child}
|
||||||
|
level={level + 1}
|
||||||
|
onRun={onRun}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</FilePickerItem>
|
</FilePickerItem>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FilePicker: React.FC<FilePickerProps> = ({ files }) => {
|
export const FilePicker: React.FC<FilePickerProps> = ({ files, onRun }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -61,7 +67,7 @@ export const FilePicker: React.FC<FilePickerProps> = ({ files }) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(files.children || []).map((child, idx) => (
|
{(files.children || []).map((child, idx) => (
|
||||||
<FilePickerTree key={idx} node={child} level={0} />
|
<FilePickerTree key={idx} node={child} level={0} onRun={onRun} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
FiChevronDown,
|
FiChevronDown,
|
||||||
FiFile,
|
FiFile,
|
||||||
FiFolder,
|
FiFolder,
|
||||||
|
FiPlay,
|
||||||
} from "react-icons/fi";
|
} from "react-icons/fi";
|
||||||
|
|
||||||
interface FilePickerItemProps {
|
interface FilePickerItemProps {
|
||||||
@@ -16,6 +17,7 @@ interface FilePickerItemProps {
|
|||||||
level: number;
|
level: number;
|
||||||
onToggleSelect?: (path: string) => void;
|
onToggleSelect?: (path: string) => void;
|
||||||
onToggleFolder?: (path: string) => void;
|
onToggleFolder?: (path: string) => void;
|
||||||
|
onRun?: (path: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FilePickerItem: React.FC<FilePickerItemProps> = ({
|
export const FilePickerItem: React.FC<FilePickerItemProps> = ({
|
||||||
@@ -28,6 +30,7 @@ export const FilePickerItem: React.FC<FilePickerItemProps> = ({
|
|||||||
level,
|
level,
|
||||||
onToggleSelect,
|
onToggleSelect,
|
||||||
onToggleFolder,
|
onToggleFolder,
|
||||||
|
onRun,
|
||||||
}) => {
|
}) => {
|
||||||
const isFolder = type === "folder";
|
const isFolder = type === "folder";
|
||||||
const extension = name.includes(".")
|
const extension = name.includes(".")
|
||||||
@@ -120,40 +123,48 @@ export const FilePickerItem: React.FC<FilePickerItemProps> = ({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Checkbox — только у файлов */}
|
{/* Run button — только у файлов */}
|
||||||
{!isFolder && onToggleSelect && (
|
{!isFolder && onRun && (
|
||||||
<div
|
<button
|
||||||
style={{
|
style={{
|
||||||
width: "18px",
|
|
||||||
height: "18px",
|
|
||||||
border: isSelected
|
|
||||||
? "2px solid #0e639c"
|
|
||||||
: "2px solid var(--border)",
|
|
||||||
borderRadius: "3px",
|
|
||||||
backgroundColor: isSelected ? "#0e639c" : "transparent",
|
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
padding: "4px",
|
||||||
|
backgroundColor: isSelected ? "#238636" : "transparent",
|
||||||
|
border: isSelected
|
||||||
|
? "1px solid #2ea043"
|
||||||
|
: "1px solid transparent",
|
||||||
|
borderRadius: "3px",
|
||||||
|
color: isSelected ? "#ffffff" : "var(--text-secondary)",
|
||||||
|
cursor: "pointer",
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
transition: "all 0.15s",
|
transition: "all 0.15s",
|
||||||
}}
|
}}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onToggleSelect(path);
|
onRun(path);
|
||||||
}}
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = "#238636";
|
||||||
|
e.currentTarget.style.color = "#ffffff";
|
||||||
|
e.currentTarget.style.borderColor = "#2ea043";
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
e.currentTarget.style.backgroundColor = isSelected
|
||||||
|
? "#238636"
|
||||||
|
: "transparent";
|
||||||
|
e.currentTarget.style.color = isSelected
|
||||||
|
? "#ffffff"
|
||||||
|
: "var(--text-secondary)";
|
||||||
|
e.currentTarget.style.borderColor = isSelected
|
||||||
|
? "#2ea043"
|
||||||
|
: "transparent";
|
||||||
|
}}
|
||||||
|
title="Run script"
|
||||||
>
|
>
|
||||||
{isSelected && (
|
<FiPlay size={12} />
|
||||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none">
|
</button>
|
||||||
<path
|
|
||||||
d="M2 6L5 9L10 3"
|
|
||||||
stroke="white"
|
|
||||||
strokeWidth="2"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
import { create } from "zustand";
|
import { create } from "zustand";
|
||||||
|
import { scriptsApi } from "../api/scripts.api";
|
||||||
|
|
||||||
interface FilePickerState {
|
interface FilePickerState {
|
||||||
selectedPaths: Set<string>;
|
selectedPaths: Set<string>;
|
||||||
expandedFolders: Set<string>;
|
expandedFolders: Set<string>;
|
||||||
|
runningScripts: Map<string, { id: number; status: string }>;
|
||||||
|
|
||||||
toggleSelection: (path: string) => void;
|
toggleSelection: (path: string) => void;
|
||||||
selectAll: (paths: string[]) => void;
|
selectAll: (paths: string[]) => void;
|
||||||
clearSelection: () => void;
|
clearSelection: () => void;
|
||||||
toggleFolder: (path: string) => void;
|
toggleFolder: (path: string) => void;
|
||||||
getSelectedPaths: () => string[];
|
getSelectedPaths: () => string[];
|
||||||
|
runScript: (path: string) => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFilePickerStore = create<FilePickerState>((set, get) => ({
|
export const useFilePickerStore = create<FilePickerState>((set, get) => ({
|
||||||
selectedPaths: new Set(),
|
selectedPaths: new Set(),
|
||||||
expandedFolders: new Set(),
|
expandedFolders: new Set(),
|
||||||
|
runningScripts: new Map(),
|
||||||
|
|
||||||
toggleSelection: (path: string) => {
|
toggleSelection: (path: string) => {
|
||||||
set((state) => {
|
set((state) => {
|
||||||
@@ -54,4 +58,17 @@ export const useFilePickerStore = create<FilePickerState>((set, get) => ({
|
|||||||
getSelectedPaths: () => {
|
getSelectedPaths: () => {
|
||||||
return Array.from(get().selectedPaths);
|
return Array.from(get().selectedPaths);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
runScript: async (path: string) => {
|
||||||
|
try {
|
||||||
|
const result = await scriptsApi.run(path);
|
||||||
|
set((state) => {
|
||||||
|
const newMap = new Map(state.runningScripts);
|
||||||
|
newMap.set(path, result);
|
||||||
|
return { runningScripts: newMap };
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to run script:", e);
|
||||||
|
}
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ const convertTreeToFileNode = (data: any[]): FileNode => {
|
|||||||
export const TemplatesPage = () => {
|
export const TemplatesPage = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const selectedPaths = useFilePickerStore((s) => s.selectedPaths);
|
const selectedPaths = useFilePickerStore((s) => s.selectedPaths);
|
||||||
|
const runningScripts = useFilePickerStore((s) => s.runningScripts);
|
||||||
|
const runScript = useFilePickerStore((s) => s.runScript);
|
||||||
const [files, setFiles] = useState<FileNode | null>(null);
|
const [files, setFiles] = useState<FileNode | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
@@ -57,6 +59,12 @@ export const TemplatesPage = () => {
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const handleRun = async (path: string) => {
|
||||||
|
await runScript(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
const runningCount = runningScripts.size;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
@@ -91,8 +99,7 @@ export const TemplatesPage = () => {
|
|||||||
>
|
>
|
||||||
<FiPlay size={13} color="#61c454" />
|
<FiPlay size={13} color="#61c454" />
|
||||||
<span style={{ fontSize: "12px", color: "var(--text-secondary)" }}>
|
<span style={{ fontSize: "12px", color: "var(--text-secondary)" }}>
|
||||||
{selectedPaths.size} script{selectedPaths.size !== 1 ? "s" : ""}{" "}
|
{runningCount} script{runningCount !== 1 ? "s" : ""} running
|
||||||
running
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -145,7 +152,7 @@ export const TemplatesPage = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : files ? (
|
) : files ? (
|
||||||
<FilePicker files={files} />
|
<FilePicker files={files} onRun={handleRun} />
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user