mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-09-11 07:08:42 -06:00
feat: replace workflow ellipsis with context menu
This commit is contained in:
@@ -231,29 +231,33 @@ export default function App() {
|
|||||||
[setTabs]
|
[setTabs]
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleSaveFlow = useCallback(async () => {
|
const handleSaveFlow = useCallback(
|
||||||
const tab = tabs.find((t) => t.id === activeTabId);
|
async (name) => {
|
||||||
if (!tab) return;
|
const tab = tabs.find((t) => t.id === activeTabId);
|
||||||
const name = window.prompt("Enter workflow name", tab.tab_name || "workflow");
|
if (!tab || !name) return;
|
||||||
if (!name) return;
|
const payload = {
|
||||||
const payload = {
|
path: tab.folderPath ? `${tab.folderPath}/${name}` : name,
|
||||||
name,
|
workflow: {
|
||||||
workflow: {
|
tab_name: tab.tab_name,
|
||||||
tab_name: tab.tab_name,
|
nodes: tab.nodes,
|
||||||
nodes: tab.nodes,
|
edges: tab.edges
|
||||||
edges: tab.edges
|
}
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
await fetch("/api/storage/save_workflow", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
});
|
||||||
|
setTabs((prev) =>
|
||||||
|
prev.map((t) => (t.id === activeTabId ? { ...t, tab_name: name } : t))
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to save workflow:", err);
|
||||||
}
|
}
|
||||||
};
|
},
|
||||||
try {
|
[tabs, activeTabId]
|
||||||
await fetch("/api/storage/save_workflow", {
|
);
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to save workflow:", err);
|
|
||||||
}
|
|
||||||
}, [tabs, activeTabId]);
|
|
||||||
|
|
||||||
const renderMainContent = () => {
|
const renderMainContent = () => {
|
||||||
switch (currentPage) {
|
switch (currentPage) {
|
||||||
@@ -266,9 +270,13 @@ export default function App() {
|
|||||||
case "workflows":
|
case "workflows":
|
||||||
return (
|
return (
|
||||||
<WorkflowList
|
<WorkflowList
|
||||||
onOpenWorkflow={async (workflow) => {
|
onOpenWorkflow={async (workflow, folderPath, name) => {
|
||||||
const newId = "flow_" + Date.now();
|
const newId = "flow_" + Date.now();
|
||||||
if (workflow && workflow.rel_path) {
|
if (workflow && workflow.rel_path) {
|
||||||
|
const folder = workflow.rel_path
|
||||||
|
.split("/")
|
||||||
|
.slice(0, -1)
|
||||||
|
.join("/");
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(
|
const resp = await fetch(
|
||||||
`/api/storage/load_workflow?path=${encodeURIComponent(
|
`/api/storage/load_workflow?path=${encodeURIComponent(
|
||||||
@@ -283,17 +291,32 @@ export default function App() {
|
|||||||
tab_name:
|
tab_name:
|
||||||
data.tab_name || workflow.name || workflow.file_name || "Workflow",
|
data.tab_name || workflow.name || workflow.file_name || "Workflow",
|
||||||
nodes: data.nodes || [],
|
nodes: data.nodes || [],
|
||||||
edges: data.edges || []
|
edges: data.edges || [],
|
||||||
|
folderPath: folder
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load workflow:", err);
|
console.error("Failed to load workflow:", err);
|
||||||
setTabs([
|
setTabs([
|
||||||
{ id: newId, tab_name: workflow?.name || "Workflow", nodes: [], edges: [] }
|
{
|
||||||
|
id: newId,
|
||||||
|
tab_name: workflow?.name || "Workflow",
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
folderPath: folder
|
||||||
|
}
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
setTabs([{ id: newId, tab_name: `Flow`, nodes: [], edges: [] }]);
|
setTabs([
|
||||||
|
{
|
||||||
|
id: newId,
|
||||||
|
tab_name: name || "Flow",
|
||||||
|
nodes: [],
|
||||||
|
edges: [],
|
||||||
|
folderPath: folderPath || ""
|
||||||
|
}
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
setActiveTabId(newId);
|
setActiveTabId(newId);
|
||||||
setCurrentPage("workflow-editor");
|
setCurrentPage("workflow-editor");
|
||||||
@@ -316,6 +339,7 @@ export default function App() {
|
|||||||
handleOpenCloseAllDialog={() => setConfirmCloseOpen(true)}
|
handleOpenCloseAllDialog={() => setConfirmCloseOpen(true)}
|
||||||
fileInputRef={fileInputRef}
|
fileInputRef={fileInputRef}
|
||||||
onFileInputChange={onFileInputChange}
|
onFileInputChange={onFileInputChange}
|
||||||
|
currentTabName={tabs.find((t) => t.id === activeTabId)?.tab_name}
|
||||||
/>
|
/>
|
||||||
<Box sx={{ display: "flex", flexDirection: "column", flexGrow: 1, overflow: "hidden" }}>
|
<Box sx={{ display: "flex", flexDirection: "column", flexGrow: 1, overflow: "hidden" }}>
|
||||||
<FlowTabs
|
<FlowTabs
|
||||||
|
@@ -132,10 +132,18 @@ export function RenameWorkflowDialog({ open, value, onChange, onCancel, onSave }
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RenameFolderDialog({ open, value, onChange, onCancel, onSave }) {
|
export function RenameFolderDialog({
|
||||||
|
open,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
onCancel,
|
||||||
|
onSave,
|
||||||
|
title = "Folder Name",
|
||||||
|
confirmText = "Save"
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onCancel} PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}>
|
<Dialog open={open} onClose={onCancel} PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}>
|
||||||
<DialogTitle>Folder Name</DialogTitle>
|
<DialogTitle>{title}</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<TextField
|
<TextField
|
||||||
autoFocus
|
autoFocus
|
||||||
@@ -157,6 +165,72 @@ export function RenameFolderDialog({ open, value, onChange, onCancel, onSave })
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onCancel} sx={{ color: "#58a6ff" }}>Cancel</Button>
|
||||||
|
<Button onClick={onSave} sx={{ color: "#58a6ff" }}>{confirmText}</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function NewWorkflowDialog({ open, value, onChange, onCancel, onCreate }) {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onCancel} PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}>
|
||||||
|
<DialogTitle>New Workflow</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
label="Workflow Name"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
sx={{
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
backgroundColor: "#2a2a2a",
|
||||||
|
color: "#ccc",
|
||||||
|
"& fieldset": { borderColor: "#444" },
|
||||||
|
"&:hover fieldset": { borderColor: "#666" }
|
||||||
|
},
|
||||||
|
label: { color: "#aaa" },
|
||||||
|
mt: 1
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onCancel} sx={{ color: "#58a6ff" }}>Cancel</Button>
|
||||||
|
<Button onClick={onCreate} sx={{ color: "#58a6ff" }}>Create</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SaveWorkflowDialog({ open, value, onChange, onCancel, onSave }) {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onCancel} PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}>
|
||||||
|
<DialogTitle>Save Workflow</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
label="Workflow Name"
|
||||||
|
fullWidth
|
||||||
|
variant="outlined"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
sx={{
|
||||||
|
"& .MuiOutlinedInput-root": {
|
||||||
|
backgroundColor: "#2a2a2a",
|
||||||
|
color: "#ccc",
|
||||||
|
"& fieldset": { borderColor: "#444" },
|
||||||
|
"&:hover fieldset": { borderColor: "#666" }
|
||||||
|
},
|
||||||
|
label: { color: "#aaa" },
|
||||||
|
mt: 1
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={onCancel} sx={{ color: "#58a6ff" }}>Cancel</Button>
|
<Button onClick={onCancel} sx={{ color: "#58a6ff" }}>Cancel</Button>
|
||||||
<Button onClick={onSave} sx={{ color: "#58a6ff" }}>Save</Button>
|
<Button onClick={onSave} sx={{ color: "#58a6ff" }}>Save</Button>
|
||||||
@@ -165,6 +239,21 @@ export function RenameFolderDialog({ open, value, onChange, onCancel, onSave })
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function ConfirmDeleteDialog({ open, message, onCancel, onConfirm }) {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onCancel} PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}>
|
||||||
|
<DialogTitle>Confirm Delete</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText sx={{ color: "#ccc" }}>{message}</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={onCancel} sx={{ color: "#58a6ff" }}>Cancel</Button>
|
||||||
|
<Button onClick={onConfirm} sx={{ color: "#58a6ff" }}>Confirm</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function DeleteDeviceDialog({ open, onCancel, onConfirm }) {
|
export function DeleteDeviceDialog({ open, onCancel, onConfirm }) {
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onClose={onCancel} PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}>
|
<Dialog open={open} onClose={onCancel} PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}>
|
||||||
|
@@ -21,6 +21,7 @@ import {
|
|||||||
ChevronLeft as ChevronLeftIcon,
|
ChevronLeft as ChevronLeftIcon,
|
||||||
ChevronRight as ChevronRightIcon
|
ChevronRight as ChevronRightIcon
|
||||||
} from "@mui/icons-material";
|
} from "@mui/icons-material";
|
||||||
|
import { SaveWorkflowDialog } from "./Dialogs";
|
||||||
|
|
||||||
export default function NodeSidebar({
|
export default function NodeSidebar({
|
||||||
categorizedNodes,
|
categorizedNodes,
|
||||||
@@ -29,10 +30,13 @@ export default function NodeSidebar({
|
|||||||
handleSaveFlow,
|
handleSaveFlow,
|
||||||
handleOpenCloseAllDialog,
|
handleOpenCloseAllDialog,
|
||||||
fileInputRef,
|
fileInputRef,
|
||||||
onFileInputChange
|
onFileInputChange,
|
||||||
|
currentTabName
|
||||||
}) {
|
}) {
|
||||||
const [expandedCategory, setExpandedCategory] = useState(null);
|
const [expandedCategory, setExpandedCategory] = useState(null);
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
const [saveOpen, setSaveOpen] = useState(false);
|
||||||
|
const [saveName, setSaveName] = useState("");
|
||||||
|
|
||||||
const handleAccordionChange = (category) => (_, isExpanded) => {
|
const handleAccordionChange = (category) => (_, isExpanded) => {
|
||||||
setExpandedCategory(isExpanded ? category : null);
|
setExpandedCategory(isExpanded ? category : null);
|
||||||
@@ -79,7 +83,15 @@ export default function NodeSidebar({
|
|||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="Save Current Flow to Workflows Folder" placement="right" arrow>
|
<Tooltip title="Save Current Flow to Workflows Folder" placement="right" arrow>
|
||||||
<Button fullWidth startIcon={<SaveIcon />} onClick={handleSaveFlow} sx={buttonStyle}>
|
<Button
|
||||||
|
fullWidth
|
||||||
|
startIcon={<SaveIcon />}
|
||||||
|
onClick={() => {
|
||||||
|
setSaveName(currentTabName || "workflow");
|
||||||
|
setSaveOpen(true);
|
||||||
|
}}
|
||||||
|
sx={buttonStyle}
|
||||||
|
>
|
||||||
Save Workflow
|
Save Workflow
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -213,6 +225,16 @@ export default function NodeSidebar({
|
|||||||
{collapsed ? <ChevronRightIcon /> : <ChevronLeftIcon />}
|
{collapsed ? <ChevronRightIcon /> : <ChevronLeftIcon />}
|
||||||
</Box>
|
</Box>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
<SaveWorkflowDialog
|
||||||
|
open={saveOpen}
|
||||||
|
value={saveName}
|
||||||
|
onChange={setSaveName}
|
||||||
|
onCancel={() => setSaveOpen(false)}
|
||||||
|
onSave={() => {
|
||||||
|
setSaveOpen(false);
|
||||||
|
handleSaveFlow(saveName);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -1,26 +1,17 @@
|
|||||||
import React, { useState, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import {
|
import { Paper, Box, Typography, Menu, MenuItem } from "@mui/material";
|
||||||
Paper,
|
import { Folder as FolderIcon, Description as DescriptionIcon } from "@mui/icons-material";
|
||||||
Box,
|
|
||||||
Typography,
|
|
||||||
Button,
|
|
||||||
IconButton,
|
|
||||||
Menu,
|
|
||||||
MenuItem
|
|
||||||
} from "@mui/material";
|
|
||||||
import {
|
|
||||||
PlayCircle as PlayCircleIcon,
|
|
||||||
MoreVert as MoreVertIcon,
|
|
||||||
Folder as FolderIcon,
|
|
||||||
Description as DescriptionIcon,
|
|
||||||
CreateNewFolder as CreateNewFolderIcon
|
|
||||||
} from "@mui/icons-material";
|
|
||||||
import {
|
import {
|
||||||
SimpleTreeView,
|
SimpleTreeView,
|
||||||
TreeItem,
|
TreeItem,
|
||||||
useTreeViewApiRef
|
useTreeViewApiRef
|
||||||
} from "@mui/x-tree-view";
|
} from "@mui/x-tree-view";
|
||||||
import { RenameWorkflowDialog, RenameFolderDialog } from "./Dialogs";
|
import {
|
||||||
|
RenameWorkflowDialog,
|
||||||
|
RenameFolderDialog,
|
||||||
|
NewWorkflowDialog,
|
||||||
|
ConfirmDeleteDialog
|
||||||
|
} from "./Dialogs";
|
||||||
|
|
||||||
function buildTree(workflows, folders) {
|
function buildTree(workflows, folders) {
|
||||||
const map = {};
|
const map = {};
|
||||||
@@ -88,11 +79,15 @@ function buildTree(workflows, folders) {
|
|||||||
export default function WorkflowList({ onOpenWorkflow }) {
|
export default function WorkflowList({ onOpenWorkflow }) {
|
||||||
const [tree, setTree] = useState([]);
|
const [tree, setTree] = useState([]);
|
||||||
const [nodeMap, setNodeMap] = useState({});
|
const [nodeMap, setNodeMap] = useState({});
|
||||||
const [menuAnchor, setMenuAnchor] = useState(null);
|
const [contextMenu, setContextMenu] = useState(null);
|
||||||
const [selectedNode, setSelectedNode] = useState(null);
|
const [selectedNode, setSelectedNode] = useState(null);
|
||||||
const [renameValue, setRenameValue] = useState("");
|
const [renameValue, setRenameValue] = useState("");
|
||||||
const [renameOpen, setRenameOpen] = useState(false);
|
const [renameOpen, setRenameOpen] = useState(false);
|
||||||
const [renameFolderOpen, setRenameFolderOpen] = useState(false);
|
const [renameFolderOpen, setRenameFolderOpen] = useState(false);
|
||||||
|
const [folderDialogMode, setFolderDialogMode] = useState("rename");
|
||||||
|
const [newWorkflowOpen, setNewWorkflowOpen] = useState(false);
|
||||||
|
const [newWorkflowName, setNewWorkflowName] = useState("");
|
||||||
|
const [deleteOpen, setDeleteOpen] = useState(false);
|
||||||
const apiRef = useTreeViewApiRef();
|
const apiRef = useTreeViewApiRef();
|
||||||
const [dragNode, setDragNode] = useState(null);
|
const [dragNode, setDragNode] = useState(null);
|
||||||
|
|
||||||
@@ -136,50 +131,57 @@ export default function WorkflowList({ onOpenWorkflow }) {
|
|||||||
loadTree();
|
loadTree();
|
||||||
}, [loadTree]);
|
}, [loadTree]);
|
||||||
|
|
||||||
const openMenu = (e, node) => {
|
const handleContextMenu = (e, node) => {
|
||||||
e.stopPropagation();
|
e.preventDefault();
|
||||||
setMenuAnchor(e.currentTarget);
|
|
||||||
setSelectedNode(node);
|
setSelectedNode(node);
|
||||||
|
setContextMenu(
|
||||||
|
contextMenu === null
|
||||||
|
? {
|
||||||
|
mouseX: e.clientX - 2,
|
||||||
|
mouseY: e.clientY - 4
|
||||||
|
}
|
||||||
|
: null
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeMenu = () => setMenuAnchor(null);
|
|
||||||
|
|
||||||
const handleRename = () => {
|
const handleRename = () => {
|
||||||
closeMenu();
|
setContextMenu(null);
|
||||||
if (!selectedNode) return;
|
if (!selectedNode) return;
|
||||||
setRenameValue(selectedNode.label);
|
setRenameValue(selectedNode.label);
|
||||||
if (selectedNode.isFolder) setRenameFolderOpen(true);
|
if (selectedNode.isFolder) {
|
||||||
else setRenameOpen(true);
|
setFolderDialogMode("rename");
|
||||||
|
setRenameFolderOpen(true);
|
||||||
|
} else setRenameOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = () => {
|
const handleEdit = () => {
|
||||||
closeMenu();
|
setContextMenu(null);
|
||||||
if (selectedNode && !selectedNode.isFolder && onOpenWorkflow) {
|
if (selectedNode && !selectedNode.isFolder && onOpenWorkflow) {
|
||||||
onOpenWorkflow(selectedNode.workflow);
|
onOpenWorkflow(selectedNode.workflow);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteWorkflow = async () => {
|
const handleDelete = () => {
|
||||||
closeMenu();
|
setContextMenu(null);
|
||||||
if (!selectedNode) return;
|
if (!selectedNode) return;
|
||||||
try {
|
setDeleteOpen(true);
|
||||||
await fetch("/api/storage/delete_workflow", {
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json" },
|
|
||||||
body: JSON.stringify({ path: selectedNode.path })
|
|
||||||
});
|
|
||||||
loadTree();
|
|
||||||
} catch (err) {
|
|
||||||
console.error("Failed to delete workflow:", err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateFolder = () => {
|
const handleNewFolder = () => {
|
||||||
closeMenu();
|
if (!selectedNode) return;
|
||||||
|
setContextMenu(null);
|
||||||
|
setFolderDialogMode("create");
|
||||||
setRenameValue("");
|
setRenameValue("");
|
||||||
setRenameFolderOpen(true);
|
setRenameFolderOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNewWorkflow = () => {
|
||||||
|
if (!selectedNode) return;
|
||||||
|
setContextMenu(null);
|
||||||
|
setNewWorkflowName("");
|
||||||
|
setNewWorkflowOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
const saveRenameWorkflow = async () => {
|
const saveRenameWorkflow = async () => {
|
||||||
if (!selectedNode) return;
|
if (!selectedNode) return;
|
||||||
try {
|
try {
|
||||||
@@ -197,7 +199,7 @@ export default function WorkflowList({ onOpenWorkflow }) {
|
|||||||
|
|
||||||
const saveRenameFolder = async () => {
|
const saveRenameFolder = async () => {
|
||||||
try {
|
try {
|
||||||
if (selectedNode && selectedNode.isFolder) {
|
if (folderDialogMode === "rename" && selectedNode) {
|
||||||
await fetch("/api/storage/rename_folder", {
|
await fetch("/api/storage/rename_folder", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
@@ -226,6 +228,29 @@ export default function WorkflowList({ onOpenWorkflow }) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmDelete = async () => {
|
||||||
|
if (!selectedNode) return;
|
||||||
|
try {
|
||||||
|
if (selectedNode.isFolder) {
|
||||||
|
await fetch("/api/storage/delete_folder", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ path: selectedNode.path })
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await fetch("/api/storage/delete_workflow", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ path: selectedNode.path })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
loadTree();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to delete:", err);
|
||||||
|
}
|
||||||
|
setDeleteOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
const renderItems = (nodes) =>
|
const renderItems = (nodes) =>
|
||||||
nodes.map((n) => (
|
nodes.map((n) => (
|
||||||
<TreeItem
|
<TreeItem
|
||||||
@@ -243,6 +268,7 @@ export default function WorkflowList({ onOpenWorkflow }) {
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleDrop(n);
|
handleDrop(n);
|
||||||
}}
|
}}
|
||||||
|
onContextMenu={(e) => handleContextMenu(e, n)}
|
||||||
>
|
>
|
||||||
{n.isFolder ? (
|
{n.isFolder ? (
|
||||||
<FolderIcon sx={{ mr: 1, color: "#0475c2" }} />
|
<FolderIcon sx={{ mr: 1, color: "#0475c2" }} />
|
||||||
@@ -250,13 +276,6 @@ export default function WorkflowList({ onOpenWorkflow }) {
|
|||||||
<DescriptionIcon sx={{ mr: 1, color: "#0475c2" }} />
|
<DescriptionIcon sx={{ mr: 1, color: "#0475c2" }} />
|
||||||
)}
|
)}
|
||||||
<Typography sx={{ flexGrow: 1, color: "#e6edf3" }}>{n.label}</Typography>
|
<Typography sx={{ flexGrow: 1, color: "#e6edf3" }}>{n.label}</Typography>
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={(e) => openMenu(e, n)}
|
|
||||||
sx={{ color: "#ccc" }}
|
|
||||||
>
|
|
||||||
<MoreVertIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
</Box>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
@@ -283,41 +302,7 @@ export default function WorkflowList({ onOpenWorkflow }) {
|
|||||||
Manage workflow folders and files.
|
Manage workflow folders and files.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box />
|
||||||
<Button
|
|
||||||
startIcon={<CreateNewFolderIcon />}
|
|
||||||
sx={{
|
|
||||||
mr: 1,
|
|
||||||
color: "#0475c2",
|
|
||||||
borderColor: "#0475c2",
|
|
||||||
textTransform: "none",
|
|
||||||
border: "1px solid #0475c2",
|
|
||||||
backgroundColor: "#1e1e1e",
|
|
||||||
"&:hover": { backgroundColor: "#1b1b1b" }
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedNode(null);
|
|
||||||
setRenameValue("");
|
|
||||||
setRenameFolderOpen(true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
New Folder
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
startIcon={<PlayCircleIcon />}
|
|
||||||
sx={{
|
|
||||||
color: "#0475c2",
|
|
||||||
borderColor: "#0475c2",
|
|
||||||
textTransform: "none",
|
|
||||||
border: "1px solid #0475c2",
|
|
||||||
backgroundColor: "#1e1e1e",
|
|
||||||
"&:hover": { backgroundColor: "#1b1b1b" }
|
|
||||||
}}
|
|
||||||
onClick={() => onOpenWorkflow && onOpenWorkflow()}
|
|
||||||
>
|
|
||||||
New Workflow
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
<Box
|
<Box
|
||||||
sx={{ p: 2 }}
|
sx={{ p: 2 }}
|
||||||
@@ -339,21 +324,30 @@ export default function WorkflowList({ onOpenWorkflow }) {
|
|||||||
</SimpleTreeView>
|
</SimpleTreeView>
|
||||||
</Box>
|
</Box>
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={menuAnchor}
|
open={contextMenu !== null}
|
||||||
open={Boolean(menuAnchor)}
|
onClose={() => setContextMenu(null)}
|
||||||
onClose={closeMenu}
|
anchorReference="anchorPosition"
|
||||||
|
anchorPosition=
|
||||||
|
{contextMenu ? { top: contextMenu.mouseY, left: contextMenu.mouseX } : undefined}
|
||||||
PaperProps={{ sx: { bgcolor: "#1e1e1e", color: "#fff", fontSize: "13px" } }}
|
PaperProps={{ sx: { bgcolor: "#1e1e1e", color: "#fff", fontSize: "13px" } }}
|
||||||
>
|
>
|
||||||
{selectedNode?.isFolder && (
|
{selectedNode?.isFolder && (
|
||||||
<MenuItem onClick={handleCreateFolder}>New Folder</MenuItem>
|
<>
|
||||||
)}
|
<MenuItem onClick={handleNewWorkflow}>New Workflow</MenuItem>
|
||||||
{selectedNode && selectedNode.id !== "root" && (
|
<MenuItem onClick={handleNewFolder}>New Subfolder</MenuItem>
|
||||||
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
{selectedNode.id !== "root" && (
|
||||||
|
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
||||||
|
)}
|
||||||
|
{selectedNode.id !== "root" && (
|
||||||
|
<MenuItem onClick={handleDelete}>Delete</MenuItem>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{!selectedNode?.isFolder && (
|
{!selectedNode?.isFolder && (
|
||||||
<>
|
<>
|
||||||
<MenuItem onClick={handleEdit}>Edit</MenuItem>
|
<MenuItem onClick={handleEdit}>Edit</MenuItem>
|
||||||
<MenuItem onClick={handleDeleteWorkflow}>Delete</MenuItem>
|
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
||||||
|
<MenuItem onClick={handleDelete}>Delete</MenuItem>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
@@ -370,6 +364,24 @@ export default function WorkflowList({ onOpenWorkflow }) {
|
|||||||
onChange={setRenameValue}
|
onChange={setRenameValue}
|
||||||
onCancel={() => setRenameFolderOpen(false)}
|
onCancel={() => setRenameFolderOpen(false)}
|
||||||
onSave={saveRenameFolder}
|
onSave={saveRenameFolder}
|
||||||
|
title={folderDialogMode === "rename" ? "Rename Folder" : "New Folder"}
|
||||||
|
confirmText={folderDialogMode === "rename" ? "Save" : "Create"}
|
||||||
|
/>
|
||||||
|
<NewWorkflowDialog
|
||||||
|
open={newWorkflowOpen}
|
||||||
|
value={newWorkflowName}
|
||||||
|
onChange={setNewWorkflowName}
|
||||||
|
onCancel={() => setNewWorkflowOpen(false)}
|
||||||
|
onCreate={() => {
|
||||||
|
setNewWorkflowOpen(false);
|
||||||
|
onOpenWorkflow && onOpenWorkflow(null, selectedNode.path, newWorkflowName);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<ConfirmDeleteDialog
|
||||||
|
open={deleteOpen}
|
||||||
|
message="If you delete this, there is no undo button, are you sure you want to proceed?"
|
||||||
|
onCancel={() => setDeleteOpen(false)}
|
||||||
|
onConfirm={confirmDelete}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
@@ -127,6 +127,22 @@ def delete_workflow():
|
|||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/storage/delete_folder", methods=["POST"])
|
||||||
|
def delete_folder():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
rel_path = (data.get("path") or "").strip()
|
||||||
|
workflows_root = os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(__file__), "..", "..", "Workflows")
|
||||||
|
)
|
||||||
|
abs_path = os.path.abspath(os.path.join(workflows_root, rel_path))
|
||||||
|
if not abs_path.startswith(workflows_root) or not os.path.isdir(abs_path):
|
||||||
|
return jsonify({"error": "Folder not found"}), 404
|
||||||
|
try:
|
||||||
|
shutil.rmtree(abs_path)
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
@app.route("/api/storage/create_folder", methods=["POST"])
|
@app.route("/api/storage/create_folder", methods=["POST"])
|
||||||
def create_folder():
|
def create_folder():
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
@@ -274,23 +290,36 @@ def load_workflow():
|
|||||||
@app.route("/api/storage/save_workflow", methods=["POST"])
|
@app.route("/api/storage/save_workflow", methods=["POST"])
|
||||||
def save_workflow():
|
def save_workflow():
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
|
rel_path = (data.get("path") or "").strip()
|
||||||
name = (data.get("name") or "").strip()
|
name = (data.get("name") or "").strip()
|
||||||
workflow = data.get("workflow")
|
workflow = data.get("workflow")
|
||||||
if not name or not isinstance(workflow, dict):
|
if not isinstance(workflow, dict):
|
||||||
return jsonify({"error": "Invalid payload"}), 400
|
return jsonify({"error": "Invalid payload"}), 400
|
||||||
|
|
||||||
if not name.lower().endswith(".json"):
|
|
||||||
name += ".json"
|
|
||||||
workflows_root = os.path.abspath(
|
workflows_root = os.path.abspath(
|
||||||
os.path.join(os.path.dirname(__file__), "..", "..", "Workflows")
|
os.path.join(os.path.dirname(__file__), "..", "..", "Workflows")
|
||||||
)
|
)
|
||||||
os.makedirs(workflows_root, exist_ok=True)
|
os.makedirs(workflows_root, exist_ok=True)
|
||||||
safe_name = os.path.basename(name)
|
|
||||||
abs_path = os.path.join(workflows_root, safe_name)
|
if rel_path:
|
||||||
|
if not rel_path.lower().endswith(".json"):
|
||||||
|
rel_path += ".json"
|
||||||
|
abs_path = os.path.abspath(os.path.join(workflows_root, rel_path))
|
||||||
|
else:
|
||||||
|
if not name:
|
||||||
|
return jsonify({"error": "Invalid payload"}), 400
|
||||||
|
if not name.lower().endswith(".json"):
|
||||||
|
name += ".json"
|
||||||
|
abs_path = os.path.abspath(os.path.join(workflows_root, os.path.basename(name)))
|
||||||
|
|
||||||
|
if not abs_path.startswith(workflows_root):
|
||||||
|
return jsonify({"error": "Invalid path"}), 400
|
||||||
|
|
||||||
|
os.makedirs(os.path.dirname(abs_path), exist_ok=True)
|
||||||
try:
|
try:
|
||||||
with open(abs_path, "w", encoding="utf-8") as fh:
|
with open(abs_path, "w", encoding="utf-8") as fh:
|
||||||
json.dump(workflow, fh, indent=2)
|
json.dump(workflow, fh, indent=2)
|
||||||
return jsonify({"status": "ok", "file_name": safe_name})
|
return jsonify({"status": "ok"})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user