diff --git a/Data/Server/WebUI/src/Workflow_List.jsx b/Data/Server/WebUI/src/Workflow_List.jsx index 24073fd..06ca5a6 100644 --- a/Data/Server/WebUI/src/Workflow_List.jsx +++ b/Data/Server/WebUI/src/Workflow_List.jsx @@ -1,6 +1,4 @@ -////////// PROJECT FILE SEPARATION LINE ////////// CODE AFTER THIS LINE ARE FROM: /Data/WebUI/src/Workflow_List.jsx - -import React, { useState, useMemo } from "react"; +import React, { useState, useMemo, useEffect } from "react"; import { Paper, Box, @@ -15,11 +13,53 @@ import { } from "@mui/material"; import { PlayCircle as PlayCircleIcon } from "@mui/icons-material"; +function formatDateTime(dateString) { + if (!dateString) return ""; + const date = new Date(dateString); + if (isNaN(date)) return ""; + const day = String(date.getDate()).padStart(2, "0"); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const year = date.getFullYear(); + let hours = date.getHours(); + const minutes = String(date.getMinutes()).padStart(2, "0"); + const ampm = hours >= 12 ? "PM" : "AM"; + hours = hours % 12 || 12; + return `${day}/${month}/${year} ${hours}:${minutes}${ampm}`; +} + export default function WorkflowList({ onOpenWorkflow }) { const [rows, setRows] = useState([]); const [orderBy, setOrderBy] = useState("name"); const [order, setOrder] = useState("asc"); + useEffect(() => { + let alive = true; + (async () => { + try { + const resp = await fetch("/api/storage/load_workflows"); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + const data = await resp.json(); + if (!alive) return; + + const mapped = (data.workflows || []).map(w => ({ + ...w, + name: w.tab_name && w.tab_name.trim() ? w.tab_name.trim() : w.file_name, + description: "", + category: "", + lastEdited: w.last_edited, + lastEditedEpoch: w.last_edited_epoch + })); + setRows(mapped); + } catch (err) { + console.error("Failed to load workflows:", err); + setRows([]); + } + })(); + return () => { + alive = false; + }; + }, []); + const handleSort = (col) => { if (orderBy === col) setOrder(order === "asc" ? "desc" : "asc"); else { @@ -31,6 +71,11 @@ export default function WorkflowList({ onOpenWorkflow }) { 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; @@ -39,7 +84,7 @@ export default function WorkflowList({ onOpenWorkflow }) { const handleNewWorkflow = () => { if (onOpenWorkflow) { - onOpenWorkflow(); // trigger App.jsx to open editor + onOpenWorkflow(); } }; @@ -49,6 +94,26 @@ export default function WorkflowList({ onOpenWorkflow }) { } }; + const renderNameCell = (r) => { + const hasPrefix = r.breadcrumb_prefix && r.breadcrumb_prefix.length > 0; + const primary = r.tab_name && r.tab_name.trim().length > 0 ? r.tab_name.trim() : r.file_name; + return ( + + {hasPrefix && ( + + {r.breadcrumb_prefix} {">"}{" "} + + )} + + {primary} + + + ); + }; + return ( @@ -124,10 +189,10 @@ export default function WorkflowList({ onOpenWorkflow }) { sx={{ cursor: "pointer" }} onClick={() => handleRowClick(r)} > - {r.name} + {renderNameCell(r)} {r.description} {r.category} - {r.lastEdited} + {formatDateTime(r.lastEdited)} ))} {sorted.length === 0 && ( diff --git a/Workflows/Flyff Universe/flyff_character_status_workflow.json b/Data/Server/Workflows/Flyff Universe/flyff_character_status_workflow.json similarity index 100% rename from Workflows/Flyff Universe/flyff_character_status_workflow.json rename to Data/Server/Workflows/Flyff Universe/flyff_character_status_workflow.json diff --git a/Data/Server/server.py b/Data/Server/server.py index a0b4704..375de66 100644 --- a/Data/Server/server.py +++ b/Data/Server/server.py @@ -10,7 +10,9 @@ from flask_socketio import SocketIO, emit from flask_cors import CORS 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 +from typing import List, Dict # Borealis Python API Endpoints from Python_API_Endpoints.ocr_engines import run_ocr_on_base64 @@ -83,6 +85,113 @@ def ocr_endpoint(): except Exception as e: return jsonify({"error": str(e)}), 500 +# --------------------------------------------- +# Borealis Storage API Endpoints +# --------------------------------------------- +def _safe_read_json(path: str) -> Dict: + """ + Try to read JSON safely. Returns {} on failure. + """ + try: + with open(path, "r", encoding="utf-8") as fh: + return json.load(fh) + except Exception: + return {} + +def _extract_tab_name(obj: Dict) -> str: + """ + Best-effort extraction of a workflow tab name from a JSON object. + Falls back to empty string when unknown. + """ + if not isinstance(obj, dict): + return "" + for key in ["tabName", "tab_name", "name", "title"]: + val = obj.get(key) + if isinstance(val, str) and val.strip(): + return val.strip() + return "" + +@app.route("/api/storage/load_workflows", methods=["GET"]) +def load_workflows(): + """ + Scan /Workflows for *.json files and return a table-friendly list. + + Response: + { + "root": "", + "workflows": [ + { + "name": "FolderA > Sub > File.json", # breadcrumb styled name for table display + "breadcrumb_prefix": "FolderA > Sub", # prefix only, to allow UI styling + "file_name": "File.json", # base filename + "rel_path": "FolderA/Sub/File.json", # path relative to Workflows + "tab_name": "Optional Tab Name", # best-effort read from JSON (may be "") + "description": "", # placeholder for future use + "category": "", # placeholder for future use + "last_edited": "YYYY-MM-DDTHH:MM:SS", # local time ISO-like string + "last_edited_epoch": 1712345678.123 # numeric, for client-side sorting + }, + ... + ] + } + """ + # Resolve /Workflows relative to this file at /Data/server.py + workflows_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "Workflows")) + results: List[Dict] = [] + + if not os.path.isdir(workflows_root): + # Directory missing is not a hard error; return empty set and the resolved path for visibility. + return jsonify({ + "root": workflows_root, + "workflows": [], + "warning": "Workflows directory not found." + }), 200 + + for root, dirs, files in os.walk(workflows_root): + for fname in files: + if not fname.lower().endswith(".json"): + continue + + full_path = os.path.join(root, fname) + rel_path = os.path.relpath(full_path, workflows_root) # e.g. SuperStuff/Example.json + + # Build breadcrumb-style display name: "SuperStuff > Example.json" + parts = rel_path.split(os.sep) + folder_parts = parts[:-1] + breadcrumb_prefix = " > ".join(folder_parts) if folder_parts else "" + display_name = f"{breadcrumb_prefix} > {fname}" if breadcrumb_prefix else fname + + # Best-effort read of tab name (not required for now) + obj = _safe_read_json(full_path) + tab_name = _extract_tab_name(obj) + + # File timestamps + try: + mtime = os.path.getmtime(full_path) + except Exception: + mtime = 0.0 + last_edited_str = time.strftime("%Y-%m-%dT%H:%M:%S", time.localtime(mtime)) + + results.append({ + "name": display_name, + "breadcrumb_prefix": breadcrumb_prefix, + "file_name": fname, + "rel_path": rel_path.replace(os.sep, "/"), + "tab_name": tab_name, + "description": "", + "category": "", + "last_edited": last_edited_str, + "last_edited_epoch": mtime + }) + + # Sort newest-first by modification time + results.sort(key=lambda x: x.get("last_edited_epoch", 0.0), reverse=True) + + return jsonify({ + "root": workflows_root, + "workflows": results + }) + # --------------------------------------------- # Borealis Agent API Endpoints # ---------------------------------------------