Enhance Process Flow Components with Improved Node and Edge Management
- Introduced intelligent synchronization for nodes and edges in ProcessFlowCanvas, preventing unnecessary re-renders and improving performance. - Added state management flags to prevent recursive updates during node and edge changes. - Implemented explicit sync method for manual canvas updates, enhancing control over the rendering process. - Updated ProcessSettingsModal to change the label for JSON export to "Source" for better clarity. - Enhanced VariableManager with improved styling and default value handling, including validation for JSON objects. - Updated navigation to remove unused icons and improve overall UI consistency. - Added support for restoring deleted processes in the API, allowing for better data management and recovery options. - Enhanced process management with new filters and improved loading states in the manage process view.
This commit is contained in:
parent
44baddb6e3
commit
8805484de2
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, onMounted, computed, shallowRef, watch, defineExpose } from 'vue';
|
import { ref, onMounted, computed, shallowRef, watch, defineExpose, nextTick } from 'vue';
|
||||||
import { VueFlow, useVueFlow, Panel } from '@vue-flow/core';
|
import { VueFlow, useVueFlow, Panel } from '@vue-flow/core';
|
||||||
import { Background } from '@vue-flow/background';
|
import { Background } from '@vue-flow/background';
|
||||||
import { Controls } from '@vue-flow/controls';
|
import { Controls } from '@vue-flow/controls';
|
||||||
@ -94,6 +94,10 @@ const flowOptions = ref({
|
|||||||
// Use shallowRef for selected node to avoid unnecessary reactivity
|
// Use shallowRef for selected node to avoid unnecessary reactivity
|
||||||
const selectedNode = shallowRef(null);
|
const selectedNode = shallowRef(null);
|
||||||
|
|
||||||
|
// State management for preventing recursive updates
|
||||||
|
const isUpdatingNodes = ref(false);
|
||||||
|
const isUpdatingEdges = ref(false);
|
||||||
|
|
||||||
// Handle node selection
|
// Handle node selection
|
||||||
const onNodeClick = ({ node }) => {
|
const onNodeClick = ({ node }) => {
|
||||||
// Check if node exists and has required properties
|
// Check if node exists and has required properties
|
||||||
@ -178,20 +182,63 @@ onMounted(() => {
|
|||||||
|
|
||||||
// Watch for changes to initialNodes prop and update the canvas
|
// Watch for changes to initialNodes prop and update the canvas
|
||||||
watch(() => props.initialNodes, async (newNodes, oldNodes) => {
|
watch(() => props.initialNodes, async (newNodes, oldNodes) => {
|
||||||
if (newNodes && Array.isArray(newNodes)) {
|
if (newNodes && Array.isArray(newNodes) && !isUpdatingNodes.value) {
|
||||||
// console.log('ProcessFlowCanvas: Updating nodes, count:', newNodes.length);
|
isUpdatingNodes.value = true;
|
||||||
|
|
||||||
// Clear existing nodes and add new ones
|
try {
|
||||||
nodes.value = [];
|
// console.log('ProcessFlowCanvas: Updating nodes, count:', newNodes.length);
|
||||||
|
|
||||||
if (newNodes.length > 0) {
|
|
||||||
addNodes([...newNodes]); // Create a copy to avoid reactivity issues
|
|
||||||
|
|
||||||
// Fit view to show all nodes after both nodes and edges are processed
|
// Instead of clearing all nodes, sync them intelligently
|
||||||
await nextTick();
|
const currentNodeIds = new Set(nodes.value.map(n => n.id));
|
||||||
|
const newNodeIds = new Set(newNodes.map(n => n.id));
|
||||||
|
|
||||||
|
// Remove nodes that are no longer in the new list
|
||||||
|
const nodesToRemove = nodes.value.filter(node => !newNodeIds.has(node.id));
|
||||||
|
if (nodesToRemove.length > 0) {
|
||||||
|
removeNodes(nodesToRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new nodes that aren't already present
|
||||||
|
const nodesToAdd = newNodes.filter(node => !currentNodeIds.has(node.id));
|
||||||
|
if (nodesToAdd.length > 0) {
|
||||||
|
addNodes([...nodesToAdd]); // Create a copy to avoid reactivity issues
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing nodes that have changed
|
||||||
|
newNodes.forEach(newNode => {
|
||||||
|
const existingNode = nodes.value.find(n => n.id === newNode.id);
|
||||||
|
if (existingNode) {
|
||||||
|
// Check if the node has actually changed before updating
|
||||||
|
const hasChanges = (
|
||||||
|
existingNode.label !== newNode.label ||
|
||||||
|
JSON.stringify(existingNode.data) !== JSON.stringify(newNode.data) ||
|
||||||
|
Math.abs(existingNode.position.x - newNode.position.x) > 1 ||
|
||||||
|
Math.abs(existingNode.position.y - newNode.position.y) > 1
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
Object.assign(existingNode, {
|
||||||
|
label: newNode.label,
|
||||||
|
data: { ...newNode.data },
|
||||||
|
position: { ...newNode.position }
|
||||||
|
});
|
||||||
|
updateNodeInternals([newNode.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fit view only if we added new nodes and this is significant change
|
||||||
|
if (nodesToAdd.length > 0) {
|
||||||
|
await nextTick();
|
||||||
|
setTimeout(() => {
|
||||||
|
fitView();
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
// Use a small delay to prevent immediate re-triggering
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
fitView();
|
isUpdatingNodes.value = false;
|
||||||
}, 100);
|
}, 50);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
@ -199,79 +246,119 @@ watch(() => props.initialNodes, async (newNodes, oldNodes) => {
|
|||||||
// Watch for changes to initialEdges prop and update the canvas
|
// Watch for changes to initialEdges prop and update the canvas
|
||||||
// This watcher depends on nodes being already present
|
// This watcher depends on nodes being already present
|
||||||
watch(() => [props.initialEdges, nodes.value.length], async ([newEdges, nodeCount]) => {
|
watch(() => [props.initialEdges, nodes.value.length], async ([newEdges, nodeCount]) => {
|
||||||
if (newEdges && Array.isArray(newEdges) && nodeCount > 0) {
|
if (newEdges && Array.isArray(newEdges) && nodeCount > 0 && !isUpdatingEdges.value) {
|
||||||
// console.log('ProcessFlowCanvas: Updating edges, count:', newEdges.length, 'nodeCount:', nodeCount);
|
isUpdatingEdges.value = true;
|
||||||
|
|
||||||
// Clear existing edges
|
try {
|
||||||
edges.value = [];
|
// console.log('ProcessFlowCanvas: Updating edges, count:', newEdges.length, 'nodeCount:', nodeCount);
|
||||||
|
|
||||||
if (newEdges.length > 0) {
|
|
||||||
// Verify all nodes exist before adding edges
|
|
||||||
const validEdges = newEdges.filter(edge => {
|
|
||||||
const sourceExists = nodes.value.some(node => node.id === edge.source);
|
|
||||||
const targetExists = nodes.value.some(node => node.id === edge.target);
|
|
||||||
|
|
||||||
if (!sourceExists || !targetExists) {
|
|
||||||
console.warn(`Skipping edge ${edge.id}: source ${edge.source} exists: ${sourceExists}, target ${edge.target} exists: ${targetExists}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (validEdges.length > 0) {
|
// Instead of clearing all edges, sync them intelligently
|
||||||
// Ensure all edges have proper handle specifications
|
const currentEdgeIds = new Set(edges.value.map(e => e.id));
|
||||||
const edgesWithHandles = validEdges.map(edge => {
|
const newEdgeIds = new Set(newEdges.map(e => e.id));
|
||||||
// If edge already has sourceHandle and targetHandle, use them
|
|
||||||
if (edge.sourceHandle && edge.targetHandle) {
|
// Remove edges that are no longer in the new list
|
||||||
return edge;
|
const edgesToRemove = edges.value.filter(edge => !newEdgeIds.has(edge.id));
|
||||||
|
if (edgesToRemove.length > 0) {
|
||||||
|
removeEdges(edgesToRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newEdges.length > 0) {
|
||||||
|
// Verify all nodes exist before adding edges
|
||||||
|
const validEdges = newEdges.filter(edge => {
|
||||||
|
const sourceExists = nodes.value.some(node => node.id === edge.source);
|
||||||
|
const targetExists = nodes.value.some(node => node.id === edge.target);
|
||||||
|
|
||||||
|
if (!sourceExists || !targetExists) {
|
||||||
|
console.warn(`Skipping edge ${edge.id}: source ${edge.source} exists: ${sourceExists}, target ${edge.target} exists: ${targetExists}`);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Otherwise, generate default handles based on node types
|
return true;
|
||||||
const sourceNode = nodes.value.find(n => n.id === edge.source);
|
|
||||||
const targetNode = nodes.value.find(n => n.id === edge.target);
|
|
||||||
|
|
||||||
let sourceHandle = edge.sourceHandle;
|
|
||||||
let targetHandle = edge.targetHandle;
|
|
||||||
|
|
||||||
// Generate default source handle if missing
|
|
||||||
if (!sourceHandle && sourceNode) {
|
|
||||||
if (sourceNode.type === 'start') {
|
|
||||||
sourceHandle = `${edge.source}-bottom`; // Start nodes prefer bottom output
|
|
||||||
} else if (sourceNode.type === 'gateway') {
|
|
||||||
sourceHandle = `${edge.source}-right`; // Gateway nodes prefer right output for first connection
|
|
||||||
} else {
|
|
||||||
sourceHandle = `${edge.source}-bottom`; // Most nodes prefer bottom output
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate default target handle if missing
|
|
||||||
if (!targetHandle && targetNode) {
|
|
||||||
if (targetNode.type === 'end') {
|
|
||||||
targetHandle = `${edge.target}-top`; // End nodes prefer top input
|
|
||||||
} else {
|
|
||||||
targetHandle = `${edge.target}-top`; // Most nodes prefer top input
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...edge,
|
|
||||||
sourceHandle,
|
|
||||||
targetHandle
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
addEdges([...edgesWithHandles]); // Create a copy to avoid reactivity issues
|
// Add new edges that aren't already present
|
||||||
// console.log('ProcessFlowCanvas: Successfully added edges with handles:', edgesWithHandles.length);
|
const edgesToAdd = validEdges.filter(edge => !currentEdgeIds.has(edge.id));
|
||||||
|
|
||||||
|
if (edgesToAdd.length > 0) {
|
||||||
|
// Ensure all edges have proper handle specifications
|
||||||
|
const edgesWithHandles = edgesToAdd.map(edge => {
|
||||||
|
// If edge already has sourceHandle and targetHandle, use them
|
||||||
|
if (edge.sourceHandle && edge.targetHandle) {
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, generate default handles based on node types
|
||||||
|
const sourceNode = nodes.value.find(n => n.id === edge.source);
|
||||||
|
const targetNode = nodes.value.find(n => n.id === edge.target);
|
||||||
|
|
||||||
|
let sourceHandle = edge.sourceHandle;
|
||||||
|
let targetHandle = edge.targetHandle;
|
||||||
|
|
||||||
|
// Generate default source handle if missing
|
||||||
|
if (!sourceHandle && sourceNode) {
|
||||||
|
if (sourceNode.type === 'start') {
|
||||||
|
sourceHandle = `${edge.source}-bottom`; // Start nodes prefer bottom output
|
||||||
|
} else if (sourceNode.type === 'gateway') {
|
||||||
|
sourceHandle = `${edge.source}-right`; // Gateway nodes prefer right output for first connection
|
||||||
|
} else {
|
||||||
|
sourceHandle = `${edge.source}-bottom`; // Most nodes prefer bottom output
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate default target handle if missing
|
||||||
|
if (!targetHandle && targetNode) {
|
||||||
|
if (targetNode.type === 'end') {
|
||||||
|
targetHandle = `${edge.target}-top`; // End nodes prefer top input
|
||||||
|
} else {
|
||||||
|
targetHandle = `${edge.target}-top`; // Most nodes prefer top input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...edge,
|
||||||
|
sourceHandle,
|
||||||
|
targetHandle
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
addEdges([...edgesWithHandles]); // Create a copy to avoid reactivity issues
|
||||||
|
// console.log('ProcessFlowCanvas: Successfully added edges with handles:', edgesWithHandles.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing edges that have changed
|
||||||
|
newEdges.forEach(newEdge => {
|
||||||
|
const existingEdge = edges.value.find(e => e.id === newEdge.id);
|
||||||
|
if (existingEdge) {
|
||||||
|
// Check if the edge has actually changed before updating
|
||||||
|
const hasChanges = (
|
||||||
|
existingEdge.label !== newEdge.label ||
|
||||||
|
existingEdge.sourceHandle !== newEdge.sourceHandle ||
|
||||||
|
existingEdge.targetHandle !== newEdge.targetHandle ||
|
||||||
|
JSON.stringify(existingEdge.style) !== JSON.stringify(newEdge.style)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
Object.assign(existingEdge, {
|
||||||
|
label: newEdge.label,
|
||||||
|
sourceHandle: newEdge.sourceHandle,
|
||||||
|
targetHandle: newEdge.targetHandle,
|
||||||
|
style: newEdge.style ? { ...newEdge.style } : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
} finally {
|
||||||
|
// Use a small delay to prevent immediate re-triggering
|
||||||
|
setTimeout(() => {
|
||||||
|
isUpdatingEdges.value = false;
|
||||||
|
}, 50);
|
||||||
}
|
}
|
||||||
} else if (newEdges && Array.isArray(newEdges) && newEdges.length > 0 && nodeCount === 0) {
|
} else if (newEdges && Array.isArray(newEdges) && newEdges.length > 0 && nodeCount === 0) {
|
||||||
// console.log('ProcessFlowCanvas: Edges provided but no nodes yet, waiting...');
|
// console.log('ProcessFlowCanvas: Edges provided but no nodes yet, waiting...');
|
||||||
}
|
}
|
||||||
}, { deep: true });
|
}, { deep: true });
|
||||||
|
|
||||||
// Remove the deep watch as it's causing recursive updates
|
|
||||||
|
|
||||||
// Handle node changes
|
// Handle node changes
|
||||||
onNodesChange((changes) => {
|
onNodesChange((changes) => {
|
||||||
emit('nodesChange', changes, nodes.value);
|
emit('nodesChange', changes, nodes.value);
|
||||||
@ -339,8 +426,11 @@ const handleConnect = (connection) => {
|
|||||||
target: connection.target
|
target: connection.target
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add the edge directly to Vue Flow for immediate visual feedback
|
||||||
addEdges([newEdge]);
|
addEdges([newEdge]);
|
||||||
emit('edgesChange', edges.value);
|
|
||||||
|
// Emit the edge change in the format the parent expects
|
||||||
|
emit('edgesChange', [{ type: 'add', id: newEdge.id }], edges.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle node removal
|
// Handle node removal
|
||||||
@ -427,7 +517,8 @@ defineExpose({
|
|||||||
updateNode,
|
updateNode,
|
||||||
addNode,
|
addNode,
|
||||||
removeNode,
|
removeNode,
|
||||||
fitView
|
fitView,
|
||||||
|
syncCanvas
|
||||||
});
|
});
|
||||||
|
|
||||||
// Update an existing node
|
// Update an existing node
|
||||||
@ -468,6 +559,155 @@ function removeNode(nodeId) {
|
|||||||
removeNodes([nodeToRemove]);
|
removeNodes([nodeToRemove]);
|
||||||
return nodeToRemove;
|
return nodeToRemove;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add explicit sync method to manually update canvas
|
||||||
|
function syncCanvas(newNodes, newEdges) {
|
||||||
|
console.log('Explicit canvas sync called:', newNodes?.length, 'nodes,', newEdges?.length, 'edges');
|
||||||
|
|
||||||
|
// Force clear the updating flags first to ensure we can process
|
||||||
|
isUpdatingNodes.value = false;
|
||||||
|
isUpdatingEdges.value = false;
|
||||||
|
|
||||||
|
// Wait a moment for any ongoing operations to complete
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
console.log('Starting canvas sync operation...');
|
||||||
|
|
||||||
|
// Sync nodes first
|
||||||
|
if (newNodes && Array.isArray(newNodes)) {
|
||||||
|
const currentNodeIds = new Set(nodes.value.map(n => n.id));
|
||||||
|
const newNodeIds = new Set(newNodes.map(n => n.id));
|
||||||
|
|
||||||
|
console.log('Current nodes:', currentNodeIds.size, 'New nodes:', newNodeIds.size);
|
||||||
|
|
||||||
|
// Remove nodes that are no longer in the new list
|
||||||
|
const nodesToRemove = nodes.value.filter(node => !newNodeIds.has(node.id));
|
||||||
|
if (nodesToRemove.length > 0) {
|
||||||
|
console.log('Removing nodes:', nodesToRemove.map(n => n.id));
|
||||||
|
removeNodes(nodesToRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new nodes that aren't already present
|
||||||
|
const nodesToAdd = newNodes.filter(node => !currentNodeIds.has(node.id));
|
||||||
|
if (nodesToAdd.length > 0) {
|
||||||
|
console.log('Adding nodes:', nodesToAdd.map(n => n.id));
|
||||||
|
addNodes([...nodesToAdd]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing nodes
|
||||||
|
newNodes.forEach(newNode => {
|
||||||
|
const existingNode = nodes.value.find(n => n.id === newNode.id);
|
||||||
|
if (existingNode) {
|
||||||
|
Object.assign(existingNode, {
|
||||||
|
label: newNode.label,
|
||||||
|
data: { ...newNode.data },
|
||||||
|
position: { ...newNode.position }
|
||||||
|
});
|
||||||
|
updateNodeInternals([newNode.id]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Nodes processed, current count:', nodes.value.length);
|
||||||
|
|
||||||
|
// Sync edges after nodes are updated
|
||||||
|
if (newEdges && Array.isArray(newEdges) && nodes.value.length > 0) {
|
||||||
|
const currentEdgeIds = new Set(edges.value.map(e => e.id));
|
||||||
|
const newEdgeIds = new Set(newEdges.map(e => e.id));
|
||||||
|
|
||||||
|
console.log('Current edges:', currentEdgeIds.size, 'New edges:', newEdgeIds.size);
|
||||||
|
|
||||||
|
// Remove edges that are no longer in the new list
|
||||||
|
const edgesToRemove = edges.value.filter(edge => !newEdgeIds.has(edge.id));
|
||||||
|
if (edgesToRemove.length > 0) {
|
||||||
|
console.log('Removing edges:', edgesToRemove.map(e => e.id));
|
||||||
|
removeEdges(edgesToRemove);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new edges that aren't already present
|
||||||
|
const edgesToAdd = newEdges.filter(edge => !currentEdgeIds.has(edge.id));
|
||||||
|
if (edgesToAdd.length > 0) {
|
||||||
|
console.log('Processing new edges:', edgesToAdd.map(e => `${e.source}->${e.target}`));
|
||||||
|
|
||||||
|
// Verify nodes exist and add handles
|
||||||
|
const validEdges = edgesToAdd.filter(edge => {
|
||||||
|
const sourceExists = nodes.value.some(node => node.id === edge.source);
|
||||||
|
const targetExists = nodes.value.some(node => node.id === edge.target);
|
||||||
|
|
||||||
|
if (!sourceExists || !targetExists) {
|
||||||
|
console.warn(`Skipping edge ${edge.id}: source ${edge.source} exists: ${sourceExists}, target ${edge.target} exists: ${targetExists}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Valid edges to add:', validEdges.length);
|
||||||
|
|
||||||
|
const edgesWithHandles = validEdges.map(edge => {
|
||||||
|
// If edge already has sourceHandle and targetHandle, use them
|
||||||
|
if (edge.sourceHandle && edge.targetHandle) {
|
||||||
|
console.log(`Edge ${edge.id} already has handles:`, edge.sourceHandle, '->', edge.targetHandle);
|
||||||
|
return edge;
|
||||||
|
}
|
||||||
|
|
||||||
|
const sourceNode = nodes.value.find(n => n.id === edge.source);
|
||||||
|
const targetNode = nodes.value.find(n => n.id === edge.target);
|
||||||
|
|
||||||
|
let sourceHandle = edge.sourceHandle;
|
||||||
|
let targetHandle = edge.targetHandle;
|
||||||
|
|
||||||
|
if (!sourceHandle && sourceNode) {
|
||||||
|
if (sourceNode.type === 'start') {
|
||||||
|
sourceHandle = `${edge.source}-bottom`;
|
||||||
|
} else if (sourceNode.type === 'gateway') {
|
||||||
|
sourceHandle = `${edge.source}-right`;
|
||||||
|
} else {
|
||||||
|
sourceHandle = `${edge.source}-bottom`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!targetHandle && targetNode) {
|
||||||
|
if (targetNode.type === 'end') {
|
||||||
|
targetHandle = `${edge.target}-top`;
|
||||||
|
} else {
|
||||||
|
targetHandle = `${edge.target}-top`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`Generated handles for edge ${edge.id}:`, sourceHandle, '->', targetHandle);
|
||||||
|
return { ...edge, sourceHandle, targetHandle };
|
||||||
|
});
|
||||||
|
|
||||||
|
if (edgesWithHandles.length > 0) {
|
||||||
|
console.log('Adding edges with handles:', edgesWithHandles.length);
|
||||||
|
addEdges([...edgesWithHandles]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update existing edges
|
||||||
|
newEdges.forEach(newEdge => {
|
||||||
|
const existingEdge = edges.value.find(e => e.id === newEdge.id);
|
||||||
|
if (existingEdge) {
|
||||||
|
Object.assign(existingEdge, {
|
||||||
|
label: newEdge.label,
|
||||||
|
sourceHandle: newEdge.sourceHandle,
|
||||||
|
targetHandle: newEdge.targetHandle,
|
||||||
|
style: newEdge.style ? { ...newEdge.style } : undefined
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else if (newEdges && Array.isArray(newEdges) && newEdges.length > 0) {
|
||||||
|
console.warn('Cannot add edges: nodes not ready. Node count:', nodes.value.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Canvas sync completed - Final count: nodes:', nodes.value.length, 'edges:', edges.value.length);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error during canvas sync:', error);
|
||||||
|
}
|
||||||
|
}, 50); // Small delay to allow any pending operations to complete
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -331,7 +331,7 @@ const settingsTabs = [
|
|||||||
{ key: 'execution', label: 'Execution', icon: 'material-symbols:play-circle-outline' },
|
{ key: 'execution', label: 'Execution', icon: 'material-symbols:play-circle-outline' },
|
||||||
{ key: 'variables', label: 'Variables & Data', icon: 'material-symbols:data-object' },
|
{ key: 'variables', label: 'Variables & Data', icon: 'material-symbols:data-object' },
|
||||||
{ key: 'permissions', label: 'Permissions', icon: 'material-symbols:security' },
|
{ key: 'permissions', label: 'Permissions', icon: 'material-symbols:security' },
|
||||||
{ key: 'json', label: 'JSON Export', icon: 'material-symbols:code' }
|
{ key: 'json', label: 'Source', icon: 'material-symbols:code' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const activeTab = ref('info')
|
const activeTab = ref('info')
|
||||||
|
@ -75,51 +75,65 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Variable List -->
|
<!-- Variable List -->
|
||||||
<div v-else-if="filteredVariables.length" class="space-y-4">
|
<div v-else-if="filteredVariables.length" class="space-y-2">
|
||||||
<div
|
<div
|
||||||
v-for="variable in filteredVariables"
|
v-for="variable in filteredVariables"
|
||||||
:key="variable.name"
|
:key="variable.name"
|
||||||
class="variable-item group"
|
class="variable-item bg-white border border-gray-200 rounded-lg hover:border-blue-300 hover:shadow-sm transition-all duration-150 group"
|
||||||
>
|
>
|
||||||
<div
|
<!-- Variable Header -->
|
||||||
class="bg-white rounded-xl border border-gray-200 hover:border-blue-300 hover:shadow-md transition-all duration-300 overflow-hidden"
|
<div class="px-4 py-3">
|
||||||
>
|
<div class="flex items-center justify-between">
|
||||||
<!-- Header with Name and Actions -->
|
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||||
<div class="flex items-center justify-between p-5 pb-3">
|
|
||||||
<div class="flex items-center gap-3 flex-1 min-w-0">
|
|
||||||
<!-- Variable Icon -->
|
<!-- Variable Icon -->
|
||||||
<div class="flex-shrink-0 w-10 h-10 bg-blue-50 rounded-lg flex items-center justify-center">
|
<div class="flex-shrink-0">
|
||||||
<Icon
|
<div
|
||||||
:name="getVariableIcon(variable.type)"
|
class="w-8 h-8 rounded-lg flex items-center justify-center text-sm font-semibold"
|
||||||
class="w-5 h-5 text-blue-600"
|
:class="{
|
||||||
/>
|
'bg-blue-100 text-blue-700': variable.type === 'string',
|
||||||
|
'bg-purple-100 text-purple-700': ['int', 'decimal'].includes(variable.type),
|
||||||
|
'bg-indigo-100 text-indigo-700': variable.type === 'boolean',
|
||||||
|
'bg-amber-100 text-amber-700': ['date', 'datetime'].includes(variable.type),
|
||||||
|
'bg-emerald-100 text-emerald-700': variable.type === 'object',
|
||||||
|
'bg-gray-100 text-gray-700': !['string', 'int', 'decimal', 'boolean', 'date', 'datetime', 'object'].includes(variable.type)
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Icon :name="getVariableIcon(variable.type)" class="w-4 h-4" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Variable Name -->
|
<!-- Variable Info -->
|
||||||
<div class="flex-1 min-w-0">
|
<div class="min-w-0 flex-1">
|
||||||
<h4 class="text-lg font-semibold text-gray-900 truncate">
|
<div class="flex items-center gap-2 mb-1">
|
||||||
{{ variable.name }}
|
<h4 class="text-sm font-medium text-gray-900 truncate">{{ variable.name }}</h4>
|
||||||
</h4>
|
<RsBadge
|
||||||
<div class="flex items-center gap-2 mt-1">
|
:variant="getTypeColor(variable.type)"
|
||||||
<RsBadge :variant="getTypeColor(variable.type)" size="sm" class="font-medium">
|
size="sm"
|
||||||
|
class="flex-shrink-0"
|
||||||
|
>
|
||||||
{{ variable.type }}
|
{{ variable.type }}
|
||||||
</RsBadge>
|
</RsBadge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<p v-if="variable.description" class="text-xs text-gray-500 line-clamp-1">
|
||||||
|
{{ variable.description }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Actions -->
|
||||||
<div class="flex items-center gap-1 opacity-60 group-hover:opacity-100 transition-opacity">
|
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<button
|
<button
|
||||||
@click="editVariable(variable)"
|
@click="editVariable(variable)"
|
||||||
class="p-2.5 text-gray-500 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-colors"
|
class="p-1.5 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors"
|
||||||
title="Edit variable"
|
title="Edit variable"
|
||||||
>
|
>
|
||||||
<Icon name="material-symbols:edit" class="w-4 h-4" />
|
<Icon name="material-symbols:edit" class="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
@click="deleteVariable(variable)"
|
@click="deleteVariable(variable)"
|
||||||
class="p-2.5 text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors"
|
class="p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded transition-colors"
|
||||||
title="Delete variable"
|
title="Delete variable"
|
||||||
>
|
>
|
||||||
<Icon name="material-symbols:delete" class="w-4 h-4" />
|
<Icon name="material-symbols:delete" class="w-4 h-4" />
|
||||||
@ -127,34 +141,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Description -->
|
<!-- Default Value Display -->
|
||||||
<div v-if="variable.description" class="px-5 pb-3">
|
<div v-if="variable.defaultValue !== undefined && variable.defaultValue !== ''" class="mt-3">
|
||||||
<p class="text-sm text-gray-600 leading-relaxed">
|
<div class="bg-amber-50 rounded-md p-2 border border-amber-100">
|
||||||
{{ variable.description }}
|
<div class="flex items-center gap-1.5 mb-1">
|
||||||
</p>
|
<Icon name="material-symbols:settings" class="w-3.5 h-3.5 text-amber-600" />
|
||||||
</div>
|
<span class="text-xs font-medium text-amber-700 uppercase tracking-wide">Default Value</span>
|
||||||
|
|
||||||
<!-- Current Value (if set) -->
|
|
||||||
<div v-if="variable.value !== undefined && variable.value !== ''" class="px-5 pb-4">
|
|
||||||
<div class="bg-gray-50 rounded-lg p-3 border border-gray-100">
|
|
||||||
<div class="flex items-center gap-2 mb-2">
|
|
||||||
<Icon name="material-symbols:code" class="w-4 h-4 text-gray-500" />
|
|
||||||
<span class="text-xs font-medium text-gray-500 uppercase tracking-wide">Current Value</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="font-mono text-sm text-gray-800 break-all">
|
<div class="font-mono text-xs text-amber-800 break-all">
|
||||||
{{ formatValue(variable.value, variable.type) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Default Value (if no current value but has default) -->
|
|
||||||
<div v-else-if="variable.defaultValue !== undefined && variable.defaultValue !== ''" class="px-5 pb-4">
|
|
||||||
<div class="bg-amber-50 rounded-lg p-3 border border-amber-100">
|
|
||||||
<div class="flex items-center gap-2 mb-2">
|
|
||||||
<Icon name="material-symbols:settings" class="w-4 h-4 text-amber-600" />
|
|
||||||
<span class="text-xs font-medium text-amber-600 uppercase tracking-wide">Default Value</span>
|
|
||||||
</div>
|
|
||||||
<div class="font-mono text-sm text-amber-800 break-all">
|
|
||||||
{{ formatValue(variable.defaultValue, variable.type) }}
|
{{ formatValue(variable.defaultValue, variable.type) }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -231,6 +225,19 @@
|
|||||||
help="Select the type of data this variable will store"
|
help="Select the type of data this variable will store"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<FormKit
|
||||||
|
name="defaultValue"
|
||||||
|
v-model="variableForm.defaultValue"
|
||||||
|
:type="getInputTypeForVariableType(variableForm.type)"
|
||||||
|
:label="`Default Value${variableForm.type === 'boolean' ? '' : ' (Optional)'}`"
|
||||||
|
:placeholder="getPlaceholderForType(variableForm.type)"
|
||||||
|
:options="variableForm.type === 'boolean' ? [
|
||||||
|
{ label: 'True', value: true },
|
||||||
|
{ label: 'False', value: false }
|
||||||
|
] : undefined"
|
||||||
|
:help="getHelpTextForType(variableForm.type)"
|
||||||
|
/>
|
||||||
|
|
||||||
<FormKit
|
<FormKit
|
||||||
name="description"
|
name="description"
|
||||||
v-model="variableForm.description"
|
v-model="variableForm.description"
|
||||||
@ -269,6 +276,7 @@ const variableForm = ref({
|
|||||||
type: "string",
|
type: "string",
|
||||||
scope: "global",
|
scope: "global",
|
||||||
description: "",
|
description: "",
|
||||||
|
defaultValue: ""
|
||||||
});
|
});
|
||||||
|
|
||||||
// Variable type options with descriptions
|
// Variable type options with descriptions
|
||||||
@ -282,6 +290,67 @@ const variableTypes = [
|
|||||||
{ label: 'Boolean - True/False values', value: 'boolean' }
|
{ label: 'Boolean - True/False values', value: 'boolean' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Helper functions for default value input
|
||||||
|
const getInputTypeForVariableType = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'int':
|
||||||
|
case 'decimal':
|
||||||
|
return 'number';
|
||||||
|
case 'boolean':
|
||||||
|
return 'select';
|
||||||
|
case 'date':
|
||||||
|
return 'date';
|
||||||
|
case 'datetime':
|
||||||
|
return 'datetime-local';
|
||||||
|
case 'object':
|
||||||
|
return 'textarea';
|
||||||
|
default:
|
||||||
|
return 'text';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPlaceholderForType = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
return 'Enter default text value';
|
||||||
|
case 'int':
|
||||||
|
return 'Enter default number (e.g. 0, 100)';
|
||||||
|
case 'decimal':
|
||||||
|
return 'Enter default decimal (e.g. 0.0, 99.99)';
|
||||||
|
case 'object':
|
||||||
|
return 'Enter default JSON object (e.g. {"key": "value"})';
|
||||||
|
case 'date':
|
||||||
|
return 'Select default date';
|
||||||
|
case 'datetime':
|
||||||
|
return 'Select default date and time';
|
||||||
|
case 'boolean':
|
||||||
|
return 'Select default boolean value';
|
||||||
|
default:
|
||||||
|
return 'Enter default value';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getHelpTextForType = (type) => {
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
return 'Default text that will be used when the variable is first created';
|
||||||
|
case 'int':
|
||||||
|
return 'Default whole number (no decimals)';
|
||||||
|
case 'decimal':
|
||||||
|
return 'Default decimal number (with decimal places)';
|
||||||
|
case 'object':
|
||||||
|
return 'Default JSON object - must be valid JSON format';
|
||||||
|
case 'date':
|
||||||
|
return 'Default date value for this variable';
|
||||||
|
case 'datetime':
|
||||||
|
return 'Default date and time value for this variable';
|
||||||
|
case 'boolean':
|
||||||
|
return 'Default true/false value for this variable';
|
||||||
|
default:
|
||||||
|
return 'Default value that will be used when the variable is first created';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Computed
|
// Computed
|
||||||
const variables = computed(() => {
|
const variables = computed(() => {
|
||||||
// Return all variables (treating everything as global)
|
// Return all variables (treating everything as global)
|
||||||
@ -304,7 +373,10 @@ const filteredVariables = computed(() => {
|
|||||||
// Methods
|
// Methods
|
||||||
const editVariable = (variable) => {
|
const editVariable = (variable) => {
|
||||||
editingVariable.value = variable;
|
editingVariable.value = variable;
|
||||||
variableForm.value = { ...variable };
|
variableForm.value = {
|
||||||
|
...variable,
|
||||||
|
defaultValue: variable.defaultValue || ""
|
||||||
|
};
|
||||||
showAddVariable.value = true;
|
showAddVariable.value = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -319,7 +391,8 @@ const resetForm = () => {
|
|||||||
name: "",
|
name: "",
|
||||||
type: "string",
|
type: "string",
|
||||||
scope: "global",
|
scope: "global",
|
||||||
description: ""
|
description: "",
|
||||||
|
defaultValue: ""
|
||||||
};
|
};
|
||||||
editingVariable.value = null;
|
editingVariable.value = null;
|
||||||
};
|
};
|
||||||
@ -331,12 +404,29 @@ const closeModal = () => {
|
|||||||
|
|
||||||
const saveVariable = async (formData) => {
|
const saveVariable = async (formData) => {
|
||||||
try {
|
try {
|
||||||
|
// Process default value based on type
|
||||||
|
let processedDefaultValue = formData.defaultValue;
|
||||||
|
|
||||||
|
if (formData.type === 'int' && processedDefaultValue !== '') {
|
||||||
|
processedDefaultValue = parseInt(processedDefaultValue);
|
||||||
|
} else if (formData.type === 'decimal' && processedDefaultValue !== '') {
|
||||||
|
processedDefaultValue = parseFloat(processedDefaultValue);
|
||||||
|
} else if (formData.type === 'object' && processedDefaultValue !== '') {
|
||||||
|
try {
|
||||||
|
processedDefaultValue = JSON.parse(processedDefaultValue);
|
||||||
|
} catch (e) {
|
||||||
|
alert('Invalid JSON format for object type. Please enter valid JSON.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create a new variable object
|
// Create a new variable object
|
||||||
const newVariable = {
|
const newVariable = {
|
||||||
name: formData.name,
|
name: formData.name,
|
||||||
type: formData.type,
|
type: formData.type,
|
||||||
scope: "global",
|
scope: "global",
|
||||||
description: formData.description
|
description: formData.description,
|
||||||
|
defaultValue: processedDefaultValue
|
||||||
};
|
};
|
||||||
|
|
||||||
if (editingVariable.value) {
|
if (editingVariable.value) {
|
||||||
@ -355,7 +445,7 @@ const saveVariable = async (formData) => {
|
|||||||
closeModal();
|
closeModal();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving variable:", error);
|
console.error("Error saving variable:", error);
|
||||||
// You might want to show an error message to the user here
|
alert('Error saving variable. Please check your input and try again.');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -426,6 +516,13 @@ const getVariableIcon = (type) => {
|
|||||||
@apply transform -translate-y-0.5;
|
@apply transform -translate-y-0.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.line-clamp-1 {
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 1;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
/* Light styling for FormKit form */
|
/* Light styling for FormKit form */
|
||||||
:deep(.formkit-outer) {
|
:deep(.formkit-outer) {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
|
@ -23,7 +23,6 @@ export default [
|
|||||||
{
|
{
|
||||||
title: "Visual Builder",
|
title: "Visual Builder",
|
||||||
path: "/process-builder",
|
path: "/process-builder",
|
||||||
icon: "material-symbols:schema",
|
|
||||||
child: [],
|
child: [],
|
||||||
meta: {
|
meta: {
|
||||||
description: "Design workflows with drag-and-drop interface"
|
description: "Design workflows with drag-and-drop interface"
|
||||||
@ -32,7 +31,6 @@ export default [
|
|||||||
{
|
{
|
||||||
title: "Manage Processes",
|
title: "Manage Processes",
|
||||||
path: "/process-builder/manage",
|
path: "/process-builder/manage",
|
||||||
icon: "material-symbols:folder-open",
|
|
||||||
child: [],
|
child: [],
|
||||||
meta: {
|
meta: {
|
||||||
description: "View and manage all your processes"
|
description: "View and manage all your processes"
|
||||||
@ -47,7 +45,6 @@ export default [
|
|||||||
{
|
{
|
||||||
title: "Form Builder",
|
title: "Form Builder",
|
||||||
path: "/form-builder",
|
path: "/form-builder",
|
||||||
icon: "material-symbols:edit-document",
|
|
||||||
child: [],
|
child: [],
|
||||||
meta: {
|
meta: {
|
||||||
description: "Create dynamic forms with advanced components"
|
description: "Create dynamic forms with advanced components"
|
||||||
@ -56,7 +53,6 @@ export default [
|
|||||||
{
|
{
|
||||||
title: "Manage Forms",
|
title: "Manage Forms",
|
||||||
path: "/form-builder/manage",
|
path: "/form-builder/manage",
|
||||||
icon: "material-symbols:library-books",
|
|
||||||
child: [],
|
child: [],
|
||||||
meta: {
|
meta: {
|
||||||
description: "View and manage all your forms"
|
description: "View and manage all your forms"
|
||||||
@ -77,7 +73,6 @@ export default [
|
|||||||
{
|
{
|
||||||
title: "Execution Dashboard",
|
title: "Execution Dashboard",
|
||||||
path: "/execution",
|
path: "/execution",
|
||||||
icon: "material-symbols:monitoring",
|
|
||||||
child: [],
|
child: [],
|
||||||
meta: {
|
meta: {
|
||||||
description: "Monitor active processes and performance metrics"
|
description: "Monitor active processes and performance metrics"
|
||||||
@ -86,7 +81,6 @@ export default [
|
|||||||
{
|
{
|
||||||
title: "Start New Process",
|
title: "Start New Process",
|
||||||
path: "/execution/new-case",
|
path: "/execution/new-case",
|
||||||
icon: "material-symbols:rocket-launch",
|
|
||||||
child: [],
|
child: [],
|
||||||
meta: {
|
meta: {
|
||||||
description: "Initiate a new process instance"
|
description: "Initiate a new process instance"
|
||||||
@ -101,7 +95,6 @@ export default [
|
|||||||
{
|
{
|
||||||
title: "My Tasks",
|
title: "My Tasks",
|
||||||
path: "/execution/inbox",
|
path: "/execution/inbox",
|
||||||
icon: "material-symbols:inbox",
|
|
||||||
child: [],
|
child: [],
|
||||||
meta: {
|
meta: {
|
||||||
description: "View and complete assigned tasks"
|
description: "View and complete assigned tasks"
|
||||||
@ -110,7 +103,6 @@ export default [
|
|||||||
{
|
{
|
||||||
title: "Process History",
|
title: "Process History",
|
||||||
path: "/execution/history",
|
path: "/execution/history",
|
||||||
icon: "material-symbols:history",
|
|
||||||
child: [],
|
child: [],
|
||||||
meta: {
|
meta: {
|
||||||
description: "Review completed processes and audit trails"
|
description: "Review completed processes and audit trails"
|
||||||
|
@ -921,7 +921,7 @@ const settingsTabs = [
|
|||||||
{ key: 'javascript', label: 'JavaScript', icon: 'material-symbols:code' },
|
{ key: 'javascript', label: 'JavaScript', icon: 'material-symbols:code' },
|
||||||
{ key: 'css', label: 'CSS', icon: 'material-symbols:palette-outline' },
|
{ key: 'css', label: 'CSS', icon: 'material-symbols:palette-outline' },
|
||||||
{ key: 'events', label: 'Events', icon: 'material-symbols:event-outline' },
|
{ key: 'events', label: 'Events', icon: 'material-symbols:event-outline' },
|
||||||
{ key: 'json', label: 'JSON', icon: 'material-symbols:data-object' }
|
{ key: 'json', label: 'Source', icon: 'material-symbols:data-object' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Computed property for form name with getter and setter
|
// Computed property for form name with getter and setter
|
||||||
|
@ -33,22 +33,36 @@
|
|||||||
<div class="flex-1 p-6 overflow-auto">
|
<div class="flex-1 p-6 overflow-auto">
|
||||||
<!-- Filters and Search -->
|
<!-- Filters and Search -->
|
||||||
<div class="mb-6 flex flex-col sm:flex-row gap-4">
|
<div class="mb-6 flex flex-col sm:flex-row gap-4">
|
||||||
<div class="relative flex-1 max-w-md">
|
<div class="flex-1 max-w-md">
|
||||||
<input
|
<FormKit
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search forms..."
|
placeholder="Search forms..."
|
||||||
class="w-full px-4 py-2 pl-10 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
:classes="{
|
||||||
/>
|
outer: 'mb-0',
|
||||||
<span class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400">
|
wrapper: 'relative',
|
||||||
<Icon name="material-symbols:search" class="text-lg" />
|
inner: 'relative',
|
||||||
</span>
|
input: 'w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500',
|
||||||
|
prefixIcon: 'absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 pointer-events-none'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #prefixIcon>
|
||||||
|
<Icon name="material-symbols:search" class="text-lg" />
|
||||||
|
</template>
|
||||||
|
</FormKit>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RsButton @click="loadForms" variant="tertiary" size="sm" :disabled="loading">
|
<div class="flex gap-2">
|
||||||
<Icon name="material-symbols:refresh" class="mr-1" />
|
<RsButton @click="clearFilters" variant="secondary" size="sm" :disabled="loading" v-if="searchQuery">
|
||||||
Refresh
|
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
||||||
</RsButton>
|
Clear Filters
|
||||||
|
</RsButton>
|
||||||
|
|
||||||
|
<RsButton @click="loadForms" variant="tertiary" size="sm" :disabled="loading">
|
||||||
|
<Icon name="material-symbols:refresh" class="mr-1" />
|
||||||
|
Refresh
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
@ -77,14 +91,28 @@
|
|||||||
<td colspan="6" class="px-6 py-12 text-center text-gray-500">
|
<td colspan="6" class="px-6 py-12 text-center text-gray-500">
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<Icon name="material-symbols:description-outline" class="w-12 h-12 text-gray-300 mb-2" />
|
<Icon name="material-symbols:description-outline" class="w-12 h-12 text-gray-300 mb-2" />
|
||||||
<p class="text-lg font-medium mb-1">No forms found</p>
|
<p class="text-lg font-medium mb-1">
|
||||||
<p class="text-sm">
|
{{ searchQuery ? 'No forms match your search' : 'No forms found' }}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm mb-2">
|
||||||
{{ searchQuery ? 'Try adjusting your search terms' : 'Create your first form to get started' }}
|
{{ searchQuery ? 'Try adjusting your search terms' : 'Create your first form to get started' }}
|
||||||
</p>
|
</p>
|
||||||
<RsButton v-if="!searchQuery" @click="createNewForm" variant="primary" size="sm" class="mt-4">
|
|
||||||
<Icon name="material-symbols:add" class="mr-1" />
|
<!-- Show current search if any -->
|
||||||
Create New Form
|
<div v-if="searchQuery" class="text-xs text-gray-400 mb-4">
|
||||||
</RsButton>
|
<div>Search: "{{ searchQuery }}"</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<RsButton v-if="searchQuery" @click="clearFilters" variant="secondary" size="sm">
|
||||||
|
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
||||||
|
Clear Filters
|
||||||
|
</RsButton>
|
||||||
|
<RsButton v-if="!searchQuery" @click="createNewForm" variant="primary" size="sm">
|
||||||
|
<Icon name="material-symbols:add" class="mr-1" />
|
||||||
|
Create New Form
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -207,7 +235,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted, watch, onUnmounted } from 'vue';
|
||||||
import { useFormBuilderStore } from '~/stores/formBuilder';
|
import { useFormBuilderStore } from '~/stores/formBuilder';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
@ -400,6 +428,28 @@ const goToDashboard = () => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadForms();
|
await loadForms();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Watch for changes in search and reload forms
|
||||||
|
watch([searchQuery], () => {
|
||||||
|
// Debounce the search to avoid too many API calls
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
loadForms();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
let searchTimeout = null;
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
const clearFilters = () => {
|
||||||
|
searchQuery.value = '';
|
||||||
|
// loadForms will be called automatically by the watcher
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clean up the search timeout on component unmount
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
@ -74,6 +74,7 @@ const pendingNavigation = ref(null);
|
|||||||
const navigationTarget = ref(null);
|
const navigationTarget = ref(null);
|
||||||
const navigationConfirmed = ref(false);
|
const navigationConfirmed = ref(false);
|
||||||
const isSaving = ref(false);
|
const isSaving = ref(false);
|
||||||
|
const isAddingComponent = ref(false); // Flag to prevent canvas reactivity issues during component addition
|
||||||
|
|
||||||
// Add a ref for the ProcessFlowCanvas component
|
// Add a ref for the ProcessFlowCanvas component
|
||||||
const processFlowCanvas = ref(null);
|
const processFlowCanvas = ref(null);
|
||||||
@ -148,6 +149,19 @@ const hasCurrentProcess = computed(() => {
|
|||||||
return !!processStore.currentProcess;
|
return !!processStore.currentProcess;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create computed properties for canvas props to control reactivity
|
||||||
|
const canvasNodes = computed(() => {
|
||||||
|
if (!processStore.currentProcess) return [];
|
||||||
|
// Return a shallow copy to avoid direct reactivity
|
||||||
|
return processStore.currentProcess.nodes.map(node => ({ ...node }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const canvasEdges = computed(() => {
|
||||||
|
if (!processStore.currentProcess) return [];
|
||||||
|
// Return a shallow copy to avoid direct reactivity
|
||||||
|
return processStore.currentProcess.edges.map(edge => ({ ...edge }));
|
||||||
|
});
|
||||||
|
|
||||||
// Computed properties for node data
|
// Computed properties for node data
|
||||||
const nodeLabel = computed({
|
const nodeLabel = computed({
|
||||||
get: () => selectedNodeData.value?.label || '',
|
get: () => selectedNodeData.value?.label || '',
|
||||||
@ -735,21 +749,47 @@ const saveProcess = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add a component handler to add components from the component panel
|
// Add a component handler to add components from the component panel
|
||||||
const onAddComponent = (component) => {
|
const onAddComponent = async (component) => {
|
||||||
// Create a new node from the component definition
|
if (isAddingComponent.value) return; // Prevent concurrent additions
|
||||||
const newNode = {
|
|
||||||
id: `${component.type}_${Date.now()}`,
|
|
||||||
type: component.type,
|
|
||||||
position: { x: 100, y: 100 }, // Default position
|
|
||||||
label: component.label,
|
|
||||||
data: component.data
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add the node to the process
|
try {
|
||||||
processStore.addNode(newNode);
|
isAddingComponent.value = true;
|
||||||
|
|
||||||
// Select the newly added node
|
// Create a new node from the component definition
|
||||||
onNodeSelected(newNode);
|
const newNode = {
|
||||||
|
id: `${component.type}_${Date.now()}`,
|
||||||
|
type: component.type,
|
||||||
|
position: { x: 100, y: 100 }, // Default position
|
||||||
|
label: component.label,
|
||||||
|
data: component.data
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the node to the process store
|
||||||
|
processStore.addNode(newNode);
|
||||||
|
|
||||||
|
// Wait for the next tick to ensure the store update is complete
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// Explicitly sync the canvas with current store state
|
||||||
|
if (processFlowCanvas.value && processFlowCanvas.value.syncCanvas) {
|
||||||
|
processFlowCanvas.value.syncCanvas(
|
||||||
|
processStore.currentProcess.nodes,
|
||||||
|
processStore.currentProcess.edges
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Select the newly added node
|
||||||
|
onNodeSelected(newNode);
|
||||||
|
|
||||||
|
console.log('Component added successfully:', component.type);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding component:', error);
|
||||||
|
} finally {
|
||||||
|
// Reset the flag after a short delay to allow canvas to stabilize
|
||||||
|
setTimeout(() => {
|
||||||
|
isAddingComponent.value = false;
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Handle template application
|
// Handle template application
|
||||||
@ -785,15 +825,15 @@ const applyProcessTemplate = async (template) => {
|
|||||||
processStore.currentProcess.edges = [];
|
processStore.currentProcess.edges = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add nodes and edges together - let the canvas watchers handle the sequencing
|
// Prepare template data
|
||||||
const templateNodes = template.nodes || [];
|
const templateNodes = template.nodes || [];
|
||||||
const templateEdges = template.edges || [];
|
const templateEdges = template.edges || [];
|
||||||
|
|
||||||
console.log('Adding template nodes:', templateNodes.length);
|
console.log('Adding template nodes:', templateNodes.length);
|
||||||
console.log('Adding template edges:', templateEdges.length);
|
console.log('Adding template edges:', templateEdges.length);
|
||||||
|
|
||||||
// Process nodes first
|
// Process nodes first and wait for them to be fully added
|
||||||
templateNodes.forEach((node) => {
|
for (const node of templateNodes) {
|
||||||
const newNode = {
|
const newNode = {
|
||||||
...node,
|
...node,
|
||||||
id: node.id, // Keep original ID for edge references
|
id: node.id, // Keep original ID for edge references
|
||||||
@ -806,10 +846,14 @@ const applyProcessTemplate = async (template) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
processStore.addNode(newNode);
|
processStore.addNode(newNode);
|
||||||
});
|
}
|
||||||
|
|
||||||
// Process edges after nodes
|
// Wait for nodes to be processed by the canvas
|
||||||
templateEdges.forEach((edge) => {
|
await nextTick();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
// Process edges after nodes are fully loaded
|
||||||
|
for (const edge of templateEdges) {
|
||||||
const newEdge = {
|
const newEdge = {
|
||||||
...edge,
|
...edge,
|
||||||
id: edge.id, // Keep original ID
|
id: edge.id, // Keep original ID
|
||||||
@ -818,7 +862,19 @@ const applyProcessTemplate = async (template) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
processStore.addEdge(newEdge);
|
processStore.addEdge(newEdge);
|
||||||
});
|
}
|
||||||
|
|
||||||
|
// Wait for edges to be added to store
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
// Explicitly sync the canvas to ensure everything is displayed
|
||||||
|
if (processFlowCanvas.value && processFlowCanvas.value.syncCanvas) {
|
||||||
|
console.log('Forcing canvas sync after template application...');
|
||||||
|
processFlowCanvas.value.syncCanvas(
|
||||||
|
processStore.currentProcess.nodes,
|
||||||
|
processStore.currentProcess.edges
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Add template variables to the variable store
|
// Add template variables to the variable store
|
||||||
if (template.variables && template.variables.length > 0) {
|
if (template.variables && template.variables.length > 0) {
|
||||||
@ -841,19 +897,20 @@ const applyProcessTemplate = async (template) => {
|
|||||||
// Mark the process as having unsaved changes
|
// Mark the process as having unsaved changes
|
||||||
processStore.unsavedChanges = true;
|
processStore.unsavedChanges = true;
|
||||||
|
|
||||||
// Fit the view to show all nodes
|
// Fit the view to show all nodes after a short delay
|
||||||
if (processFlowCanvas.value && processFlowCanvas.value.fitView) {
|
await nextTick();
|
||||||
nextTick(() => {
|
setTimeout(() => {
|
||||||
|
if (processFlowCanvas.value && processFlowCanvas.value.fitView) {
|
||||||
processFlowCanvas.value.fitView();
|
processFlowCanvas.value.fitView();
|
||||||
});
|
}
|
||||||
}
|
}, 200);
|
||||||
|
|
||||||
// Show success message
|
// Show success message
|
||||||
console.log(`Template "${template.name}" applied successfully`);
|
toast.success(`Template "${template.name}" applied successfully`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error applying process template:', error);
|
console.error('Error applying process template:', error);
|
||||||
alert('Failed to apply template: ' + error.message);
|
toast.error('Failed to apply template: ' + error.message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -1072,8 +1129,8 @@ watch(() => processStore.hasUnsavedChanges, (hasChanges) => {
|
|||||||
<div class="flex-1 relative">
|
<div class="flex-1 relative">
|
||||||
<ProcessFlowCanvas
|
<ProcessFlowCanvas
|
||||||
ref="processFlowCanvas"
|
ref="processFlowCanvas"
|
||||||
:initial-nodes="processStore.currentProcess.nodes"
|
:initial-nodes="canvasNodes"
|
||||||
:initial-edges="processStore.currentProcess.edges"
|
:initial-edges="canvasEdges"
|
||||||
@node-selected="onNodeSelected"
|
@node-selected="onNodeSelected"
|
||||||
@edge-selected="onEdgeSelected"
|
@edge-selected="onEdgeSelected"
|
||||||
@pane-click="onPaneClick"
|
@pane-click="onPaneClick"
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted } from 'vue';
|
import { ref, computed, onMounted, watch, onUnmounted } from 'vue';
|
||||||
import { useProcessBuilderStore } from '~/stores/processBuilder';
|
import { useProcessBuilderStore } from '~/stores/processBuilder';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
|
||||||
@ -30,29 +30,13 @@ const statusOptions = [
|
|||||||
{ value: '', label: 'All Status' },
|
{ value: '', label: 'All Status' },
|
||||||
{ value: 'draft', label: 'Draft' },
|
{ value: 'draft', label: 'Draft' },
|
||||||
{ value: 'published', label: 'Published' },
|
{ value: 'published', label: 'Published' },
|
||||||
{ value: 'archived', label: 'Archived' }
|
{ value: 'archived', label: 'Archived' },
|
||||||
|
{ value: 'deleted', label: 'Deleted' }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Filtered processes
|
// Filtered processes - now just returns the processes from store since filtering happens on backend
|
||||||
const filteredProcesses = computed(() => {
|
const filteredProcesses = computed(() => {
|
||||||
let filtered = processStore.processes;
|
return processStore.processes;
|
||||||
|
|
||||||
// Filter by search query
|
|
||||||
if (searchQuery.value) {
|
|
||||||
const query = searchQuery.value.toLowerCase();
|
|
||||||
filtered = filtered.filter(
|
|
||||||
process =>
|
|
||||||
process.name.toLowerCase().includes(query) ||
|
|
||||||
(process.description && process.description.toLowerCase().includes(query))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter by status
|
|
||||||
if (statusFilter.value) {
|
|
||||||
filtered = filtered.filter(process => process.status === statusFilter.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
return filtered;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Format date for display
|
// Format date for display
|
||||||
@ -75,18 +59,30 @@ const getStatusVariant = (status) => {
|
|||||||
case 'published': return 'success';
|
case 'published': return 'success';
|
||||||
case 'draft': return 'warning';
|
case 'draft': return 'warning';
|
||||||
case 'archived': return 'secondary';
|
case 'archived': return 'secondary';
|
||||||
|
case 'deleted': return 'danger';
|
||||||
default: return 'primary';
|
default: return 'primary';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load processes from API
|
// Load processes from API with filters
|
||||||
const loadProcesses = async () => {
|
const loadProcesses = async () => {
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
try {
|
try {
|
||||||
await processStore.fetchProcesses({
|
const options = {
|
||||||
sortBy: sortBy.value,
|
sortBy: sortBy.value,
|
||||||
sortOrder: sortOrder.value
|
sortOrder: sortOrder.value
|
||||||
});
|
};
|
||||||
|
|
||||||
|
// Add filters if they have values
|
||||||
|
if (statusFilter.value) {
|
||||||
|
options.status = statusFilter.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (searchQuery.value.trim()) {
|
||||||
|
options.search = searchQuery.value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
await processStore.fetchProcesses(options);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading processes:', error);
|
console.error('Error loading processes:', error);
|
||||||
// TODO: Show error notification
|
// TODO: Show error notification
|
||||||
@ -95,6 +91,17 @@ const loadProcesses = async () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Watch for changes in filters and reload processes
|
||||||
|
watch([searchQuery, statusFilter], () => {
|
||||||
|
// Debounce the search to avoid too many API calls
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
searchTimeout = setTimeout(() => {
|
||||||
|
loadProcesses();
|
||||||
|
}, 500);
|
||||||
|
});
|
||||||
|
|
||||||
|
let searchTimeout = null;
|
||||||
|
|
||||||
// Edit a process
|
// Edit a process
|
||||||
const editProcess = async (processId) => {
|
const editProcess = async (processId) => {
|
||||||
try {
|
try {
|
||||||
@ -166,6 +173,24 @@ const publishProcess = async (processId) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Restore a deleted process
|
||||||
|
const restoreProcess = async (processId) => {
|
||||||
|
try {
|
||||||
|
loading.value = true;
|
||||||
|
await processStore.restoreProcess(processId);
|
||||||
|
|
||||||
|
// Refresh the process list
|
||||||
|
await loadProcesses();
|
||||||
|
|
||||||
|
// TODO: Show success notification
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error restoring process:', error);
|
||||||
|
// TODO: Show error notification
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Show delete confirmation
|
// Show delete confirmation
|
||||||
const confirmDelete = (processId) => {
|
const confirmDelete = (processId) => {
|
||||||
processToDelete.value = processId;
|
processToDelete.value = processId;
|
||||||
@ -194,6 +219,18 @@ const goToBuilder = () => {
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await loadProcesses();
|
await loadProcesses();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Clear all filters
|
||||||
|
const clearFilters = () => {
|
||||||
|
searchQuery.value = '';
|
||||||
|
statusFilter.value = '';
|
||||||
|
// loadProcesses will be called automatically by the watcher
|
||||||
|
};
|
||||||
|
|
||||||
|
// Clean up the search timeout on component unmount
|
||||||
|
onUnmounted(() => {
|
||||||
|
clearTimeout(searchTimeout);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@ -231,31 +268,49 @@ onMounted(async () => {
|
|||||||
<div class="flex-1 p-6 overflow-auto">
|
<div class="flex-1 p-6 overflow-auto">
|
||||||
<!-- Filters and Search -->
|
<!-- Filters and Search -->
|
||||||
<div class="mb-6 flex flex-col sm:flex-row gap-4">
|
<div class="mb-6 flex flex-col sm:flex-row gap-4">
|
||||||
<div class="relative flex-1 max-w-md">
|
<div class="flex-1 max-w-md">
|
||||||
<input
|
<FormKit
|
||||||
v-model="searchQuery"
|
v-model="searchQuery"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search processes..."
|
placeholder="Search processes..."
|
||||||
class="w-full px-4 py-2 pl-10 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
:classes="{
|
||||||
/>
|
outer: 'mb-0',
|
||||||
<span class="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400">
|
wrapper: 'relative',
|
||||||
<Icon name="material-symbols:search" class="text-lg" />
|
inner: 'relative',
|
||||||
</span>
|
input: 'w-full pl-10 pr-4 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500',
|
||||||
|
prefixIcon: 'absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 pointer-events-none'
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #prefixIcon>
|
||||||
|
<Icon name="material-symbols:search" class="text-lg" />
|
||||||
|
</template>
|
||||||
|
</FormKit>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<select
|
<div class="min-w-48">
|
||||||
v-model="statusFilter"
|
<FormKit
|
||||||
class="px-3 py-2 border rounded bg-white focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
v-model="statusFilter"
|
||||||
>
|
type="select"
|
||||||
<option v-for="option in statusOptions" :key="option.value" :value="option.value">
|
:options="statusOptions"
|
||||||
{{ option.label }}
|
placeholder="Filter by status"
|
||||||
</option>
|
:classes="{
|
||||||
</select>
|
outer: 'mb-0',
|
||||||
|
input: 'w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 bg-white'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<RsButton @click="loadProcesses" variant="tertiary" size="sm" :disabled="loading">
|
<div class="flex gap-2">
|
||||||
<Icon name="material-symbols:refresh" class="mr-1" />
|
<RsButton @click="clearFilters" variant="secondary" size="sm" :disabled="loading" v-if="searchQuery || statusFilter">
|
||||||
Refresh
|
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
||||||
</RsButton>
|
Clear Filters
|
||||||
|
</RsButton>
|
||||||
|
|
||||||
|
<RsButton @click="loadProcesses" variant="tertiary" size="sm" :disabled="loading">
|
||||||
|
<Icon name="material-symbols:refresh" class="mr-1" />
|
||||||
|
Refresh
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading State -->
|
<!-- Loading State -->
|
||||||
@ -285,14 +340,29 @@ onMounted(async () => {
|
|||||||
<td colspan="7" class="px-6 py-12 text-center text-gray-500">
|
<td colspan="7" class="px-6 py-12 text-center text-gray-500">
|
||||||
<div class="flex flex-col items-center">
|
<div class="flex flex-col items-center">
|
||||||
<Icon name="material-symbols:folder-open-outline" class="w-12 h-12 text-gray-300 mb-2" />
|
<Icon name="material-symbols:folder-open-outline" class="w-12 h-12 text-gray-300 mb-2" />
|
||||||
<p class="text-lg font-medium mb-1">No processes found</p>
|
<p class="text-lg font-medium mb-1">
|
||||||
<p class="text-sm">
|
{{ (searchQuery || statusFilter) ? 'No processes match your filters' : 'No processes found' }}
|
||||||
{{ searchQuery || statusFilter ? 'Try adjusting your filters' : 'Create your first process to get started' }}
|
|
||||||
</p>
|
</p>
|
||||||
<RsButton v-if="!searchQuery && !statusFilter" @click="createNewProcess" variant="primary" size="sm" class="mt-4">
|
<p class="text-sm mb-2">
|
||||||
<Icon name="material-symbols:add" class="mr-1" />
|
{{ (searchQuery || statusFilter) ? 'Try adjusting your search or filter criteria' : 'Create your first process to get started' }}
|
||||||
Create New Process
|
</p>
|
||||||
</RsButton>
|
|
||||||
|
<!-- Show current filters if any -->
|
||||||
|
<div v-if="searchQuery || statusFilter" class="text-xs text-gray-400 mb-4 space-y-1">
|
||||||
|
<div v-if="searchQuery">Search: "{{ searchQuery }}"</div>
|
||||||
|
<div v-if="statusFilter">Status: {{ statusOptions.find(opt => opt.value === statusFilter)?.label }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<RsButton v-if="searchQuery || statusFilter" @click="clearFilters" variant="secondary" size="sm">
|
||||||
|
<Icon name="material-symbols:filter-alt-off" class="mr-1" />
|
||||||
|
Clear Filters
|
||||||
|
</RsButton>
|
||||||
|
<RsButton v-if="!searchQuery && !statusFilter" @click="createNewProcess" variant="primary" size="sm">
|
||||||
|
<Icon name="material-symbols:add" class="mr-1" />
|
||||||
|
Create New Process
|
||||||
|
</RsButton>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -326,42 +396,56 @@ onMounted(async () => {
|
|||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||||
<div class="flex space-x-2 justify-end">
|
<div class="flex space-x-2 justify-end">
|
||||||
|
<!-- Show restore button for deleted processes -->
|
||||||
<button
|
<button
|
||||||
@click="editProcess(process.id)"
|
v-if="process.status === 'deleted'"
|
||||||
class="p-1 text-blue-600 hover:text-blue-900 hover:bg-blue-50 rounded"
|
@click="restoreProcess(process.id)"
|
||||||
title="Edit Process"
|
|
||||||
:disabled="loading"
|
|
||||||
>
|
|
||||||
<Icon name="material-symbols:edit" class="text-lg" />
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
v-if="process.status === 'draft'"
|
|
||||||
@click="publishProcess(process.id)"
|
|
||||||
class="p-1 text-green-600 hover:text-green-900 hover:bg-green-50 rounded"
|
class="p-1 text-green-600 hover:text-green-900 hover:bg-green-50 rounded"
|
||||||
title="Publish Process"
|
title="Restore Process"
|
||||||
:disabled="loading"
|
:disabled="loading"
|
||||||
>
|
>
|
||||||
<Icon name="material-symbols:publish" class="text-lg" />
|
<Icon name="material-symbols:restore" class="text-lg" />
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<!-- Regular action buttons for non-deleted processes -->
|
||||||
@click="duplicateProcess(process)"
|
<template v-else>
|
||||||
class="p-1 text-indigo-600 hover:text-indigo-900 hover:bg-indigo-50 rounded"
|
<button
|
||||||
title="Duplicate Process"
|
@click="editProcess(process.id)"
|
||||||
:disabled="loading"
|
class="p-1 text-blue-600 hover:text-blue-900 hover:bg-blue-50 rounded"
|
||||||
>
|
title="Edit Process"
|
||||||
<Icon name="material-symbols:content-copy" class="text-lg" />
|
:disabled="loading"
|
||||||
</button>
|
>
|
||||||
|
<Icon name="material-symbols:edit" class="text-lg" />
|
||||||
<button
|
</button>
|
||||||
@click="confirmDelete(process.id)"
|
|
||||||
class="p-1 text-red-600 hover:text-red-900 hover:bg-red-50 rounded"
|
<button
|
||||||
title="Delete Process"
|
v-if="process.status === 'draft'"
|
||||||
:disabled="loading || process.status === 'published'"
|
@click="publishProcess(process.id)"
|
||||||
>
|
class="p-1 text-green-600 hover:text-green-900 hover:bg-green-50 rounded"
|
||||||
<Icon name="material-symbols:delete" class="text-lg" />
|
title="Publish Process"
|
||||||
</button>
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:publish" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="duplicateProcess(process)"
|
||||||
|
class="p-1 text-indigo-600 hover:text-indigo-900 hover:bg-indigo-50 rounded"
|
||||||
|
title="Duplicate Process"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:content-copy" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
@click="confirmDelete(process.id)"
|
||||||
|
class="p-1 text-red-600 hover:text-red-900 hover:bg-red-50 rounded"
|
||||||
|
title="Delete Process"
|
||||||
|
:disabled="loading || process.status === 'published'"
|
||||||
|
>
|
||||||
|
<Icon name="material-symbols:delete" class="text-lg" />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@ -379,14 +463,17 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Delete confirmation dialog -->
|
<!-- Delete confirmation dialog -->
|
||||||
<RsModal v-model="showDeleteConfirm" title="Confirm Delete" size="md" position="center">
|
<RsModal v-model="showDeleteConfirm" title="Move Process to Trash" size="md" position="center">
|
||||||
<div class="p-4">
|
<div class="p-4">
|
||||||
<div class="flex items-center mb-4">
|
<div class="flex items-center mb-4">
|
||||||
<Icon name="material-symbols:warning-outline" class="text-yellow-500 w-8 h-8 mr-3 flex-shrink-0" />
|
<Icon name="material-symbols:delete-outline" class="text-orange-500 w-8 h-8 mr-3 flex-shrink-0" />
|
||||||
<div>
|
<div>
|
||||||
<p class="text-gray-600 font-medium mb-1">Delete Process</p>
|
<p class="text-gray-600 font-medium mb-1">Move to Trash</p>
|
||||||
<p class="text-gray-600 text-sm">
|
<p class="text-gray-600 text-sm">
|
||||||
Are you sure you want to delete this process? This action cannot be undone and will permanently remove all process data.
|
This process will be moved to trash but not permanently deleted. You can restore it later from the "Deleted" filter if needed.
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-500 text-xs mt-2">
|
||||||
|
Note: Published processes cannot be deleted and must be unpublished first.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -396,9 +483,9 @@ onMounted(async () => {
|
|||||||
<RsButton @click="cancelDelete" variant="tertiary" :disabled="loading">
|
<RsButton @click="cancelDelete" variant="tertiary" :disabled="loading">
|
||||||
Cancel
|
Cancel
|
||||||
</RsButton>
|
</RsButton>
|
||||||
<RsButton @click="deleteProcess" variant="danger" :disabled="loading">
|
<RsButton @click="deleteProcess" variant="warning" :disabled="loading">
|
||||||
<Icon v-if="loading" name="material-symbols:progress-activity" class="w-4 h-4 animate-spin mr-1" />
|
<Icon v-if="loading" name="material-symbols:progress-activity" class="w-4 h-4 animate-spin mr-1" />
|
||||||
Delete
|
Move to Trash
|
||||||
</RsButton>
|
</RsButton>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -337,6 +337,13 @@
|
|||||||
"null"
|
"null"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"processDeletedDate": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"format": "date-time"
|
||||||
|
},
|
||||||
"creator": {
|
"creator": {
|
||||||
"anyOf": [
|
"anyOf": [
|
||||||
{
|
{
|
||||||
|
@ -88,6 +88,7 @@ model process {
|
|||||||
processSettings Json?
|
processSettings Json?
|
||||||
processVariables Json?
|
processVariables Json?
|
||||||
templateCategory String? @db.VarChar(100)
|
templateCategory String? @db.VarChar(100)
|
||||||
|
processDeletedDate DateTime? @db.DateTime(0)
|
||||||
creator user? @relation("ProcessCreator", fields: [processCreatedBy], references: [userID])
|
creator user? @relation("ProcessCreator", fields: [processCreatedBy], references: [userID])
|
||||||
|
|
||||||
@@index([processCreatedBy], map: "FK_process_creator")
|
@@index([processCreatedBy], map: "FK_process_creator")
|
||||||
|
@ -18,11 +18,14 @@ export default defineEventHandler(async (event) => {
|
|||||||
// Check if the ID is a UUID or numeric ID
|
// Check if the ID is a UUID or numeric ID
|
||||||
const isUUID = processId.length === 36 && processId.includes('-');
|
const isUUID = processId.length === 36 && processId.includes('-');
|
||||||
|
|
||||||
// First, check if the process exists
|
// First, check if the process exists and isn't already deleted
|
||||||
const existingProcess = await prisma.process.findFirst({
|
const existingProcess = await prisma.process.findFirst({
|
||||||
where: isUUID
|
where: {
|
||||||
? { processUUID: processId }
|
...(isUUID
|
||||||
: { processID: parseInt(processId) },
|
? { processUUID: processId }
|
||||||
|
: { processID: parseInt(processId) }),
|
||||||
|
processStatus: { not: 'deleted' } // Exclude already deleted processes
|
||||||
|
},
|
||||||
select: {
|
select: {
|
||||||
processID: true,
|
processID: true,
|
||||||
processName: true,
|
processName: true,
|
||||||
@ -33,7 +36,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
if (!existingProcess) {
|
if (!existingProcess) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Process not found'
|
error: 'Process not found or already deleted'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,20 +44,28 @@ export default defineEventHandler(async (event) => {
|
|||||||
if (existingProcess.processStatus === 'published') {
|
if (existingProcess.processStatus === 'published') {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: 'Cannot delete published processes. Please set status to draft first.'
|
error: 'Cannot delete published processes. Please unpublish the process first.'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Delete the process
|
// Soft delete: Update status to 'deleted' instead of actual deletion
|
||||||
await prisma.process.delete({
|
// This preserves all associated data and relationships
|
||||||
|
await prisma.process.update({
|
||||||
where: isUUID
|
where: isUUID
|
||||||
? { processUUID: processId }
|
? { processUUID: processId }
|
||||||
: { processID: parseInt(processId) }
|
: { processID: parseInt(processId) },
|
||||||
|
data: {
|
||||||
|
processStatus: 'deleted',
|
||||||
|
// Optionally add deletion metadata
|
||||||
|
processDeletedDate: new Date(),
|
||||||
|
processModifiedDate: new Date()
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
message: `Process "${existingProcess.processName}" deleted successfully`
|
message: `Process "${existingProcess.processName}" has been moved to trash`,
|
||||||
|
note: 'Process data has been preserved and can be restored if needed'
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error deleting process:', error);
|
console.error('Error deleting process:', error);
|
||||||
|
@ -31,10 +31,47 @@ export default defineEventHandler(async (event) => {
|
|||||||
if (body.processCategory !== undefined) updateData.processCategory = body.processCategory;
|
if (body.processCategory !== undefined) updateData.processCategory = body.processCategory;
|
||||||
if (body.processPriority !== undefined) updateData.processPriority = body.processPriority;
|
if (body.processPriority !== undefined) updateData.processPriority = body.processPriority;
|
||||||
if (body.processOwner !== undefined) updateData.processOwner = body.processOwner;
|
if (body.processOwner !== undefined) updateData.processOwner = body.processOwner;
|
||||||
if (body.processStatus !== undefined) updateData.processStatus = body.processStatus;
|
|
||||||
if (body.isTemplate !== undefined) updateData.isTemplate = body.isTemplate;
|
if (body.isTemplate !== undefined) updateData.isTemplate = body.isTemplate;
|
||||||
if (body.templateCategory !== undefined) updateData.templateCategory = body.templateCategory;
|
if (body.templateCategory !== undefined) updateData.templateCategory = body.templateCategory;
|
||||||
|
|
||||||
|
// Get current process to check status
|
||||||
|
const currentProcess = await prisma.process.findFirst({
|
||||||
|
where: isUUID
|
||||||
|
? { processUUID: processId }
|
||||||
|
: { processID: parseInt(processId) },
|
||||||
|
select: {
|
||||||
|
processStatus: true,
|
||||||
|
processVersion: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!currentProcess) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Process not found'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle status changes with validation
|
||||||
|
if (body.processStatus !== undefined) {
|
||||||
|
const currentStatus = currentProcess.processStatus;
|
||||||
|
const newStatus = body.processStatus;
|
||||||
|
|
||||||
|
// Validate status transitions
|
||||||
|
if (currentStatus === 'published' && newStatus === 'draft') {
|
||||||
|
// Allow unpublishing only if explicitly requested
|
||||||
|
if (body.allowUnpublish !== true) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Cannot change published process to draft without explicit confirmation. Use allowUnpublish: true.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateData.processStatus = newStatus;
|
||||||
|
}
|
||||||
|
// If no status provided, preserve current status (don't change it)
|
||||||
|
|
||||||
// Process definition (nodes, edges, viewport)
|
// Process definition (nodes, edges, viewport)
|
||||||
if (body.nodes !== undefined || body.edges !== undefined || body.viewport !== undefined) {
|
if (body.nodes !== undefined || body.edges !== undefined || body.viewport !== undefined) {
|
||||||
updateData.processDefinition = {
|
updateData.processDefinition = {
|
||||||
@ -61,16 +98,7 @@ export default defineEventHandler(async (event) => {
|
|||||||
|
|
||||||
// Version increment if major changes
|
// Version increment if major changes
|
||||||
if (body.incrementVersion === true) {
|
if (body.incrementVersion === true) {
|
||||||
const currentProcess = await prisma.process.findFirst({
|
updateData.processVersion = currentProcess.processVersion + 1;
|
||||||
where: isUUID
|
|
||||||
? { processUUID: processId }
|
|
||||||
: { processID: parseInt(processId) },
|
|
||||||
select: { processVersion: true }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (currentProcess) {
|
|
||||||
updateData.processVersion = currentProcess.processVersion + 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the process
|
// Update the process
|
||||||
|
86
server/api/process/[id]/restore.post.js
Normal file
86
server/api/process/[id]/restore.post.js
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
|
||||||
|
// Initialize Prisma client
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
export default defineEventHandler(async (event) => {
|
||||||
|
try {
|
||||||
|
// Get the process ID from the route parameter
|
||||||
|
const processId = getRouterParam(event, 'id');
|
||||||
|
|
||||||
|
if (!processId) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Process ID is required'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the ID is a UUID or numeric ID
|
||||||
|
const isUUID = processId.length === 36 && processId.includes('-');
|
||||||
|
|
||||||
|
// First, check if the process exists and is deleted
|
||||||
|
const existingProcess = await prisma.process.findFirst({
|
||||||
|
where: {
|
||||||
|
...(isUUID
|
||||||
|
? { processUUID: processId }
|
||||||
|
: { processID: parseInt(processId) }),
|
||||||
|
processStatus: 'deleted'
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
processID: true,
|
||||||
|
processName: true,
|
||||||
|
processStatus: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existingProcess) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Deleted process not found'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore the process by setting status back to draft
|
||||||
|
const restoredProcess = await prisma.process.update({
|
||||||
|
where: isUUID
|
||||||
|
? { processUUID: processId }
|
||||||
|
: { processID: parseInt(processId) },
|
||||||
|
data: {
|
||||||
|
processStatus: 'draft', // Restore as draft for safety
|
||||||
|
processDeletedDate: null, // Clear deletion date
|
||||||
|
processModifiedDate: new Date()
|
||||||
|
},
|
||||||
|
include: {
|
||||||
|
creator: {
|
||||||
|
select: {
|
||||||
|
userID: true,
|
||||||
|
userFullName: true,
|
||||||
|
userUsername: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
message: `Process "${existingProcess.processName}" has been restored`,
|
||||||
|
process: restoredProcess
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error restoring process:', error);
|
||||||
|
|
||||||
|
// Handle specific Prisma errors
|
||||||
|
if (error.code === 'P2025') {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Process not found'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: 'Failed to restore process',
|
||||||
|
details: process.env.NODE_ENV === 'development' ? error.message : undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
@ -21,8 +21,18 @@ export default defineEventHandler(async (event) => {
|
|||||||
// Build where clause
|
// Build where clause
|
||||||
const where = {};
|
const where = {};
|
||||||
|
|
||||||
if (status) {
|
// Exclude deleted processes by default unless explicitly requested
|
||||||
|
if (query.includeDeleted !== 'true') {
|
||||||
|
where.processStatus = { not: 'deleted' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status && status !== 'deleted') {
|
||||||
|
// If status filter is provided and it's not 'deleted', filter by that status
|
||||||
|
// and still exclude deleted processes
|
||||||
where.processStatus = status;
|
where.processStatus = status;
|
||||||
|
} else if (status === 'deleted') {
|
||||||
|
// If specifically requesting deleted processes, only show those
|
||||||
|
where.processStatus = 'deleted';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (category) {
|
if (category) {
|
||||||
|
@ -254,6 +254,8 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
|||||||
variables: useVariableStore().getAllVariables.process || {},
|
variables: useVariableStore().getAllVariables.process || {},
|
||||||
settings: this.currentProcess.settings || {},
|
settings: this.currentProcess.settings || {},
|
||||||
permissions: this.currentProcess.permissions || {}
|
permissions: this.currentProcess.permissions || {}
|
||||||
|
// Note: processStatus is intentionally NOT included here to preserve current status
|
||||||
|
// Status changes should only happen through explicit publish/unpublish actions
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await $fetch(`/api/process/${this.currentProcess.id}`, {
|
const response = await $fetch(`/api/process/${this.currentProcess.id}`, {
|
||||||
@ -293,7 +295,7 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
// Remove from local processes array
|
// Remove from local processes array (since we're filtering out deleted ones)
|
||||||
const index = this.processes.findIndex(p => p.id === processId);
|
const index = this.processes.findIndex(p => p.id === processId);
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
this.processes.splice(index, 1);
|
this.processes.splice(index, 1);
|
||||||
@ -317,6 +319,28 @@ export const useProcessBuilderStore = defineStore('processBuilder', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore a deleted process
|
||||||
|
*/
|
||||||
|
async restoreProcess(processId) {
|
||||||
|
try {
|
||||||
|
const response = await $fetch(`/api/process/${processId}/restore`, {
|
||||||
|
method: 'POST'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.success) {
|
||||||
|
// Refresh the processes list to reflect the change
|
||||||
|
// Don't modify local array directly since status filtering might be complex
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
throw new Error(response.error || 'Failed to restore process');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error restoring process:', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch all processes from database
|
* Fetch all processes from database
|
||||||
*/
|
*/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user