@flowuent-org/diagramming-core 1.2.0 → 1.2.2

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.
@@ -149,6 +149,51 @@ export const automationDefaultNodes = [
149
149
  height: 150,
150
150
  measured: { width: 336, height: 150 },
151
151
  },
152
+ {
153
+ id: 'navigation-node',
154
+ type: 'AutomationNavigationNode',
155
+ position: { x: 600, y: 200 },
156
+ data: {
157
+ // Display data for the node UI
158
+ label: 'Navigate to Website',
159
+ description: 'Navigate to the target website and wait for page load',
160
+ status: 'Ready',
161
+ navigationType: 'navigate',
162
+ url: 'https://example.com',
163
+ lastRun: 'Never',
164
+ backgroundColor: '#181C25',
165
+ textColor: '#ffffff',
166
+ borderColor: '#1e293b',
167
+ iconName: 'Navigation',
168
+ // Form data for configuration
169
+ formData: {
170
+ nodeId: 'navigation-node',
171
+ title: 'Navigate to Website',
172
+ type: 'navigation',
173
+ navigationType: 'navigate',
174
+ url: 'https://example.com',
175
+ timeout: 30000,
176
+ retryCount: 3,
177
+ outputVariable: 'navigationResult',
178
+ errorHandling: {
179
+ onError: 'retry',
180
+ maxRetries: 3,
181
+ fallbackAction: 'skip',
182
+ },
183
+ isPinned: false,
184
+ isBlock: false,
185
+ blocks: [],
186
+ parallelChildrenCount: 0,
187
+ conditions: {
188
+ combinator: 'and',
189
+ rules: [],
190
+ },
191
+ },
192
+ },
193
+ width: 336,
194
+ height: 150,
195
+ measured: { width: 336, height: 150 },
196
+ },
152
197
  {
153
198
  id: 'ai-suggestion-node',
154
199
  type: 'AutomationAISuggestionNode',
@@ -504,8 +549,27 @@ export const automationDefaultEdges = [
504
549
  },
505
550
  },
