Skip to content

Commit 214731d

Browse files
committed
Enhances workflow editor with connection handling and improves PRD documentation
Adds connection creation functionality to the workflow editor, enabling users to link nodes in the visual DAG builder through drag-and-drop interactions. Implements branch strategy selection and enhanced webhook configuration options in the node config modal, providing more granular control over workflow behavior. Updates extensive PRD documentation across multiple components including queue health monitoring, error tracking integration, GitHub Actions workflows, and performance optimization features to reflect current project state and implementation details. Modifies issue PRD generation script to completely replace issue bodies with PRD content rather than appending, improving documentation consistency and reducing clutter while preserving original content in generated files.
1 parent fff512a commit 214731d

File tree

66 files changed

+2514
-1826
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+2514
-1826
lines changed

apps/web/src/lib/components/NodeConfigModal.svelte

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
branch: {
3838
fields: [
3939
{ name: 'branches', label: 'Number of Branches', type: 'number', required: true, placeholder: '2', min: 2, max: 10 },
40+
{ name: 'strategy', label: 'Branch Strategy', type: 'select', required: true, options: ['parallel', 'sequential'] },
4041
{ name: 'description', label: 'Description', type: 'text', placeholder: 'Branch description (e.g., "Broadcast to Discord and Slack")' }
4142
]
4243
},
@@ -48,7 +49,10 @@
4849
},
4950
webhook: {
5051
fields: [
51-
{ name: 'path', label: 'Webhook Path', type: 'text', required: true, placeholder: '/hooks/my-workflow' },
52+
{ name: 'url', label: 'Webhook URL', type: 'text', required: true, placeholder: 'https://api.example.com/webhook' },
53+
{ name: 'method', label: 'HTTP Method', type: 'select', required: true, options: ['POST', 'PUT', 'PATCH'] },
54+
{ name: 'headers', label: 'Headers (JSON)', type: 'textarea', placeholder: '{"Content-Type": "application/json", "Authorization": "Bearer {{token}}"}' },
55+
{ name: 'bodyTemplate', label: 'Body Template', type: 'textarea', required: true, placeholder: '{\n "title": "{{title}}",\n "content": "{{content}}",\n "timestamp": "{{timestamp}}"\n}', rows: 8 },
5256
{ name: 'description', label: 'Description', type: 'text', placeholder: 'Webhook description' }
5357
]
5458
},
@@ -133,7 +137,7 @@
133137
bind:value={config[field.name]}
134138
placeholder={field.placeholder || ''}
135139
required={field.required}
136-
rows="4"
140+
rows={field.rows || 4}
137141
></textarea>
138142
{:else if field.type === 'select'}
139143
<select id={field.name} bind:value={config[field.name]} required={field.required}>

