testeranto
Version:
the AI powered BDD test framework for typescript projects
342 lines (341 loc) • 13.4 kB
JavaScript
/* 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 })));
};