506
551
  {
507
- id: 'edge-api-to-formatting',
552
+ id: 'edge-api-to-navigation',
508
553
  source: 'api-call-node',
554
+ target: 'navigation-node',
555
+ sourceHandle: 'right',
556
+ targetHandle: 'left',
557
+ data: {
558
+ label: '',
559
+ type: 'flow',
560
+ },
561
+ style: {
562
+ stroke: '#ffffff',
563
+ strokeWidth: 2,
564
+ },
565
+ markerEnd: {
566
+ type: MarkerType.ArrowClosed,
567
+ color: '#ffffff',
568
+ },
569
+ },
570
+ {
571
+ id: 'edge-navigation-to-formatting',
572
+ source: 'navigation-node',
509
573
  target: 'data-formatting-node',
510
574
  sourceHandle: 'right',
511
575
  targetHandle: 'left',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flowuent-org/diagramming-core",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "license": "MIT",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -0,0 +1,281 @@
1
+ import React, { useState, useEffect, useRef, useCallback } from 'react';
2
+ import { Box, TextField, IconButton, Typography, Paper } from '@mui/material';
3
+ import { RiCloseLine, RiArrowUpLine, RiArrowDownLine } from 'react-icons/ri';
4
+ import { Node } from '@xyflow/react';
5
+ import { useSearch } from '../contexts/SearchContext';
6
+
7
+ interface CanvasSearchBarProps {
8
+ nodes: Node[];
9
+ onClose: () => void;
10
+ onNodeSelect: (nodeId: string) => void;
11
+ onViewportChange: (nodeId: string) => void;
12
+ getViewport: () => { x: number; y: number; zoom: number };
13
+ setViewport: (viewport: { x: number; y: number; zoom: number }) => void;
14
+ }
15
+
16
+ export const CanvasSearchBar: React.FC<CanvasSearchBarProps> = ({
17
+ nodes,
18
+ onClose,
19
+ onNodeSelect,
20
+ onViewportChange,
21
+ getViewport,
22
+ setViewport,
23
+ }) => {
24
+ const { searchQuery, setSearchQuery } = useSearch();
25
+ const [currentMatchIndex, setCurrentMatchIndex] = useState(0);
26
+ const [matchingNodes, setMatchingNodes] = useState<Node[]>([]);
27
+ const inputRef = useRef<HTMLInputElement>(null);
28
+
29
+ // Focus input when component mounts
30
+ useEffect(() => {
31
+ inputRef.current?.focus();
32
+ }, []);
33
+
34
+ // Search nodes when query changes
35
+ useEffect(() => {
36
+ if (!searchQuery.trim()) {
37
+ setMatchingNodes([]);
38
+ setCurrentMatchIndex(0);
39
+ return;
40
+ }
41
+
42
+ const searchLower = searchQuery.toLowerCase().trim();
43
+ const matches = nodes.filter((node) => {
44
+ const label = (node.data as any)?.label?.toLowerCase() || '';
45
+ const id = node.id.toLowerCase();
46
+ const description = (node.data as any)?.description?.toLowerCase() || '';
47
+ return (
48
+ label.includes(searchLower) ||
49
+ id.includes(searchLower) ||
50
+ description.includes(searchLower)
51
+ );
52
+ });
53
+
54
+ setMatchingNodes(matches);
55
+ setCurrentMatchIndex(0);
56
+
57
+ // Navigate to first match if any
58
+ if (matches.length > 0) {
59
+ navigateToNode(matches[0].id);
60
+ }
61
+ }, [searchQuery, nodes]);
62
+
63
+ // Navigate to a specific node
64
+ const navigateToNode = useCallback(
65
+ (nodeId: string) => {
66
+ const node = nodes.find((n) => n.id === nodeId);
67
+ if (!node) return;
68
+
69
+ // Center view on the node
70
+ const viewport = getViewport();
71
+ setViewport({
72
+ x: -node.position.x + window.innerWidth / 2 - (node.width || 0) / 2,
73
+ y: -node.position.y + window.innerHeight / 2 - (node.height || 0) / 2,
74
+ zoom: viewport.zoom,
75
+ });
76
+
77
+ // Select the node
78
+ onNodeSelect(nodeId);
79
+ onViewportChange(nodeId);
80
+ },
81
+ [nodes, getViewport, setViewport, onNodeSelect, onViewportChange]
82
+ );
83
+
84
+ // Navigate to next match
85
+ const handleNextMatch = useCallback(() => {
86
+ if (matchingNodes.length === 0) return;
87
+ const nextIndex = (currentMatchIndex + 1) % matchingNodes.length;
88
+ setCurrentMatchIndex(nextIndex);
89
+ navigateToNode(matchingNodes[nextIndex].id);
90
+ }, [matchingNodes, currentMatchIndex, navigateToNode]);
91
+
92
+ // Navigate to previous match
93
+ const handlePreviousMatch = useCallback(() => {
94
+ if (matchingNodes.length === 0) return;
95
+ const prevIndex =
96
+ currentMatchIndex === 0 ? matchingNodes.length - 1 : currentMatchIndex - 1;
97
+ setCurrentMatchIndex(prevIndex);
98
+ navigateToNode(matchingNodes[prevIndex].id);
99
+ }, [matchingNodes, currentMatchIndex, navigateToNode]);
100
+
101
+ // Clear search query when closing
102
+ const handleClose = useCallback(() => {
103
+ setSearchQuery('');
104
+ onClose();
105
+ }, [setSearchQuery, onClose]);
106
+
107
+ // Handle keyboard shortcuts
108
+ useEffect(() => {
109
+ const handleKeyDown = (event: KeyboardEvent) => {
110
+ if (event.key === 'Escape') {
111
+ handleClose();
112
+ } else if (event.key === 'Enter') {
113
+ if (event.shiftKey) {
114
+ handlePreviousMatch();
115
+ } else {
116
+ handleNextMatch();
117
+ }
118
+ } else if (event.key === 'ArrowDown' && event.ctrlKey) {
119
+ event.preventDefault();
120
+ handleNextMatch();
121
+ } else if (event.key === 'ArrowUp' && event.ctrlKey) {
122
+ event.preventDefault();
123
+ handlePreviousMatch();
124
+ }
125
+ };
126
+
127
+ document.addEventListener('keydown', handleKeyDown);
128
+ return () => {
129
+ document.removeEventListener('keydown', handleKeyDown);
130
+ };
131
+ }, [handleClose, handleNextMatch, handlePreviousMatch]);
132
+
133
+ return (
134
+ <Paper
135
+ elevation={8}
136
+ sx={{
137
+ position: 'fixed',
138
+ top: 16,
139
+ left: '50%',
140
+ transform: 'translateX(-50%)',
141
+ zIndex: 10000,
142
+ display: 'flex',
143
+ alignItems: 'center',
144
+ gap: 1,
145
+ p: 1,
146
+ bgcolor: '#1F2937',
147
+ border: '1px solid #374151',
148
+ borderRadius: '8px',
149
+ minWidth: '400px',
150
+ boxShadow: '0 4px 20px rgba(0, 0, 0, 0.5)',
151
+ }}
152
+ >
153
+ <TextField
154
+ inputRef={inputRef}
155
+ placeholder="Search nodes..."
156
+ value={searchQuery}
157
+ onChange={(e) => setSearchQuery(e.target.value)}
158
+ size="small"
159
+ autoFocus
160
+ sx={{
161
+ flex: 1,
162
+ '& .MuiOutlinedInput-root': {
163
+ bgcolor: '#111827',
164
+ color: '#fff',
165
+ borderRadius: '6px',
166
+ '& fieldset': {
167
+ borderColor: '#374151',
168
+ },
169
+ '&:hover fieldset': {
170
+ borderColor: '#4B5563',
171
+ },
172
+ '&.Mui-focused fieldset': {
173
+ borderColor: '#3b82f6',
174
+ },
175
+ },
176
+ '& .MuiInputBase-input': {
177
+ color: '#fff',
178
+ fontSize: '14px',
179
+ '&::placeholder': {
180
+ color: '#9CA3AF',
181
+ opacity: 1,
182
+ },
183
+ },
184
+ }}
185
+ />
186
+
187
+ {/* Match count and navigation */}
188
+ {searchQuery.trim() && matchingNodes.length > 0 && (
189
+ <Box
190
+ sx={{
191
+ display: 'flex',
192
+ alignItems: 'center',
193
+ gap: 0.5,
194
+ px: 1,
195
+ borderRight: '1px solid #374151',
196
+ borderLeft: '1px solid #374151',
197
+ }}
198
+ >
199
+ <Typography
200
+ variant="caption"
201
+ sx={{
202
+ color: '#9CA3AF',
203
+ fontSize: '12px',
204
+ minWidth: '50px',
205
+ textAlign: 'center',
206
+ }}
207
+ >
208
+ {currentMatchIndex + 1}/{matchingNodes.length}
209
+ </Typography>
210
+ <IconButton
211
+ size="small"
212
+ onClick={handlePreviousMatch}
213
+ disabled={matchingNodes.length === 0}
214
+ sx={{
215
+ color: '#9CA3AF',
216
+ '&:hover': {
217
+ bgcolor: 'rgba(255, 255, 255, 0.1)',
218
+ color: '#fff',
219
+ },
220
+ '&:disabled': {
221
+ color: '#4B5563',
222
+ },
223
+ }}
224
+ title="Previous match (Shift+Enter or Ctrl+↑)"
225
+ >
226
+ <RiArrowUpLine size={16} />
227
+ </IconButton>
228
+ <IconButton
229
+ size="small"
230
+ onClick={handleNextMatch}
231
+ disabled={matchingNodes.length === 0}
232
+ sx={{
233
+ color: '#9CA3AF',
234
+ '&:hover': {
235
+ bgcolor: 'rgba(255, 255, 255, 0.1)',
236
+ color: '#fff',
237
+ },
238
+ '&:disabled': {
239
+ color: '#4B5563',
240
+ },
241
+ }}
242
+ title="Next match (Enter or Ctrl+↓)"
243
+ >
244
+ <RiArrowDownLine size={16} />
245
+ </IconButton>
246
+ </Box>
247
+ )}
248
+
249
+ {/* No results indicator */}
250
+ {searchQuery.trim() && matchingNodes.length === 0 && (
251
+ <Typography
252
+ variant="caption"
253
+ sx={{
254
+ color: '#EF4444',
255
+ fontSize: '12px',
256
+ px: 1,
257
+ }}
258
+ >
259
+ No matches
260
+ </Typography>
261
+ )}
262
+
263
+ {/* Close button */}
264
+ <IconButton
265
+ size="small"
266
+ onClick={handleClose}
267
+ sx={{
268
+ color: '#9CA3AF',
269
+ '&:hover': {
270
+ bgcolor: 'rgba(255, 255, 255, 0.1)',
271
+ color: '#fff',
272
+ },
273
+ }}
274
+ title="Close (Esc)"
275
+ >
276
+ <RiCloseLine size={18} />
277
+ </IconButton>
278
+ </Paper>
279
+ );
280
+ };
281
+
@@ -21,6 +21,7 @@ import { AISuggestion } from './AISuggestionsModal';
21
21
  import { AISuggestionsPanel } from './AISuggestionsPanel';
22
22
  import { NodeActionButtons } from './NodeActionButtons';
23
23
  import { showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
24
+ import { useSearch } from '../../contexts/SearchContext';
24
25
 
25
26
  interface AutomationApiNodeProps {
26
27
  data: {
@@ -60,6 +61,7 @@ interface AutomationApiNodeProps {
60
61
 
61
62
  export const AutomationApiNode: React.FC<AutomationApiNodeProps> = ({ data, selected }) => {
62
63
  const { t } = useTranslation();
64
+ const { highlightText } = useSearch();
63
65
  const [isJsonOpen, setIsJsonOpen] = useState(false);
64
66
  const [showSuggestions, setShowSuggestions] = useState(false);
65
67
  const rootRef = useRef<any>(null);
@@ -346,7 +348,7 @@ export const AutomationApiNode: React.FC<AutomationApiNodeProps> = ({ data, sele
346
348
  <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
347
349
  </Box>
348
350
  <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
349
- {data.label}
351
+ {highlightText(data.label)}
350
352
  </Typography>
351
353
  </Box>
352
354
  <Chip
@@ -399,7 +401,7 @@ export const AutomationApiNode: React.FC<AutomationApiNodeProps> = ({ data, sele
399
401
  lineHeight: 1.4,
400
402
  margin: 0
401
403
  }}>
402
- {data.description}
404
+ {highlightText(data.description)}
403
405
  </Typography>
404
406
  </Box>
405
407
  </Box>
@@ -12,6 +12,7 @@ import { AISuggestion } from './AISuggestionsModal';
12
12
  import { AISuggestionsPanel } from './AISuggestionsPanel';
13
13
  import { NodeActionButtons } from './NodeActionButtons';
14
14
  import { showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
15
+ import { useSearch } from '../../contexts/SearchContext';
15
16
 
16
17
  interface AutomationEndNodeProps {
17
18
  data: {
@@ -40,6 +41,7 @@ interface AutomationEndNodeProps {
40
41
 
41
42
  export const AutomationEndNode: React.FC<AutomationEndNodeProps> = ({ data, selected }) => {
42
43
  const { t } = useTranslation();
44
+ const { highlightText } = useSearch();
43
45
  const [isJsonOpen, setIsJsonOpen] = useState(false);
44
46
  const [showSuggestions, setShowSuggestions] = useState(false);
45
47
  const rootRef = useRef<any>(null);
@@ -275,7 +277,7 @@ export const AutomationEndNode: React.FC<AutomationEndNodeProps> = ({ data, sele
275
277
  <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
276
278
  </Box>
277
279
  <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
278
- {data.label}
280
+ {highlightText(data.label)}
279
281
  </Typography>
280
282
  </Box>
281
283
  <Chip
@@ -21,6 +21,7 @@ import { AISuggestion } from './AISuggestionsModal';
21
21
  import { AISuggestionsPanel } from './AISuggestionsPanel';
22
22
  import { NodeActionButtons } from './NodeActionButtons';
23
23
  import { showNodeAIAssistantPopup } from './NodeAIAssistantPopup';
24
+ import { useSearch } from '../../contexts/SearchContext';
24
25
 
25
26
  interface AutomationFormattingNodeProps {
26
27
  data: {
@@ -65,6 +66,7 @@ interface AutomationFormattingNodeProps {
65
66
 
66
67
  export const AutomationFormattingNode: React.FC<AutomationFormattingNodeProps> = ({ data, selected }) => {
67
68
  const { t } = useTranslation();
69
+ const { highlightText } = useSearch();
68
70
  const [isJsonOpen, setIsJsonOpen] = useState(false);
69
71
  const [showSuggestions, setShowSuggestions] = useState(false);
70
72
  const rootRef = useRef<any>(null);
@@ -349,7 +351,7 @@ export const AutomationFormattingNode: React.FC<AutomationFormattingNodeProps> =
349
351
  <IconComponent sx={{ color: 'white', fontSize: '18px' }} />
350
352
  </Box>
351
353
  <Typography variant="h6" sx={{ fontWeight: 600, fontSize: '16px' }}>
352
- {data.label}
354
+ {highlightText(data.label)}
353
355
  </Typography>
354
356
  </Box>
355
357
  <Chip