@d34dman/flowdrop 0.0.32 → 0.0.34
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/WorkflowEditor.svelte +22 -0
- package/dist/components/playground/ChatPanel.svelte +5 -1
- package/dist/components/playground/ChatPanel.svelte.d.ts +2 -0
- package/dist/components/playground/MessageBubble.svelte +177 -28
- package/dist/components/playground/MessageBubble.svelte.d.ts +2 -0
- package/dist/components/playground/Playground.svelte +14 -8
- package/dist/core/index.d.ts +2 -1
- package/dist/core/index.js +2 -0
- package/dist/helpers/workflowEditorHelper.d.ts +32 -3
- package/dist/helpers/workflowEditorHelper.js +57 -6
- package/dist/services/playgroundService.js +5 -2
- package/dist/styles/base.css +9 -0
- package/dist/types/index.d.ts +3 -2
- package/dist/types/playground.d.ts +2 -0
- package/dist/utils/connections.d.ts +46 -0
- package/dist/utils/connections.js +91 -0
- package/package.json +1 -1
|
@@ -620,4 +620,26 @@
|
|
|
620
620
|
stroke: var(--flowdrop-edge-data-color-selected);
|
|
621
621
|
stroke-width: 2;
|
|
622
622
|
}
|
|
623
|
+
|
|
624
|
+
/* Loopback Edge: Dashed gray line for loop iteration connections */
|
|
625
|
+
:global(.flowdrop--edge--loopback path.svelte-flow__edge-path) {
|
|
626
|
+
stroke: var(--flowdrop-edge-loopback-color);
|
|
627
|
+
stroke-width: var(--flowdrop-edge-loopback-width);
|
|
628
|
+
stroke-dasharray: var(--flowdrop-edge-loopback-dasharray);
|
|
629
|
+
opacity: var(--flowdrop-edge-loopback-opacity);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
:global(.flowdrop--edge--loopback:hover path.svelte-flow__edge-path) {
|
|
633
|
+
stroke: var(--flowdrop-edge-loopback-color-hover);
|
|
634
|
+
stroke-width: var(--flowdrop-edge-loopback-width-hover);
|
|
635
|
+
opacity: 1;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
:global(.flowdrop--edge--loopback.selected path.svelte-flow__edge-path) {
|
|
639
|
+
stroke: var(--flowdrop-edge-loopback-color-selected);
|
|
640
|
+
stroke-width: var(--flowdrop-edge-loopback-width-hover);
|
|
641
|
+
stroke-dasharray: var(--flowdrop-edge-loopback-dasharray);
|
|
642
|
+
filter: drop-shadow(0 0 3px rgba(139, 92, 246, 0.4));
|
|
643
|
+
opacity: 1;
|
|
644
|
+
}
|
|
623
645
|
</style>
|
|
@@ -35,6 +35,8 @@
|
|
|
35
35
|
onStopExecution?: () => void;
|
|
36
36
|
/** Whether to show log messages inline (false = hide them) */
|
|
37
37
|
showLogsInline?: boolean;
|
|
38
|
+
/** Whether to enable markdown rendering in messages */
|
|
39
|
+
enableMarkdown?: boolean;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
42
|
let {
|
|
@@ -43,7 +45,8 @@
|
|
|
43
45
|
placeholder = 'Type your message...',
|
|
44
46
|
onSendMessage,
|
|
45
47
|
onStopExecution,
|
|
46
|
-
showLogsInline = false
|
|
48
|
+
showLogsInline = false,
|
|
49
|
+
enableMarkdown = true
|
|
47
50
|
}: Props = $props();
|
|
48
51
|
|
|
49
52
|
/** Input field value */
|
|
@@ -206,6 +209,7 @@
|
|
|
206
209
|
{message}
|
|
207
210
|
showTimestamp={showTimestamps}
|
|
208
211
|
isLast={index === displayMessages.length - 1}
|
|
212
|
+
{enableMarkdown}
|
|
209
213
|
/>
|
|
210
214
|
{/each}
|
|
211
215
|
|
|
@@ -14,6 +14,8 @@ interface Props {
|
|
|
14
14
|
onStopExecution?: () => void;
|
|
15
15
|
/** Whether to show log messages inline (false = hide them) */
|
|
16
16
|
showLogsInline?: boolean;
|
|
17
|
+
/** Whether to enable markdown rendering in messages */
|
|
18
|
+
enableMarkdown?: boolean;
|
|
17
19
|
}
|
|
18
20
|
declare const ChatPanel: import("svelte").Component<Props, {}, "">;
|
|
19
21
|
type ChatPanel = ReturnType<typeof ChatPanel>;
|
|
@@ -3,11 +3,13 @@
|
|
|
3
3
|
|
|
4
4
|
Renders individual messages in the playground chat interface.
|
|
5
5
|
Supports different message roles with distinct styling.
|
|
6
|
+
Supports markdown rendering for message content.
|
|
6
7
|
Styled with BEM syntax.
|
|
7
8
|
-->
|
|
8
9
|
|
|
9
10
|
<script lang="ts">
|
|
10
11
|
import Icon from '@iconify/svelte';
|
|
12
|
+
import { marked } from 'marked';
|
|
11
13
|
import type { PlaygroundMessage, PlaygroundMessageRole } from '../../types/playground.js';
|
|
12
14
|
|
|
13
15
|
/**
|
|
@@ -20,9 +22,20 @@
|
|
|
20
22
|
showTimestamp?: boolean;
|
|
21
23
|
/** Whether this is the last message (affects styling) */
|
|
22
24
|
isLast?: boolean;
|
|
25
|
+
/** Whether to render markdown content */
|
|
26
|
+
enableMarkdown?: boolean;
|
|
23
27
|
}
|
|
24
28
|
|
|
25
|
-
let { message, showTimestamp = true, isLast = false }: Props = $props();
|
|
29
|
+
let { message, showTimestamp = true, isLast = false, enableMarkdown = true }: Props = $props();
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Render content as markdown or plain text
|
|
33
|
+
*/
|
|
34
|
+
const renderedContent = $derived(
|
|
35
|
+
enableMarkdown && message.role !== 'log'
|
|
36
|
+
? marked.parse(message.content || '')
|
|
37
|
+
: message.content
|
|
38
|
+
);
|
|
26
39
|
|
|
27
40
|
/**
|
|
28
41
|
* Get the icon for the message role
|
|
@@ -144,7 +157,13 @@
|
|
|
144
157
|
|
|
145
158
|
<!-- Message Text -->
|
|
146
159
|
<div class="message-bubble__text">
|
|
147
|
-
{message.
|
|
160
|
+
{#if enableMarkdown && message.role !== 'log'}
|
|
161
|
+
<!-- Markdown content - marked.js sanitizes content by default -->
|
|
162
|
+
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
|
|
163
|
+
{@html renderedContent}
|
|
164
|
+
{:else}
|
|
165
|
+
{message.content}
|
|
166
|
+
{/if}
|
|
148
167
|
</div>
|
|
149
168
|
|
|
150
169
|
<!-- Metadata Footer -->
|
|
@@ -188,32 +207,33 @@
|
|
|
188
207
|
}
|
|
189
208
|
}
|
|
190
209
|
|
|
191
|
-
/* Role-specific styling */
|
|
210
|
+
/* Role-specific styling - Neutral theme */
|
|
192
211
|
.message-bubble--user {
|
|
193
|
-
background:
|
|
194
|
-
|
|
212
|
+
background-color: #f1f5f9;
|
|
213
|
+
border: 1px solid #e2e8f0;
|
|
214
|
+
color: #1e293b;
|
|
195
215
|
margin-left: 2rem;
|
|
196
216
|
flex-direction: row-reverse;
|
|
197
217
|
}
|
|
198
218
|
|
|
199
219
|
.message-bubble--assistant {
|
|
200
|
-
background-color: #
|
|
201
|
-
border: 1px solid #
|
|
202
|
-
color: #
|
|
220
|
+
background-color: #ffffff;
|
|
221
|
+
border: 1px solid #e5e7eb;
|
|
222
|
+
color: #1f2937;
|
|
203
223
|
margin-right: 2rem;
|
|
204
224
|
}
|
|
205
225
|
|
|
206
226
|
.message-bubble--system {
|
|
207
|
-
background-color: #
|
|
208
|
-
border: 1px solid #
|
|
209
|
-
color: #
|
|
227
|
+
background-color: #f9fafb;
|
|
228
|
+
border: 1px solid #e5e7eb;
|
|
229
|
+
color: #6b7280;
|
|
210
230
|
margin: 0 1rem;
|
|
211
231
|
font-size: 0.875rem;
|
|
212
232
|
}
|
|
213
233
|
|
|
214
234
|
.message-bubble--log {
|
|
215
|
-
background-color: #
|
|
216
|
-
border: 1px solid #
|
|
235
|
+
background-color: #f8fafc;
|
|
236
|
+
border: 1px solid #e2e8f0;
|
|
217
237
|
color: #475569;
|
|
218
238
|
margin: 0 1rem;
|
|
219
239
|
font-size: 0.8125rem;
|
|
@@ -249,18 +269,18 @@
|
|
|
249
269
|
}
|
|
250
270
|
|
|
251
271
|
.message-bubble--user .message-bubble__avatar {
|
|
252
|
-
background-color:
|
|
253
|
-
color: #
|
|
272
|
+
background-color: #e2e8f0;
|
|
273
|
+
color: #475569;
|
|
254
274
|
}
|
|
255
275
|
|
|
256
276
|
.message-bubble--assistant .message-bubble__avatar {
|
|
257
|
-
background-color: #
|
|
258
|
-
color: #
|
|
277
|
+
background-color: #e5e7eb;
|
|
278
|
+
color: #374151;
|
|
259
279
|
}
|
|
260
280
|
|
|
261
281
|
.message-bubble--system .message-bubble__avatar {
|
|
262
|
-
background-color: #
|
|
263
|
-
color: #
|
|
282
|
+
background-color: #f3f4f6;
|
|
283
|
+
color: #6b7280;
|
|
264
284
|
}
|
|
265
285
|
|
|
266
286
|
.message-bubble--log .message-bubble__avatar {
|
|
@@ -292,14 +312,15 @@
|
|
|
292
312
|
.message-bubble__role {
|
|
293
313
|
font-weight: 600;
|
|
294
314
|
font-size: 0.8125rem;
|
|
315
|
+
color: #374151;
|
|
295
316
|
}
|
|
296
317
|
|
|
297
318
|
.message-bubble--user .message-bubble__role {
|
|
298
|
-
color:
|
|
319
|
+
color: #475569;
|
|
299
320
|
}
|
|
300
321
|
|
|
301
322
|
.message-bubble--assistant .message-bubble__role {
|
|
302
|
-
color: #
|
|
323
|
+
color: #374151;
|
|
303
324
|
}
|
|
304
325
|
|
|
305
326
|
.message-bubble--log .message-bubble__role {
|
|
@@ -319,8 +340,8 @@
|
|
|
319
340
|
}
|
|
320
341
|
|
|
321
342
|
.message-bubble__log-level--info {
|
|
322
|
-
background-color: #
|
|
323
|
-
color: #
|
|
343
|
+
background-color: #e0f2fe;
|
|
344
|
+
color: #0369a1;
|
|
324
345
|
}
|
|
325
346
|
|
|
326
347
|
.message-bubble__log-level--warning {
|
|
@@ -340,24 +361,152 @@
|
|
|
340
361
|
|
|
341
362
|
.message-bubble__timestamp {
|
|
342
363
|
font-size: 0.6875rem;
|
|
343
|
-
|
|
364
|
+
color: #9ca3af;
|
|
344
365
|
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
345
366
|
}
|
|
346
367
|
|
|
347
368
|
.message-bubble--user .message-bubble__timestamp {
|
|
348
|
-
color:
|
|
369
|
+
color: #9ca3af;
|
|
349
370
|
}
|
|
350
371
|
|
|
351
372
|
/* Message text */
|
|
352
373
|
.message-bubble__text {
|
|
353
|
-
line-height: 1.
|
|
354
|
-
white-space: pre-wrap;
|
|
374
|
+
line-height: 1.6;
|
|
355
375
|
word-break: break-word;
|
|
356
376
|
}
|
|
357
377
|
|
|
358
378
|
.message-bubble--log .message-bubble__text {
|
|
359
379
|
font-size: 0.8125rem;
|
|
360
380
|
line-height: 1.4;
|
|
381
|
+
white-space: pre-wrap;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/* Markdown styling for message content */
|
|
385
|
+
.message-bubble__text :global(p) {
|
|
386
|
+
margin: 0 0 0.75rem 0;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
.message-bubble__text :global(p:last-child) {
|
|
390
|
+
margin-bottom: 0;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.message-bubble__text :global(h1),
|
|
394
|
+
.message-bubble__text :global(h2),
|
|
395
|
+
.message-bubble__text :global(h3),
|
|
396
|
+
.message-bubble__text :global(h4),
|
|
397
|
+
.message-bubble__text :global(h5),
|
|
398
|
+
.message-bubble__text :global(h6) {
|
|
399
|
+
margin: 1rem 0 0.5rem 0;
|
|
400
|
+
font-weight: 600;
|
|
401
|
+
line-height: 1.3;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
.message-bubble__text :global(h1:first-child),
|
|
405
|
+
.message-bubble__text :global(h2:first-child),
|
|
406
|
+
.message-bubble__text :global(h3:first-child),
|
|
407
|
+
.message-bubble__text :global(h4:first-child),
|
|
408
|
+
.message-bubble__text :global(h5:first-child),
|
|
409
|
+
.message-bubble__text :global(h6:first-child) {
|
|
410
|
+
margin-top: 0;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
.message-bubble__text :global(h1) {
|
|
414
|
+
font-size: 1.25rem;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.message-bubble__text :global(h2) {
|
|
418
|
+
font-size: 1.125rem;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
.message-bubble__text :global(h3) {
|
|
422
|
+
font-size: 1rem;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
.message-bubble__text :global(ul),
|
|
426
|
+
.message-bubble__text :global(ol) {
|
|
427
|
+
margin: 0.5rem 0;
|
|
428
|
+
padding-left: 1.5rem;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.message-bubble__text :global(li) {
|
|
432
|
+
margin: 0.25rem 0;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
.message-bubble__text :global(code) {
|
|
436
|
+
background-color: rgba(0, 0, 0, 0.06);
|
|
437
|
+
padding: 0.125rem 0.375rem;
|
|
438
|
+
border-radius: 0.25rem;
|
|
439
|
+
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
|
440
|
+
font-size: 0.875em;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.message-bubble__text :global(pre) {
|
|
444
|
+
background-color: #1e293b;
|
|
445
|
+
color: #e2e8f0;
|
|
446
|
+
padding: 0.75rem 1rem;
|
|
447
|
+
border-radius: 0.5rem;
|
|
448
|
+
overflow-x: auto;
|
|
449
|
+
margin: 0.75rem 0;
|
|
450
|
+
font-size: 0.8125rem;
|
|
451
|
+
line-height: 1.5;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
.message-bubble__text :global(pre code) {
|
|
455
|
+
background-color: transparent;
|
|
456
|
+
padding: 0;
|
|
457
|
+
border-radius: 0;
|
|
458
|
+
color: inherit;
|
|
459
|
+
font-size: inherit;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.message-bubble__text :global(blockquote) {
|
|
463
|
+
border-left: 3px solid #d1d5db;
|
|
464
|
+
padding-left: 1rem;
|
|
465
|
+
margin: 0.75rem 0;
|
|
466
|
+
color: #6b7280;
|
|
467
|
+
font-style: italic;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
.message-bubble__text :global(a) {
|
|
471
|
+
color: #2563eb;
|
|
472
|
+
text-decoration: none;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
.message-bubble__text :global(a:hover) {
|
|
476
|
+
text-decoration: underline;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.message-bubble__text :global(hr) {
|
|
480
|
+
border: none;
|
|
481
|
+
border-top: 1px solid #e5e7eb;
|
|
482
|
+
margin: 1rem 0;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.message-bubble__text :global(table) {
|
|
486
|
+
border-collapse: collapse;
|
|
487
|
+
width: 100%;
|
|
488
|
+
margin: 0.75rem 0;
|
|
489
|
+
font-size: 0.875rem;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.message-bubble__text :global(th),
|
|
493
|
+
.message-bubble__text :global(td) {
|
|
494
|
+
border: 1px solid #e5e7eb;
|
|
495
|
+
padding: 0.5rem 0.75rem;
|
|
496
|
+
text-align: left;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
.message-bubble__text :global(th) {
|
|
500
|
+
background-color: #f9fafb;
|
|
501
|
+
font-weight: 600;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
.message-bubble__text :global(strong) {
|
|
505
|
+
font-weight: 600;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
.message-bubble__text :global(em) {
|
|
509
|
+
font-style: italic;
|
|
361
510
|
}
|
|
362
511
|
|
|
363
512
|
/* Footer */
|
|
@@ -367,7 +516,7 @@
|
|
|
367
516
|
gap: 0.75rem;
|
|
368
517
|
margin-top: 0.5rem;
|
|
369
518
|
font-size: 0.6875rem;
|
|
370
|
-
|
|
519
|
+
color: #9ca3af;
|
|
371
520
|
}
|
|
372
521
|
|
|
373
522
|
.message-bubble--user .message-bubble__footer {
|
|
@@ -9,6 +9,8 @@ interface Props {
|
|
|
9
9
|
showTimestamp?: boolean;
|
|
10
10
|
/** Whether this is the last message (affects styling) */
|
|
11
11
|
isLast?: boolean;
|
|
12
|
+
/** Whether to render markdown content */
|
|
13
|
+
enableMarkdown?: boolean;
|
|
12
14
|
}
|
|
13
15
|
declare const MessageBubble: import("svelte").Component<Props, {}, "">;
|
|
14
16
|
type MessageBubble = ReturnType<typeof MessageBubble>;
|
|
@@ -335,15 +335,20 @@
|
|
|
335
335
|
playgroundActions.addMessages(response.data);
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
338
|
+
// Update session status
|
|
339
|
+
if (response.sessionStatus) {
|
|
340
|
+
playgroundActions.updateSessionStatus(response.sessionStatus);
|
|
341
|
+
|
|
342
|
+
// Stop executing if idle, completed, or failed
|
|
343
|
+
// "idle" means no processing is happening (execution finished)
|
|
344
|
+
if (
|
|
345
|
+
response.sessionStatus === 'idle' ||
|
|
346
|
+
response.sessionStatus === 'completed' ||
|
|
347
|
+
response.sessionStatus === 'failed'
|
|
348
|
+
) {
|
|
349
|
+
playgroundActions.setExecuting(false);
|
|
346
350
|
}
|
|
351
|
+
}
|
|
347
352
|
},
|
|
348
353
|
pollingInterval
|
|
349
354
|
);
|
|
@@ -513,6 +518,7 @@
|
|
|
513
518
|
showTimestamps={config.showTimestamps ?? true}
|
|
514
519
|
autoScroll={config.autoScroll ?? true}
|
|
515
520
|
showLogsInline={config.logDisplayMode === 'inline'}
|
|
521
|
+
enableMarkdown={config.enableMarkdown ?? true}
|
|
516
522
|
onSendMessage={handleSendMessage}
|
|
517
523
|
onStopExecution={handleStopExecution}
|
|
518
524
|
/>
|
package/dist/core/index.d.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* import { getStatusColor, createDefaultExecutionInfo } from "@d34dman/flowdrop/core";
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
|
-
export type { NodeCategory, NodeDataType, NodePort, DynamicPort, Branch, NodeMetadata, NodeExtensions, NodeUIExtensions, ConfigValues, WorkflowNode, WorkflowEdge, Workflow, ApiResponse, NodesResponse, WorkflowResponse, WorkflowsResponse, ExecutionStatus, ExecutionResult, FlowDropConfig, WorkflowEvents, BuiltinNodeType, PortConfig, PortCompatibilityRule, ConfigSchema, ConfigProperty, HttpMethod, DynamicSchemaEndpoint, ExternalEditLink, ConfigEditOptions } from '../types/index.js';
|
|
17
|
+
export type { NodeCategory, NodeDataType, NodePort, DynamicPort, Branch, NodeMetadata, NodeExtensions, NodeUIExtensions, ConfigValues, WorkflowNode, WorkflowEdge, Workflow, ApiResponse, NodesResponse, WorkflowResponse, WorkflowsResponse, ExecutionStatus, ExecutionResult, FlowDropConfig, WorkflowEvents, BuiltinNodeType, PortConfig, PortCompatibilityRule, ConfigSchema, ConfigProperty, HttpMethod, DynamicSchemaEndpoint, ExternalEditLink, ConfigEditOptions, EdgeCategory } from '../types/index.js';
|
|
18
18
|
export type { WorkflowEditorConfig, EditorFeatures, UIConfig, APIConfig, ExecutionConfig, StorageConfig } from '../types/config.js';
|
|
19
19
|
export type { AuthProvider, StaticAuthConfig, CallbackAuthConfig } from '../types/auth.js';
|
|
20
20
|
export type { WorkflowChangeType, FlowDropEventHandlers, FlowDropFeatures } from '../types/events.js';
|
|
@@ -35,6 +35,7 @@ export * from '../utils/colors.js';
|
|
|
35
35
|
export * from '../utils/icons.js';
|
|
36
36
|
export * from '../utils/config.js';
|
|
37
37
|
export * from '../utils/nodeTypes.js';
|
|
38
|
+
export { isLoopbackEdge, isValidLoopbackCycle, hasCycles, hasInvalidCycles } from '../utils/connections.js';
|
|
38
39
|
export { isFieldOptionArray, normalizeOptions } from '../components/form/types.js';
|
|
39
40
|
export { DEFAULT_PORT_CONFIG } from '../config/defaultPortConfig.js';
|
|
40
41
|
export { defaultEndpointConfig, createEndpointConfig } from '../config/endpoints.js';
|
package/dist/core/index.js
CHANGED
|
@@ -38,6 +38,8 @@ export * from '../utils/icons.js';
|
|
|
38
38
|
export * from '../utils/config.js';
|
|
39
39
|
// Node type utilities
|
|
40
40
|
export * from '../utils/nodeTypes.js';
|
|
41
|
+
// Connection utilities (including loopback edge detection)
|
|
42
|
+
export { isLoopbackEdge, isValidLoopbackCycle, hasCycles, hasInvalidCycles } from '../utils/connections.js';
|
|
41
43
|
// Form type utilities
|
|
42
44
|
export { isFieldOptionArray, normalizeOptions } from '../components/form/types.js';
|
|
43
45
|
// ============================================================================
|
|
@@ -14,9 +14,10 @@ export declare function generateNodeId(nodeTypeId: string, existingNodes: Workfl
|
|
|
14
14
|
* Edge category type for styling purposes
|
|
15
15
|
* - trigger: For control flow connections (dataType: "trigger")
|
|
16
16
|
* - tool: Dashed amber line for tool connections (dataType: "tool")
|
|
17
|
+
* - loopback: Dashed gray line for loop iteration connections (targets loop_back port)
|
|
17
18
|
* - data: Normal gray line for all other data connections
|
|
18
19
|
*/
|
|
19
|
-
export type EdgeCategory = 'trigger' | 'tool' | 'data';
|
|
20
|
+
export type EdgeCategory = 'trigger' | 'tool' | 'loopback' | 'data';
|
|
20
21
|
/**
|
|
21
22
|
* Edge styling configuration based on source port data type
|
|
22
23
|
*/
|
|
@@ -49,12 +50,25 @@ export declare class EdgeStylingHelper {
|
|
|
49
50
|
static getPortDataType(node: WorkflowNodeType, portId: string, portType: 'input' | 'output'): string | null;
|
|
50
51
|
/**
|
|
51
52
|
* Determine the edge category based on source port data type
|
|
53
|
+
* Note: This method does not check for loopback edges.
|
|
54
|
+
* Use getEdgeCategoryWithLoopback() for full edge categorization.
|
|
55
|
+
*
|
|
52
56
|
* @param sourcePortDataType - The data type of the source output port
|
|
53
57
|
* @returns The edge category for styling
|
|
54
58
|
*/
|
|
55
59
|
static getEdgeCategory(sourcePortDataType: string | null): EdgeCategory;
|
|
56
60
|
/**
|
|
57
|
-
*
|
|
61
|
+
* Determine the full edge category including loopback detection
|
|
62
|
+
* Loopback edges take precedence over source port data type
|
|
63
|
+
*
|
|
64
|
+
* @param edge - The edge to categorize
|
|
65
|
+
* @param sourcePortDataType - The data type of the source output port
|
|
66
|
+
* @returns The edge category for styling
|
|
67
|
+
*/
|
|
68
|
+
static getEdgeCategoryWithLoopback(edge: WorkflowEdge, sourcePortDataType: string | null): EdgeCategory;
|
|
69
|
+
/**
|
|
70
|
+
* Apply custom styling to connection edges based on edge type:
|
|
71
|
+
* - Loopback: Dashed gray line for loop iteration (targets loop_back port)
|
|
58
72
|
* - Trigger ports: Solid black line with arrow
|
|
59
73
|
* - Tool ports: Dashed amber line with arrow
|
|
60
74
|
* - Data ports: Normal gray line with arrow
|
|
@@ -118,9 +132,24 @@ export declare class WorkflowOperationsHelper {
|
|
|
118
132
|
*/
|
|
119
133
|
static exportWorkflow(workflow: Workflow | null): void;
|
|
120
134
|
/**
|
|
121
|
-
* Check if workflow has cycles
|
|
135
|
+
* Check if workflow has invalid cycles (excludes valid loopback cycles)
|
|
136
|
+
* Valid loopback cycles are used for ForEach node iteration and should not
|
|
137
|
+
* trigger a warning.
|
|
138
|
+
*
|
|
139
|
+
* @param nodes - Array of workflow nodes
|
|
140
|
+
* @param edges - Array of workflow edges
|
|
141
|
+
* @returns True if there are invalid (non-loopback) cycles
|
|
122
142
|
*/
|
|
123
143
|
static checkWorkflowCycles(nodes: WorkflowNodeType[], edges: WorkflowEdge[]): boolean;
|
|
144
|
+
/**
|
|
145
|
+
* Check if workflow has any cycles (including valid loopback cycles)
|
|
146
|
+
* Use this when you need to detect ALL cycles regardless of type.
|
|
147
|
+
*
|
|
148
|
+
* @param nodes - Array of workflow nodes
|
|
149
|
+
* @param edges - Array of workflow edges
|
|
150
|
+
* @returns True if any cycle exists
|
|
151
|
+
*/
|
|
152
|
+
static checkWorkflowHasAnyCycles(nodes: WorkflowNodeType[], edges: WorkflowEdge[]): boolean;
|
|
124
153
|
}
|
|
125
154
|
/**
|
|
126
155
|
* Configuration helper
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Contains business logic for workflow operations
|
|
4
4
|
*/
|
|
5
5
|
import { MarkerType } from '@xyflow/svelte';
|
|
6
|
-
import { hasCycles } from '../utils/connections.js';
|
|
6
|
+
import { hasCycles, hasInvalidCycles, isLoopbackEdge } from '../utils/connections.js';
|
|
7
7
|
import { workflowApi, nodeApi, setEndpointConfig } from '../services/api.js';
|
|
8
8
|
import { v4 as uuidv4 } from 'uuid';
|
|
9
9
|
import { workflowActions } from '../stores/workflowStore.js';
|
|
@@ -104,6 +104,9 @@ export class EdgeStylingHelper {
|
|
|
104
104
|
}
|
|
105
105
|
/**
|
|
106
106
|
* Determine the edge category based on source port data type
|
|
107
|
+
* Note: This method does not check for loopback edges.
|
|
108
|
+
* Use getEdgeCategoryWithLoopback() for full edge categorization.
|
|
109
|
+
*
|
|
107
110
|
* @param sourcePortDataType - The data type of the source output port
|
|
108
111
|
* @returns The edge category for styling
|
|
109
112
|
*/
|
|
@@ -118,7 +121,25 @@ export class EdgeStylingHelper {
|
|
|
118
121
|
return 'data';
|
|
119
122
|
}
|
|
120
123
|
/**
|
|
121
|
-
*
|
|
124
|
+
* Determine the full edge category including loopback detection
|
|
125
|
+
* Loopback edges take precedence over source port data type
|
|
126
|
+
*
|
|
127
|
+
* @param edge - The edge to categorize
|
|
128
|
+
* @param sourcePortDataType - The data type of the source output port
|
|
129
|
+
* @returns The edge category for styling
|
|
130
|
+
*/
|
|
131
|
+
static getEdgeCategoryWithLoopback(edge, sourcePortDataType) {
|
|
132
|
+
// Loopback edges are identified by their target handle
|
|
133
|
+
// Check this first as it takes precedence
|
|
134
|
+
if (isLoopbackEdge(edge)) {
|
|
135
|
+
return 'loopback';
|
|
136
|
+
}
|
|
137
|
+
// Fall back to source port data type categorization
|
|
138
|
+
return this.getEdgeCategory(sourcePortDataType);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Apply custom styling to connection edges based on edge type:
|
|
142
|
+
* - Loopback: Dashed gray line for loop iteration (targets loop_back port)
|
|
122
143
|
* - Trigger ports: Solid black line with arrow
|
|
123
144
|
* - Tool ports: Dashed amber line with arrow
|
|
124
145
|
* - Data ports: Normal gray line with arrow
|
|
@@ -130,17 +151,30 @@ export class EdgeStylingHelper {
|
|
|
130
151
|
const sourcePortDataType = sourcePortId
|
|
131
152
|
? this.getPortDataType(sourceNode, sourcePortId, 'output')
|
|
132
153
|
: null;
|
|
133
|
-
// Determine edge category
|
|
134
|
-
const edgeCategory = this.
|
|
154
|
+
// Determine edge category (loopback takes precedence)
|
|
155
|
+
const edgeCategory = this.getEdgeCategoryWithLoopback(edge, sourcePortDataType);
|
|
135
156
|
// Edge color constants (matching CSS tokens in base.css)
|
|
136
157
|
const EDGE_COLORS = {
|
|
137
158
|
trigger: '#111827', // --color-ref-gray-900
|
|
138
159
|
tool: '#f59e0b', // --color-ref-amber-500
|
|
160
|
+
loopback: '#6b7280', // --color-ref-gray-500
|
|
139
161
|
data: '#9ca3af' // --color-ref-gray-400
|
|
140
162
|
};
|
|
141
163
|
// Apply styling based on edge category
|
|
142
164
|
// CSS classes handle styling via tokens; inline styles are fallback
|
|
143
165
|
switch (edgeCategory) {
|
|
166
|
+
case 'loopback':
|
|
167
|
+
// Loopback edges: dashed gray line for loop iteration
|
|
168
|
+
edge.style =
|
|
169
|
+
'stroke: var(--flowdrop-edge-loopback-color); stroke-dasharray: 5 5; stroke-width: var(--flowdrop-edge-loopback-width);';
|
|
170
|
+
edge.class = 'flowdrop--edge--loopback';
|
|
171
|
+
edge.markerEnd = {
|
|
172
|
+
type: MarkerType.ArrowClosed,
|
|
173
|
+
width: 14,
|
|
174
|
+
height: 14,
|
|
175
|
+
color: EDGE_COLORS.loopback
|
|
176
|
+
};
|
|
177
|
+
break;
|
|
144
178
|
case 'trigger':
|
|
145
179
|
// Trigger edges: solid dark line for control flow
|
|
146
180
|
edge.style =
|
|
@@ -183,7 +217,7 @@ export class EdgeStylingHelper {
|
|
|
183
217
|
metadata: {
|
|
184
218
|
...(edge.data?.metadata || {}),
|
|
185
219
|
edgeType: edgeCategory,
|
|
186
|
-
sourcePortDataType: sourcePortDataType
|
|
220
|
+
sourcePortDataType: sourcePortDataType ?? undefined
|
|
187
221
|
},
|
|
188
222
|
targetNodeType: targetNode.type,
|
|
189
223
|
targetCategory: targetNode.data.metadata.category
|
|
@@ -484,9 +518,26 @@ export class WorkflowOperationsHelper {
|
|
|
484
518
|
URL.revokeObjectURL(url);
|
|
485
519
|
}
|
|
486
520
|
/**
|
|
487
|
-
* Check if workflow has cycles
|
|
521
|
+
* Check if workflow has invalid cycles (excludes valid loopback cycles)
|
|
522
|
+
* Valid loopback cycles are used for ForEach node iteration and should not
|
|
523
|
+
* trigger a warning.
|
|
524
|
+
*
|
|
525
|
+
* @param nodes - Array of workflow nodes
|
|
526
|
+
* @param edges - Array of workflow edges
|
|
527
|
+
* @returns True if there are invalid (non-loopback) cycles
|
|
488
528
|
*/
|
|
489
529
|
static checkWorkflowCycles(nodes, edges) {
|
|
530
|
+
return hasInvalidCycles(nodes, edges);
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Check if workflow has any cycles (including valid loopback cycles)
|
|
534
|
+
* Use this when you need to detect ALL cycles regardless of type.
|
|
535
|
+
*
|
|
536
|
+
* @param nodes - Array of workflow nodes
|
|
537
|
+
* @param edges - Array of workflow edges
|
|
538
|
+
* @returns True if any cycle exists
|
|
539
|
+
*/
|
|
540
|
+
static checkWorkflowHasAnyCycles(nodes, edges) {
|
|
490
541
|
return hasCycles(nodes, edges);
|
|
491
542
|
}
|
|
492
543
|
}
|
|
@@ -263,8 +263,11 @@ export class PlaygroundService {
|
|
|
263
263
|
this.currentBackoff = interval;
|
|
264
264
|
// Call the callback with new messages
|
|
265
265
|
callback(response);
|
|
266
|
-
// Stop polling if session is completed or failed
|
|
267
|
-
|
|
266
|
+
// Stop polling if session is idle, completed, or failed
|
|
267
|
+
// "idle" means no processing is happening (execution finished)
|
|
268
|
+
if (response.sessionStatus === 'idle' ||
|
|
269
|
+
response.sessionStatus === 'completed' ||
|
|
270
|
+
response.sessionStatus === 'failed') {
|
|
268
271
|
this.stopPolling();
|
|
269
272
|
return;
|
|
270
273
|
}
|
package/dist/styles/base.css
CHANGED
|
@@ -1157,6 +1157,15 @@
|
|
|
1157
1157
|
--flowdrop-edge-data-color-hover: var(--color-ref-gray-500);
|
|
1158
1158
|
--flowdrop-edge-data-color-selected: var(--color-ref-violet-600);
|
|
1159
1159
|
|
|
1160
|
+
/* Loopback edge styling tokens */
|
|
1161
|
+
--flowdrop-edge-loopback-color: var(--color-ref-gray-500);
|
|
1162
|
+
--flowdrop-edge-loopback-color-hover: var(--color-ref-gray-600);
|
|
1163
|
+
--flowdrop-edge-loopback-color-selected: var(--color-ref-violet-600);
|
|
1164
|
+
--flowdrop-edge-loopback-width: 1.5px;
|
|
1165
|
+
--flowdrop-edge-loopback-width-hover: 2.5px;
|
|
1166
|
+
--flowdrop-edge-loopback-dasharray: 5 5;
|
|
1167
|
+
--flowdrop-edge-loopback-opacity: 0.85;
|
|
1168
|
+
|
|
1160
1169
|
/* Tool node theming tokens */
|
|
1161
1170
|
--flowdrop-tool-node-color: var(--color-ref-amber-500);
|
|
1162
1171
|
--flowdrop-tool-node-color-light: var(--color-ref-amber-50);
|
package/dist/types/index.d.ts
CHANGED
|
@@ -656,13 +656,14 @@ export interface WorkflowNode extends Node {
|
|
|
656
656
|
};
|
|
657
657
|
}
|
|
658
658
|
/**
|
|
659
|
-
* Edge category types based on source port data type
|
|
659
|
+
* Edge category types based on source port data type or target handle
|
|
660
660
|
* Used for visual styling of edges on the canvas
|
|
661
661
|
* - trigger: For control flow connections (dataType: "trigger")
|
|
662
662
|
* - tool: Dashed amber line for tool connections (dataType: "tool")
|
|
663
|
+
* - loopback: Dashed gray line for loop iteration (targets loop_back port)
|
|
663
664
|
* - data: Normal gray line for all other data connections
|
|
664
665
|
*/
|
|
665
|
-
export type EdgeCategory = 'trigger' | 'tool' | 'data';
|
|
666
|
+
export type EdgeCategory = 'trigger' | 'tool' | 'loopback' | 'data';
|
|
666
667
|
/**
|
|
667
668
|
* Extended edge type for workflows
|
|
668
669
|
*/
|
|
@@ -178,6 +178,8 @@ export interface PlaygroundConfig {
|
|
|
178
178
|
showTimestamps?: boolean;
|
|
179
179
|
/** Show log messages inline or in collapsible section (default: "collapsible") */
|
|
180
180
|
logDisplayMode?: 'inline' | 'collapsible';
|
|
181
|
+
/** Enable markdown rendering in messages (default: true) */
|
|
182
|
+
enableMarkdown?: boolean;
|
|
181
183
|
}
|
|
182
184
|
/**
|
|
183
185
|
* Display mode for the Playground component
|
|
@@ -2,6 +2,29 @@
|
|
|
2
2
|
* Connection validation utilities for FlowDrop
|
|
3
3
|
*/
|
|
4
4
|
import type { NodeMetadata, NodePort, NodeDataType, WorkflowNode, WorkflowEdge, PortConfig, PortDataTypeConfig } from '../types/index.js';
|
|
5
|
+
/**
|
|
6
|
+
* Determines if an edge is a loopback edge.
|
|
7
|
+
* Loopback edges target the special `loop_back` input port on ForEach nodes.
|
|
8
|
+
* These edges are used to trigger the next iteration in a loop construct.
|
|
9
|
+
*
|
|
10
|
+
* @param edge - The edge to check
|
|
11
|
+
* @returns True if the edge is a loopback edge
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```typescript
|
|
15
|
+
* const edge = { targetHandle: "foreach.1-input-loop_back", ... };
|
|
16
|
+
* const isLoop = isLoopbackEdge(edge); // true
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export declare function isLoopbackEdge(edge: WorkflowEdge): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Checks if a cycle consists entirely of loopback edges.
|
|
22
|
+
* A valid loopback cycle only contains edges that target loop_back ports.
|
|
23
|
+
*
|
|
24
|
+
* @param cycleEdges - Array of edges that form a cycle
|
|
25
|
+
* @returns True if all edges in the cycle are loopback edges
|
|
26
|
+
*/
|
|
27
|
+
export declare function isValidLoopbackCycle(cycleEdges: WorkflowEdge[]): boolean;
|
|
5
28
|
/**
|
|
6
29
|
* Configurable port compatibility checker
|
|
7
30
|
*/
|
|
@@ -71,8 +94,31 @@ export declare function getConnectionSuggestions(nodeId: string, nodes: Workflow
|
|
|
71
94
|
}>;
|
|
72
95
|
/**
|
|
73
96
|
* Check if a workflow has any cycles (prevent infinite loops)
|
|
97
|
+
* Note: This function detects ALL cycles, including valid loopback cycles.
|
|
98
|
+
* Use `hasInvalidCycles` to check only for cycles that could cause infinite execution.
|
|
99
|
+
*
|
|
100
|
+
* @param nodes - Array of workflow nodes
|
|
101
|
+
* @param edges - Array of workflow edges
|
|
102
|
+
* @returns True if any cycle exists in the workflow
|
|
74
103
|
*/
|
|
75
104
|
export declare function hasCycles(nodes: WorkflowNode[], edges: WorkflowEdge[]): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Check if a workflow has any invalid cycles (non-loopback cycles).
|
|
107
|
+
* This excludes valid loopback cycles used for ForEach iteration.
|
|
108
|
+
* Only cycles that could cause infinite execution are detected.
|
|
109
|
+
*
|
|
110
|
+
* @param nodes - Array of workflow nodes
|
|
111
|
+
* @param edges - Array of workflow edges
|
|
112
|
+
* @returns True if any invalid (non-loopback) cycle exists
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* // A cycle through a loopback edge is valid (returns false)
|
|
117
|
+
* // A cycle through regular data edges is invalid (returns true)
|
|
118
|
+
* const hasInvalid = hasInvalidCycles(nodes, edges);
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export declare function hasInvalidCycles(nodes: WorkflowNode[], edges: WorkflowEdge[]): boolean;
|
|
76
122
|
/**
|
|
77
123
|
* Get the execution order for a workflow (topological sort)
|
|
78
124
|
*/
|
|
@@ -1,6 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Connection validation utilities for FlowDrop
|
|
3
3
|
*/
|
|
4
|
+
/**
|
|
5
|
+
* Loopback port name constant
|
|
6
|
+
* This is the standard input port name used for loop iteration triggers
|
|
7
|
+
*/
|
|
8
|
+
const LOOPBACK_PORT_NAME = "loop_back";
|
|
9
|
+
/**
|
|
10
|
+
* Determines if an edge is a loopback edge.
|
|
11
|
+
* Loopback edges target the special `loop_back` input port on ForEach nodes.
|
|
12
|
+
* These edges are used to trigger the next iteration in a loop construct.
|
|
13
|
+
*
|
|
14
|
+
* @param edge - The edge to check
|
|
15
|
+
* @returns True if the edge is a loopback edge
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```typescript
|
|
19
|
+
* const edge = { targetHandle: "foreach.1-input-loop_back", ... };
|
|
20
|
+
* const isLoop = isLoopbackEdge(edge); // true
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function isLoopbackEdge(edge) {
|
|
24
|
+
const targetHandle = edge.targetHandle ?? "";
|
|
25
|
+
return targetHandle.includes(`-input-${LOOPBACK_PORT_NAME}`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Checks if a cycle consists entirely of loopback edges.
|
|
29
|
+
* A valid loopback cycle only contains edges that target loop_back ports.
|
|
30
|
+
*
|
|
31
|
+
* @param cycleEdges - Array of edges that form a cycle
|
|
32
|
+
* @returns True if all edges in the cycle are loopback edges
|
|
33
|
+
*/
|
|
34
|
+
export function isValidLoopbackCycle(cycleEdges) {
|
|
35
|
+
return cycleEdges.every((edge) => isLoopbackEdge(edge));
|
|
36
|
+
}
|
|
4
37
|
/**
|
|
5
38
|
* Configurable port compatibility checker
|
|
6
39
|
*/
|
|
@@ -225,6 +258,12 @@ export function getConnectionSuggestions(nodeId, nodes, nodeTypes) {
|
|
|
225
258
|
}
|
|
226
259
|
/**
|
|
227
260
|
* Check if a workflow has any cycles (prevent infinite loops)
|
|
261
|
+
* Note: This function detects ALL cycles, including valid loopback cycles.
|
|
262
|
+
* Use `hasInvalidCycles` to check only for cycles that could cause infinite execution.
|
|
263
|
+
*
|
|
264
|
+
* @param nodes - Array of workflow nodes
|
|
265
|
+
* @param edges - Array of workflow edges
|
|
266
|
+
* @returns True if any cycle exists in the workflow
|
|
228
267
|
*/
|
|
229
268
|
export function hasCycles(nodes, edges) {
|
|
230
269
|
const visited = new Set();
|
|
@@ -254,6 +293,58 @@ export function hasCycles(nodes, edges) {
|
|
|
254
293
|
}
|
|
255
294
|
return false;
|
|
256
295
|
}
|
|
296
|
+
/**
|
|
297
|
+
* Check if a workflow has any invalid cycles (non-loopback cycles).
|
|
298
|
+
* This excludes valid loopback cycles used for ForEach iteration.
|
|
299
|
+
* Only cycles that could cause infinite execution are detected.
|
|
300
|
+
*
|
|
301
|
+
* @param nodes - Array of workflow nodes
|
|
302
|
+
* @param edges - Array of workflow edges
|
|
303
|
+
* @returns True if any invalid (non-loopback) cycle exists
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* ```typescript
|
|
307
|
+
* // A cycle through a loopback edge is valid (returns false)
|
|
308
|
+
* // A cycle through regular data edges is invalid (returns true)
|
|
309
|
+
* const hasInvalid = hasInvalidCycles(nodes, edges);
|
|
310
|
+
* ```
|
|
311
|
+
*/
|
|
312
|
+
export function hasInvalidCycles(nodes, edges) {
|
|
313
|
+
// Filter out loopback edges - these create valid cycles for loop iteration
|
|
314
|
+
const nonLoopbackEdges = edges.filter((edge) => !isLoopbackEdge(edge));
|
|
315
|
+
// Check for cycles using only non-loopback edges
|
|
316
|
+
const visited = new Set();
|
|
317
|
+
const recursionStack = new Set();
|
|
318
|
+
/**
|
|
319
|
+
* DFS utility to detect cycles in the graph
|
|
320
|
+
* @param nodeId - Current node being visited
|
|
321
|
+
* @returns True if a cycle is found from this node
|
|
322
|
+
*/
|
|
323
|
+
function hasCycleUtil(nodeId) {
|
|
324
|
+
if (recursionStack.has(nodeId))
|
|
325
|
+
return true;
|
|
326
|
+
if (visited.has(nodeId))
|
|
327
|
+
return false;
|
|
328
|
+
visited.add(nodeId);
|
|
329
|
+
recursionStack.add(nodeId);
|
|
330
|
+
// Get all outgoing non-loopback edges from this node
|
|
331
|
+
const outgoingEdges = nonLoopbackEdges.filter((e) => e.source === nodeId);
|
|
332
|
+
for (const edge of outgoingEdges) {
|
|
333
|
+
if (hasCycleUtil(edge.target))
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
recursionStack.delete(nodeId);
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
// Check each node for cycles
|
|
340
|
+
for (const node of nodes) {
|
|
341
|
+
if (!visited.has(node.id)) {
|
|
342
|
+
if (hasCycleUtil(node.id))
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
257
348
|
/**
|
|
258
349
|
* Get the execution order for a workflow (topological sort)
|
|
259
350
|
*/
|