mirror of
https://github.com/bunny-lab-io/Borealis.git
synced 2025-09-11 06:48:43 -06:00
feat: replace workflow list with tree view
This commit is contained in:
@@ -12,6 +12,7 @@
|
|||||||
"@emotion/styled": "11.14.0",
|
"@emotion/styled": "11.14.0",
|
||||||
"@mui/icons-material": "7.0.2",
|
"@mui/icons-material": "7.0.2",
|
||||||
"@mui/material": "7.0.2",
|
"@mui/material": "7.0.2",
|
||||||
|
"@mui/x-tree-view": "8.10.0",
|
||||||
"normalize.css": "8.0.1",
|
"normalize.css": "8.0.1",
|
||||||
"prismjs": "1.30.0",
|
"prismjs": "1.30.0",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
|
@@ -132,6 +132,39 @@ export function RenameWorkflowDialog({ open, value, onChange, onCancel, onSave }
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function RenameFolderDialog({ open, value, onChange, onCancel, onSave }) {
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onClose={onCancel} PaperProps={{ sx: { bgcolor: "#121212", color: "#fff" } }}>
|
||||||
|
<DialogTitle>Folder Name</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<TextField
|
||||||
|
autoFocus
|
||||||
|
margin="dense"
|
||||||
|
label="Folder 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={onSave} sx={{ color: "#58a6ff" }}>Save</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" } }}>
|
||||||
|
@@ -1,273 +1,317 @@
|
|||||||
import React, { useState, useMemo, useEffect, useCallback } from "react";
|
import React, { useState, useEffect, useCallback } from "react";
|
||||||
import {
|
import {
|
||||||
Paper,
|
Paper,
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
Button,
|
Button,
|
||||||
Table,
|
|
||||||
TableBody,
|
|
||||||
TableCell,
|
|
||||||
TableHead,
|
|
||||||
TableRow,
|
|
||||||
TableSortLabel,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem
|
MenuItem
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import { PlayCircle as PlayCircleIcon, MoreVert as MoreVertIcon } from "@mui/icons-material";
|
import {
|
||||||
import { RenameWorkflowDialog } from "./Dialogs";
|
PlayCircle as PlayCircleIcon,
|
||||||
|
MoreVert as MoreVertIcon,
|
||||||
|
Folder as FolderIcon,
|
||||||
|
Description as DescriptionIcon,
|
||||||
|
CreateNewFolder as CreateNewFolderIcon
|
||||||
|
} from "@mui/icons-material";
|
||||||
|
import {
|
||||||
|
SimpleTreeView,
|
||||||
|
TreeItem,
|
||||||
|
useTreeViewApiRef,
|
||||||
|
useTreeViewDragAndDrop
|
||||||
|
} from "@mui/x-tree-view";
|
||||||
|
import { RenameWorkflowDialog, RenameFolderDialog } from "./Dialogs";
|
||||||
|
|
||||||
function formatDateTime(dateString) {
|
function buildTree(workflows) {
|
||||||
if (!dateString) return "";
|
const map = {};
|
||||||
const date = new Date(dateString);
|
const root = [];
|
||||||
if (isNaN(date)) return "";
|
(workflows || []).forEach((w) => {
|
||||||
const day = String(date.getDate()).padStart(2, "0");
|
const parts = (w.rel_path || "").split("/");
|
||||||
const month = String(date.getMonth() + 1).padStart(2, "0");
|
let children = root;
|
||||||
const year = date.getFullYear();
|
let parentPath = "";
|
||||||
let hours = date.getHours();
|
parts.forEach((part, idx) => {
|
||||||
const minutes = String(date.getMinutes()).padStart(2, "0");
|
const path = parentPath ? `${parentPath}/${part}` : part;
|
||||||
const ampm = hours >= 12 ? "PM" : "AM";
|
const isFile = idx === parts.length - 1;
|
||||||
hours = hours % 12 || 12;
|
let node = children.find((n) => n.id === path);
|
||||||
return `${day}/${month}/${year} ${hours}:${minutes}${ampm}`;
|
if (!node) {
|
||||||
|
node = {
|
||||||
|
id: path,
|
||||||
|
label: isFile
|
||||||
|
? w.tab_name && w.tab_name.trim().length > 0
|
||||||
|
? w.tab_name.trim()
|
||||||
|
: w.file_name
|
||||||
|
: part,
|
||||||
|
path,
|
||||||
|
isFolder: !isFile,
|
||||||
|
fileName: w.file_name,
|
||||||
|
workflow: isFile ? w : null,
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
children.push(node);
|
||||||
|
map[path] = node;
|
||||||
|
}
|
||||||
|
if (!isFile) {
|
||||||
|
children = node.children;
|
||||||
|
parentPath = path;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return { root, map };
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function WorkflowList({ onOpenWorkflow }) {
|
export default function WorkflowList({ onOpenWorkflow }) {
|
||||||
const [rows, setRows] = useState([]);
|
const [tree, setTree] = useState([]);
|
||||||
const [orderBy, setOrderBy] = useState("name");
|
const [nodeMap, setNodeMap] = useState({});
|
||||||
const [order, setOrder] = useState("asc");
|
|
||||||
const [menuAnchor, setMenuAnchor] = useState(null);
|
const [menuAnchor, setMenuAnchor] = useState(null);
|
||||||
const [selected, setSelected] = useState(null);
|
const [selectedNode, setSelectedNode] = useState(null);
|
||||||
const [renameOpen, setRenameOpen] = useState(false);
|
|
||||||
const [renameValue, setRenameValue] = useState("");
|
const [renameValue, setRenameValue] = useState("");
|
||||||
|
const [renameOpen, setRenameOpen] = useState(false);
|
||||||
|
const [renameFolderOpen, setRenameFolderOpen] = useState(false);
|
||||||
|
const apiRef = useTreeViewApiRef();
|
||||||
|
|
||||||
const loadRows = useCallback(async () => {
|
useTreeViewDragAndDrop(apiRef, {
|
||||||
|
onItemDrop: async (params) => {
|
||||||
|
const source = nodeMap[params.dragItemId];
|
||||||
|
const target = nodeMap[params.dropTargetId];
|
||||||
|
if (source && target && !source.isFolder && target.isFolder) {
|
||||||
|
const newPath = `${target.path}/${source.fileName}`;
|
||||||
|
try {
|
||||||
|
await fetch("/api/storage/move_workflow", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ path: source.path, new_path: newPath })
|
||||||
|
});
|
||||||
|
loadTree();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to move workflow:", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadTree = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const resp = await fetch("/api/storage/load_workflows");
|
const resp = await fetch("/api/storage/load_workflows");
|
||||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||||
const data = await resp.json();
|
const data = await resp.json();
|
||||||
const mapped = (data.workflows || []).map((w) => ({
|
const { root, map } = buildTree(data.workflows || []);
|
||||||
...w,
|
setTree(root);
|
||||||
name: w.tab_name && w.tab_name.trim() ? w.tab_name.trim() : w.file_name,
|
setNodeMap(map);
|
||||||
description: "",
|
|
||||||
category: "",
|
|
||||||
lastEdited: w.last_edited,
|
|
||||||
lastEditedEpoch: w.last_edited_epoch
|
|
||||||
}));
|
|
||||||
setRows(mapped);
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to load workflows:", err);
|
console.error("Failed to load workflows:", err);
|
||||||
setRows([]);
|
setTree([]);
|
||||||
|
setNodeMap({});
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadRows();
|
loadTree();
|
||||||
}, [loadRows]);
|
}, [loadTree]);
|
||||||
|
|
||||||
const handleSort = (col) => {
|
const openMenu = (e, node) => {
|
||||||
if (orderBy === col) setOrder(order === "asc" ? "desc" : "asc");
|
|
||||||
else {
|
|
||||||
setOrderBy(col);
|
|
||||||
setOrder("asc");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const sorted = useMemo(() => {
|
|
||||||
const dir = order === "asc" ? 1 : -1;
|
|
||||||
return [...rows].sort((a, b) => {
|
|
||||||
if (orderBy === "lastEdited" || orderBy === "lastEditedEpoch") {
|
|
||||||
const A = Number(a.lastEditedEpoch || 0);
|
|
||||||
const B = Number(b.lastEditedEpoch || 0);
|
|
||||||
return (A - B) * dir;
|
|
||||||
}
|
|
||||||
const A = a[orderBy] || "";
|
|
||||||
const B = b[orderBy] || "";
|
|
||||||
return String(A).localeCompare(String(B)) * dir;
|
|
||||||
});
|
|
||||||
}, [rows, orderBy, order]);
|
|
||||||
|
|
||||||
const handleNewWorkflow = () => {
|
|
||||||
if (onOpenWorkflow) {
|
|
||||||
onOpenWorkflow();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRowClick = (workflow) => {
|
|
||||||
if (onOpenWorkflow) {
|
|
||||||
onOpenWorkflow(workflow);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const openMenu = (e, row) => {
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setMenuAnchor(e.currentTarget);
|
setMenuAnchor(e.currentTarget);
|
||||||
setSelected(row);
|
setSelectedNode(node);
|
||||||
};
|
};
|
||||||
|
|
||||||
const closeMenu = () => setMenuAnchor(null);
|
const closeMenu = () => setMenuAnchor(null);
|
||||||
|
|
||||||
const startRename = () => {
|
const handleRename = () => {
|
||||||
closeMenu();
|
closeMenu();
|
||||||
if (selected) {
|
if (!selectedNode) return;
|
||||||
const initial = selected.tab_name && selected.tab_name.trim().length > 0
|
setRenameValue(selectedNode.label);
|
||||||
? selected.tab_name.trim()
|
if (selectedNode.isFolder) setRenameFolderOpen(true);
|
||||||
: selected.file_name.replace(/\.json$/i, "");
|
else setRenameOpen(true);
|
||||||
setRenameValue(initial);
|
};
|
||||||
setRenameOpen(true);
|
|
||||||
|
const handleDeleteWorkflow = async () => {
|
||||||
|
closeMenu();
|
||||||
|
if (!selectedNode) return;
|
||||||
|
try {
|
||||||
|
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 handleRenameSave = async () => {
|
const handleCreateFolder = () => {
|
||||||
if (!selected) return;
|
closeMenu();
|
||||||
|
setRenameValue("");
|
||||||
|
setRenameFolderOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const saveRenameWorkflow = async () => {
|
||||||
|
if (!selectedNode) return;
|
||||||
try {
|
try {
|
||||||
await fetch("/api/storage/rename_workflow", {
|
await fetch("/api/storage/rename_workflow", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ path: selected.rel_path, new_name: renameValue })
|
body: JSON.stringify({ path: selectedNode.path, new_name: renameValue })
|
||||||
});
|
});
|
||||||
await loadRows();
|
loadTree();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to rename workflow:", err);
|
console.error("Failed to rename workflow:", err);
|
||||||
}
|
}
|
||||||
setRenameOpen(false);
|
setRenameOpen(false);
|
||||||
setSelected(null);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderNameCell = (r) => {
|
const saveRenameFolder = async () => {
|
||||||
const hasPrefix = r.breadcrumb_prefix && r.breadcrumb_prefix.length > 0;
|
try {
|
||||||
const primary = r.tab_name && r.tab_name.trim().length > 0 ? r.tab_name.trim() : r.file_name;
|
if (selectedNode && selectedNode.isFolder) {
|
||||||
return (
|
await fetch("/api/storage/rename_folder", {
|
||||||
<Box component="span">
|
method: "POST",
|
||||||
{hasPrefix && (
|
headers: { "Content-Type": "application/json" },
|
||||||
<Typography
|
body: JSON.stringify({ path: selectedNode.path, new_name: renameValue })
|
||||||
component="span"
|
});
|
||||||
sx={{ color: "#6b6b6b", mr: 0.5 }}
|
} else {
|
||||||
>
|
const basePath = selectedNode ? selectedNode.path : "";
|
||||||
{r.breadcrumb_prefix} {">"}{" "}
|
const newPath = basePath ? `${basePath}/${renameValue}` : renameValue;
|
||||||
</Typography>
|
await fetch("/api/storage/create_folder", {
|
||||||
)}
|
method: "POST",
|
||||||
<Typography component="span" sx={{ color: "#e6edf3" }}>
|
headers: { "Content-Type": "application/json" },
|
||||||
{primary}
|
body: JSON.stringify({ path: newPath })
|
||||||
</Typography>
|
});
|
||||||
</Box>
|
}
|
||||||
);
|
loadTree();
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Folder operation failed:", err);
|
||||||
|
}
|
||||||
|
setRenameFolderOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleNodeSelect = (event, itemId) => {
|
||||||
|
const node = nodeMap[itemId];
|
||||||
|
if (node && !node.isFolder && onOpenWorkflow) {
|
||||||
|
onOpenWorkflow(node.workflow);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderItems = (nodes) =>
|
||||||
|
nodes.map((n) => (
|
||||||
|
<TreeItem
|
||||||
|
key={n.id}
|
||||||
|
itemId={n.id}
|
||||||
|
label={
|
||||||
|
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||||
|
{n.isFolder ? (
|
||||||
|
<FolderIcon sx={{ mr: 1, color: "#ffd54f" }} />
|
||||||
|
) : (
|
||||||
|
<DescriptionIcon sx={{ mr: 1, color: "#90caf9" }} />
|
||||||
|
)}
|
||||||
|
<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>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{n.children && n.children.length > 0 ? renderItems(n.children) : null}
|
||||||
|
</TreeItem>
|
||||||
|
));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper sx={{ m: 2, p: 0, bgcolor: "#1e1e1e" }} elevation={2}>
|
<Paper sx={{ m: 2, p: 0, bgcolor: "#1e1e1e" }} elevation={2}>
|
||||||
<Box sx={{ p: 2, pb: 1, display: "flex", justifyContent: "space-between", alignItems: "center" }}>
|
<Box
|
||||||
|
sx={{
|
||||||
|
p: 2,
|
||||||
|
pb: 1,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Box>
|
<Box>
|
||||||
<Typography variant="h6" sx={{ color: "#58a6ff", mb: 0 }}>
|
<Typography variant="h6" sx={{ color: "#58a6ff", mb: 0 }}>
|
||||||
Workflows
|
Workflows
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography variant="body2" sx={{ color: "#aaa" }}>
|
<Typography variant="body2" sx={{ color: "#aaa" }}>
|
||||||
List of available workflows.
|
Manage workflow folders and files.
|
||||||
</Typography>
|
</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
<Button
|
<Box>
|
||||||
startIcon={<PlayCircleIcon />}
|
<Button
|
||||||
sx={{
|
startIcon={<CreateNewFolderIcon />}
|
||||||
color: "#58a6ff",
|
sx={{
|
||||||
borderColor: "#58a6ff",
|
mr: 1,
|
||||||
textTransform: "none",
|
color: "#58a6ff",
|
||||||
border: "1px solid #58a6ff",
|
borderColor: "#58a6ff",
|
||||||
backgroundColor: "#1e1e1e",
|
textTransform: "none",
|
||||||
"&:hover": { backgroundColor: "#1b1b1b" }
|
border: "1px solid #58a6ff",
|
||||||
}}
|
backgroundColor: "#1e1e1e",
|
||||||
onClick={handleNewWorkflow}
|
"&:hover": { backgroundColor: "#1b1b1b" }
|
||||||
>
|
}}
|
||||||
New Workflow
|
onClick={() => {
|
||||||
</Button>
|
setSelectedNode(null);
|
||||||
|
setRenameValue("");
|
||||||
|
setRenameFolderOpen(true);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
New Folder
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
startIcon={<PlayCircleIcon />}
|
||||||
|
sx={{
|
||||||
|
color: "#58a6ff",
|
||||||
|
borderColor: "#58a6ff",
|
||||||
|
textTransform: "none",
|
||||||
|
border: "1px solid #58a6ff",
|
||||||
|
backgroundColor: "#1e1e1e",
|
||||||
|
"&:hover": { backgroundColor: "#1b1b1b" }
|
||||||
|
}}
|
||||||
|
onClick={() => onOpenWorkflow && onOpenWorkflow()}
|
||||||
|
>
|
||||||
|
New Workflow
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box sx={{ p: 2 }}>
|
||||||
|
<SimpleTreeView
|
||||||
|
sx={{ color: "#e6edf3" }}
|
||||||
|
onNodeSelect={handleNodeSelect}
|
||||||
|
apiRef={apiRef}
|
||||||
|
>
|
||||||
|
{renderItems(tree)}
|
||||||
|
</SimpleTreeView>
|
||||||
</Box>
|
</Box>
|
||||||
<Table size="small" sx={{ minWidth: 680 }}>
|
|
||||||
<TableHead>
|
|
||||||
<TableRow>
|
|
||||||
<TableCell sortDirection={orderBy === "name" ? order : false}>
|
|
||||||
<TableSortLabel
|
|
||||||
active={orderBy === "name"}
|
|
||||||
direction={orderBy === "name" ? order : "asc"}
|
|
||||||
onClick={() => handleSort("name")}
|
|
||||||
>
|
|
||||||
Name
|
|
||||||
</TableSortLabel>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sortDirection={orderBy === "description" ? order : false}>
|
|
||||||
<TableSortLabel
|
|
||||||
active={orderBy === "description"}
|
|
||||||
direction={orderBy === "description" ? order : "asc"}
|
|
||||||
onClick={() => handleSort("description")}
|
|
||||||
>
|
|
||||||
Description
|
|
||||||
</TableSortLabel>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sortDirection={orderBy === "category" ? order : false}>
|
|
||||||
<TableSortLabel
|
|
||||||
active={orderBy === "category"}
|
|
||||||
direction={orderBy === "category" ? order : "asc"}
|
|
||||||
onClick={() => handleSort("category")}
|
|
||||||
>
|
|
||||||
Category
|
|
||||||
</TableSortLabel>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell sortDirection={orderBy === "lastEdited" ? order : false}>
|
|
||||||
<TableSortLabel
|
|
||||||
active={orderBy === "lastEdited"}
|
|
||||||
direction={orderBy === "lastEdited" ? order : "asc"}
|
|
||||||
onClick={() => handleSort("lastEdited")}
|
|
||||||
>
|
|
||||||
Last Edited
|
|
||||||
</TableSortLabel>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell />
|
|
||||||
</TableRow>
|
|
||||||
</TableHead>
|
|
||||||
<TableBody>
|
|
||||||
{sorted.map((r, i) => (
|
|
||||||
<TableRow
|
|
||||||
key={i}
|
|
||||||
hover
|
|
||||||
sx={{ cursor: "pointer" }}
|
|
||||||
onClick={() => handleRowClick(r)}
|
|
||||||
>
|
|
||||||
<TableCell>{renderNameCell(r)}</TableCell>
|
|
||||||
<TableCell>{r.description}</TableCell>
|
|
||||||
<TableCell>{r.category}</TableCell>
|
|
||||||
<TableCell>{formatDateTime(r.lastEdited)}</TableCell>
|
|
||||||
<TableCell align="right" onClick={(e) => e.stopPropagation()}>
|
|
||||||
<IconButton
|
|
||||||
size="small"
|
|
||||||
onClick={(e) => openMenu(e, r)}
|
|
||||||
sx={{ color: "#ccc" }}
|
|
||||||
>
|
|
||||||
<MoreVertIcon fontSize="small" />
|
|
||||||
</IconButton>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
))}
|
|
||||||
{sorted.length === 0 && (
|
|
||||||
<TableRow>
|
|
||||||
<TableCell colSpan={5} sx={{ color: "#888" }}>
|
|
||||||
No workflows found.
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
)}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
<Menu
|
<Menu
|
||||||
anchorEl={menuAnchor}
|
anchorEl={menuAnchor}
|
||||||
open={Boolean(menuAnchor)}
|
open={Boolean(menuAnchor)}
|
||||||
onClose={closeMenu}
|
onClose={closeMenu}
|
||||||
PaperProps={{ sx: { bgcolor: "#1e1e1e", color: "#fff", fontSize: "13px" } }}
|
PaperProps={{ sx: { bgcolor: "#1e1e1e", color: "#fff", fontSize: "13px" } }}
|
||||||
>
|
>
|
||||||
<MenuItem onClick={startRename}>Rename</MenuItem>
|
{selectedNode?.isFolder && (
|
||||||
|
<MenuItem onClick={handleCreateFolder}>New Folder</MenuItem>
|
||||||
|
)}
|
||||||
|
<MenuItem onClick={handleRename}>Rename</MenuItem>
|
||||||
|
{!selectedNode?.isFolder && (
|
||||||
|
<MenuItem onClick={handleDeleteWorkflow}>Delete</MenuItem>
|
||||||
|
)}
|
||||||
</Menu>
|
</Menu>
|
||||||
<RenameWorkflowDialog
|
<RenameWorkflowDialog
|
||||||
open={renameOpen}
|
open={renameOpen}
|
||||||
value={renameValue}
|
value={renameValue}
|
||||||
onChange={setRenameValue}
|
onChange={setRenameValue}
|
||||||
onCancel={() => setRenameOpen(false)}
|
onCancel={() => setRenameOpen(false)}
|
||||||
onSave={handleRenameSave}
|
onSave={saveRenameWorkflow}
|
||||||
|
/>
|
||||||
|
<RenameFolderDialog
|
||||||
|
open={renameFolderOpen}
|
||||||
|
value={renameValue}
|
||||||
|
onChange={setRenameValue}
|
||||||
|
onCancel={() => setRenameFolderOpen(false)}
|
||||||
|
onSave={saveRenameFolder}
|
||||||
/>
|
/>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -12,6 +12,7 @@ from flask_cors import CORS
|
|||||||
import time
|
import time
|
||||||
import os # To Read Production ReactJS Server Folder
|
import os # To Read Production ReactJS Server Folder
|
||||||
import json # For reading workflow JSON files
|
import json # For reading workflow JSON files
|
||||||
|
import shutil # For moving workflow files and folders
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
|
|
||||||
# Borealis Python API Endpoints
|
# Borealis Python API Endpoints
|
||||||
@@ -85,6 +86,84 @@ def ocr_endpoint():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
return jsonify({"error": str(e)}), 500
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
# New storage management endpoints
|
||||||
|
|
||||||
|
@app.route("/api/storage/move_workflow", methods=["POST"])
|
||||||
|
def move_workflow():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
rel_path = (data.get("path") or "").strip()
|
||||||
|
new_rel = (data.get("new_path") or "").strip()
|
||||||
|
workflows_root = os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(__file__), "..", "..", "Workflows")
|
||||||
|
)
|
||||||
|
old_abs = os.path.abspath(os.path.join(workflows_root, rel_path))
|
||||||
|
new_abs = os.path.abspath(os.path.join(workflows_root, new_rel))
|
||||||
|
if not old_abs.startswith(workflows_root) or not os.path.isfile(old_abs):
|
||||||
|
return jsonify({"error": "Workflow not found"}), 404
|
||||||
|
if not new_abs.startswith(workflows_root):
|
||||||
|
return jsonify({"error": "Invalid destination"}), 400
|
||||||
|
os.makedirs(os.path.dirname(new_abs), exist_ok=True)
|
||||||
|
try:
|
||||||
|
shutil.move(old_abs, new_abs)
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/storage/delete_workflow", methods=["POST"])
|
||||||
|
def delete_workflow():
|
||||||
|
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.isfile(abs_path):
|
||||||
|
return jsonify({"error": "Workflow not found"}), 404
|
||||||
|
try:
|
||||||
|
os.remove(abs_path)
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/storage/create_folder", methods=["POST"])
|
||||||
|
def create_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):
|
||||||
|
return jsonify({"error": "Invalid path"}), 400
|
||||||
|
try:
|
||||||
|
os.makedirs(abs_path, exist_ok=True)
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/api/storage/rename_folder", methods=["POST"])
|
||||||
|
def rename_folder():
|
||||||
|
data = request.get_json(silent=True) or {}
|
||||||
|
rel_path = (data.get("path") or "").strip()
|
||||||
|
new_name = (data.get("new_name") or "").strip()
|
||||||
|
workflows_root = os.path.abspath(
|
||||||
|
os.path.join(os.path.dirname(__file__), "..", "..", "Workflows")
|
||||||
|
)
|
||||||
|
old_abs = os.path.abspath(os.path.join(workflows_root, rel_path))
|
||||||
|
if not old_abs.startswith(workflows_root) or not os.path.isdir(old_abs):
|
||||||
|
return jsonify({"error": "Folder not found"}), 404
|
||||||
|
if not new_name:
|
||||||
|
return jsonify({"error": "Invalid new_name"}), 400
|
||||||
|
new_abs = os.path.join(os.path.dirname(old_abs), new_name)
|
||||||
|
try:
|
||||||
|
os.rename(old_abs, new_abs)
|
||||||
|
return jsonify({"status": "ok"})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
# Borealis Storage API Endpoints
|
# Borealis Storage API Endpoints
|
||||||
# ---------------------------------------------
|
# ---------------------------------------------
|
||||||
|
Reference in New Issue
Block a user