apps/web/src/lib/components/WorkflowEditor.svelte

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,25 @@
99
let reactFlowWrapper = $state(null);
1010
let selectedNodeId = $state(null);
1111
12+
// Handle connection creation
13+
function handleConnect(connection) {
14+
const newEdge = {
15+
id: `edge-${connection.source}-${connection.target}-${Date.now()}`,
16+
source: connection.source,
17+
target: connection.target,
18+
sourceHandle: connection.sourceHandle,
19+
targetHandle: connection.targetHandle,
20+
type: 'smoothstep',
21+
animated: true
22+
};
23+
24+
edges = [...edges, newEdge];
25+
26+
if (onEdgesChange) {
27+
onEdgesChange(edges);
28+
}
29+
}
30+
1231
// Handle drag over (required to enable drop)
1332
function handleDragOver(event) {
1433
event.preventDefault();
@@ -83,6 +102,7 @@
83102
{nodes}
84103
{edges}
85104
fitView
105+
onconnect={handleConnect}
86106
defaultEdgeOptions={{ type: 'smoothstep', animated: true }}
87107
>
88108
<Background gap={12} size={1} />
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/**
2+
* Template processor for webhook body templates
3+
* Supports {{ variable }} syntax for field mapping
4+
* Uses JMESPath for complex expressions
5+
*/
6+
7+
import jmespath from 'jmespath';
8+
9+
/**
10+
* Process a template string with data using {{ }} syntax
11+
* @param {string} template - Template string with {{ }} placeholders
12+
* @param {object} data - Data object to extract values from
13+
* @returns {string} Processed template with values replaced
14+
*/
15+
export function processTemplate(template, data) {
16+
if (!template || typeof template !== 'string') {
17+
return template;
18+
}
19+
20+
// Match {{ expression }} patterns
21+
const regex = /\{\{([^}]+)\}\}/g;
22+
23+
return template.replace(regex, (match, expression) => {
24+
const trimmedExpr = expression.trim();
25+
26+
try {
27+
// Use JMESPath to evaluate the expression
28+
const result = jmespath.search(data, trimmedExpr);
29+
30+
// Handle different result types
31+
if (result === null || result === undefined) {
32+
return '';
33+
}
34+
35+
if (typeof result === 'object') {
36+
return JSON.stringify(result);
37+
}
38+
39+
return String(result);
40+
} catch (error) {
41+
console.error(`Template processing error for expression "${trimmedExpr}":`, error);
42+
return match; // Return original placeholder on error
43+
}
44+
});
45+
}
46+
47+
/**
48+
* Process a template object (like headers or body JSON)
49+
* @param {object|string} template - Template object or string
50+
* @param {object} data - Data object to extract values from
51+
* @returns {object|string} Processed template with values replaced
52+
*/
53+
export function processTemplateObject(template, data) {
54+
if (typeof template === 'string') {
55+
try {
56+
// Try to parse as JSON first
57+
const parsed = JSON.parse(template);
58+
return processTemplateObject(parsed, data);
59+
} catch {
60+
// If not JSON, process as string template
61+
return processTemplate(template, data);
62+
}
63+
}
64+
65+
if (Array.isArray(template)) {
66+
return template.map(item => processTemplateObject(item, data));
67+
}
68+
69+
if (template && typeof template === 'object') {
70+
const result = {};
71+
for (const [key, value] of Object.entries(template)) {
72+
const processedKey = processTemplate(key, data);
73+
result[processedKey] = processTemplateObject(value, data);
74+
}
75+
return result;
76+
}
77+
78+
return template;
79+
}
80+
81+
/**
82+
* Validate a template string for syntax errors
83+
* @param {string} template - Template string to validate
84+
* @returns {object} Validation result with { valid: boolean, errors: string[] }
85+
*/
86+
export function validateTemplate(template) {
87+
const errors = [];
88+
89+
if (!template || typeof template !== 'string') {
90+
return { valid: true, errors: [] };
91+
}
92+
93+
// Check for unmatched braces
94+
const openBraces = (template.match(/\{\{/g) || []).length;
95+
const closeBraces = (template.match(/\}\}/g) || []).length;
96+
97+
if (openBraces !== closeBraces) {
98+
errors.push('Unmatched template braces {{ }}');
99+
}
100+
101+
// Extract all expressions and validate them
102+
const regex = /\{\{([^}]+)\}\}/g;
103+
let match;
104+
105+
while ((match = regex.exec(template)) !== null) {
106+
const expression = match[1].trim();
107+
108+
if (!expression) {
109+
errors.push('Empty template expression found');
110+
continue;
111+
}
112+
113+
// Basic JMESPath syntax validation
114+
// We'll validate by attempting to search with empty data
115+
try {
116+
jmespath.search({}, expression);
117+
} catch (error) {
118+
errors.push(`Invalid JMESPath expression "${expression}": ${error.message}`);
119+
}
120+
}
121+
122+
return {
123+
valid: errors.length === 0,
124+
errors
125+
};
126+
}
127+
128+
/**
129+
* Extract all template variables from a template string
130+
* @param {string} template - Template string
131+
* @returns {string[]} Array of variable expressions found
132+
*/
133+
export function extractTemplateVariables(template) {
134+
if (!template || typeof template !== 'string') {
135+
return [];
136+
}
137+
138+
const variables = [];
139+
const regex = /\{\{([^}]+)\}\}/g;
140+
let match;
141+
142+
while ((match = regex.exec(template)) !== null) {
143+
variables.push(match[1].trim());
144+
}
145+
146+
return variables;
147+
}

0 commit comments

Comments
 (0)