UNPKG

testeranto

Version:

the AI powered BDD test framework for typescript projects

342 lines (341 loc) 13.4 kB
/* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable @typescript-eslint/no-explicit-any */ import React, { useState, useEffect } from "react"; import { DndProvider, useDrag, useDrop } from "react-dnd"; import { HTML5Backend } from "react-dnd-html5-backend"; import { GenericXMLEditorPage } from "../components/stateful/GenericXMLEditorPage"; import { AttributeEditor } from "../components/stateful/GenericXMLEditor/AttributeEditor"; // Function to parse XML string to XMLNode structure const parseXmlToNode = (xmlString) => { const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xmlString, "text/xml"); const parseElement = (element) => { var _a; // Get attributes const attributes = {}; for (let i = 0; i < element.attributes.length; i++) { const attr = element.attributes[i]; attributes[attr.name] = attr.value; } // Process children const children = []; let textContent; for (let i = 0; i < element.childNodes.length; i++) { const node = element.childNodes[i]; if (node.nodeType === Node.ELEMENT_NODE) { children.push(parseElement(node)); } else if (node.nodeType === Node.TEXT_NODE && ((_a = node.textContent) === null || _a === void 0 ? void 0 : _a.trim())) { textContent = (textContent || "") + node.textContent; } } // Trim text content if it exists if (textContent) { textContent = textContent.trim(); } return { id: `${element.nodeName}-${Date.now()}-${Math.random()}`, type: element.nodeName, attributes, children, textContent: textContent || undefined, }; }; // Get the root element const rootElement = xmlDoc.documentElement; return parseElement(rootElement); }; // Function to load XML file const loadXmlFile = async (filePath) => { try { const response = await fetch(`/api/files/read?path=${encodeURIComponent(filePath)}`); if (!response.ok) { throw new Error(`Failed to load XML file: ${response.statusText}`); } const xmlContent = await response.text(); return parseXmlToNode(xmlContent); } catch (error) { console.error("Error loading XML file:", error); throw error; } }; // Define node types based on the XSD const nodeTypes = [ { label: "Kanban Process", type: "kanban:KanbanProcess" }, { label: "Metadata", type: "core:Metadata" }, { label: "Version", type: "core:Version" }, { label: "Date Created", type: "core:DateCreated" }, { label: "Description", type: "core:Description" }, { label: "GraphML", type: "graphml:graphml" }, { label: "Key", type: "graphml:key" }, { label: "Graph", type: "graphml:graph" }, { label: "Node", type: "graphml:node" }, { label: "Data", type: "graphml:data" }, { label: "Edge", type: "graphml:edge" }, ]; // Function to create a new node const createKanbanNode = (parentId, nodeType) => { const id = `${nodeType}-${Date.now()}`; const baseNode = { id, type: nodeType, attributes: {}, children: [], }; // Add type-specific attributes switch (nodeType) { case "graphml:key": baseNode.attributes = { id: "new-key", for: "node", "attr.name": "", "attr.type": "string", }; break; case "graphml:node": baseNode.attributes = { id: `node-${Date.now()}` }; break; case "graphml:data": baseNode.attributes = { key: "name" }; break; case "graphml:edge": baseNode.attributes = { source: "", target: "" }; break; // Add more cases as needed } return baseNode; }; // Draggable Kanban Card Component const KanbanCard = ({ task, status, onMoveTask }) => { const [{ isDragging }, drag] = useDrag(() => ({ type: "task", item: { id: task.id, status }, collect: (monitor) => ({ isDragging: !!monitor.isDragging(), }), })); return (React.createElement("div", { ref: drag, className: "kanban-card", style: { background: "#fff", borderRadius: "4px", padding: "12px", boxShadow: "0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24)", opacity: isDragging ? 0.5 : 1, cursor: "move", } }, React.createElement("div", { style: { fontWeight: "bold", marginBottom: "8px" } }, task.name), React.createElement("div", { style: { fontSize: "0.875rem", color: "#666" } }, task.role))); }; // Kanban Column Component const KanbanColumn = ({ status, tasks, onMoveTask }) => { const [{ isOver }, drop] = useDrop(() => ({ accept: "task", drop: (item) => { if (item.status !== status) { onMoveTask(item.id, status); } }, collect: (monitor) => ({ isOver: !!monitor.isOver(), }), })); return (React.createElement("div", { ref: drop, className: "kanban-column", style: { minWidth: "250px", background: isOver ? "#e6f7ff" : "#f4f5f7", borderRadius: "8px", padding: "12px", display: "flex", flexDirection: "column", gap: "8px", transition: "background 0.2s ease", } }, React.createElement("h6", { style: { margin: "0 0 12px 0", padding: "8px", background: "#fff", borderRadius: "4px", } }, status, " (", tasks.length, ")"), React.createElement("div", { style: { flex: 1, display: "flex", flexDirection: "column", gap: "8px", } }, tasks.map((task) => (React.createElement(KanbanCard, { key: task.id, task: task, status: status, onMoveTask: onMoveTask })))))); }; // Kanban board preview component const KanbanBoardPreview = ({ xmlTree, onTreeUpdate }) => { const [tasks, setTasks] = useState([]); const findGraphmlNode = React.useCallback((node) => { if (node.type === "graphml:graphml") return node; for (const child of node.children) { const result = findGraphmlNode(child); if (result) return result; } return null; }, []); // Extract tasks from the graphml structure const extractTasks = React.useCallback((graphmlNode) => { if (!graphmlNode) return []; // Find the graph node const graphNode = graphmlNode.children.find((child) => child.type === "graphml:graph"); if (!graphNode) return []; // Process each node to extract task information const tasks = graphNode.children .filter((child) => child.type === "graphml:node") .map((node) => { let task = { id: node.attributes.id || "", name: "", status: "", role: "", }; // Extract data from child data nodes node.children.forEach((dataNode) => { if (dataNode.type === "graphml:data") { const key = dataNode.attributes.key; const value = dataNode.textContent || ""; switch (key) { case "name": task.name = value; break; case "status": task.status = value; break; case "role": task.role = value; break; } } }); return task; }); return tasks; }, []); // Update tasks whenever xmlTree changes React.useEffect(() => { const graphmlNode = findGraphmlNode(xmlTree); const extractedTasks = extractTasks(graphmlNode); setTasks(extractedTasks); }, [xmlTree, findGraphmlNode, extractTasks]); // Handle moving tasks between statuses const handleMoveTask = (taskId, newStatus) => { // Deep clone the xmlTree const newTree = JSON.parse(JSON.stringify(xmlTree)); // Find the task node and update its status data const updateStatusInTree = (node) => { if (node.type === "graphml:node" && node.attributes.id === taskId) { // Find the status data child and update its textContent for (const child of node.children) { if (child.type === "graphml:data" && child.attributes.key === "status") { child.textContent = newStatus; return true; } } // If status data doesn't exist, create it const statusDataNode = { id: `status-${Date.now()}`, type: "graphml:data", attributes: { key: "status" }, children: [], textContent: newStatus, }; node.children.push(statusDataNode); return true; } for (const child of node.children) { if (updateStatusInTree(child)) { return true; } } return false; }; if (updateStatusInTree(newTree)) { // Update the tree using the provided callback onTreeUpdate(newTree); // Update the local state to show the drag and drop working setTasks((prevTasks) => prevTasks.map((task) => task.id === taskId ? Object.assign(Object.assign({}, task), { status: newStatus }) : task)); } }; // Group tasks by status - use a more reliable approach const statusGroups = React.useMemo(() => { const groups = {}; tasks.forEach((task) => { if (!groups[task.status]) { groups[task.status] = []; } groups[task.status].push(task); }); return groups; }, [tasks]); // Define column order const statusOrder = ["To Do", "In Progress", "Done"]; return (React.createElement(DndProvider, { backend: HTML5Backend }, React.createElement("div", { className: "kanban-board", style: { display: "flex", gap: "16px", padding: "16px", height: "100%", overflowX: "auto", } }, statusOrder.map((status) => { const columnTasks = statusGroups[status] || []; return (React.createElement(KanbanColumn, { key: status, status: status, tasks: columnTasks, onMoveTask: handleMoveTask })); })))); }; // Custom preview renderer for the Kanban board const renderKanbanPreview = (node, isSelected, eventHandlers, onTreeUpdate) => { // Always render the Kanban board - the node parameter is always the root xmlTree return (React.createElement("div", Object.assign({ style: { height: "100%", width: "100%", } }, eventHandlers), React.createElement(KanbanBoardPreview, { xmlTree: node, onTreeUpdate: onTreeUpdate }))); }; export const FluaPage = () => { const [initialTree, setInitialTree] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const loadKanbanProcess = async () => { try { setLoading(true); const tree = await loadXmlFile("example/single-kanban-process.xml"); setInitialTree(tree); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load XML"); console.error("Error loading kanban process:", err); } finally { setLoading(false); } }; loadKanbanProcess(); }, []); if (loading) { return React.createElement("div", null, "Loading kanban process..."); } if (error) { throw error; } if (!initialTree) { return React.createElement("div", null, "No XML data available"); } return (React.createElement(DndProvider, { backend: HTML5Backend }, React.createElement(GenericXMLEditorPage, { initialTree: initialTree, renderPreview: renderKanbanPreview, attributeEditor: (node, onUpdateAttributes, onUpdateTextContent) => { console.log("attributeEditor called with:", { node, onUpdateAttributes, onUpdateTextContent, }); return (React.createElement(AttributeEditor, { node: node, onUpdateAttributes: onUpdateAttributes, onUpdateTextContent: onUpdateTextContent })); }, nodeTypes: nodeTypes, onAddNode: createKanbanNode }))); };