Skip to content

Commit 8f09613

Browse files
authored
Merge pull request #357 from avinxshKD/feat/search-panel
feat: add Ctrl+F search panel for finding nodes and edges
2 parents ebb9229 + 9f0d7d2 commit 8f09613

10 files changed

Lines changed: 218 additions & 4 deletions

File tree

src/GraphWorkspace.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React from 'react';
22
import ZoomComp from './component/ZoomSetter';
33
import ConfirmModal from './component/modals/ConfirmModal';
4+
import SearchPanel from './component/SearchPanel';
45
import { actionType as T } from './reducer';
56
import './graphWorkspace.css';
67
// import localStorageManager from './graph-builder/local-storage-manager';
@@ -59,7 +60,7 @@ const GraphComp = (props) => {
5960
}}
6061
>
6162
<TabBar superState={superState} dispatcher={dispatcher} />
62-
<div style={{ flex: 1 }} className="graph-container" ref={graphContainerRef}>
63+
<div style={{ flex: 1, position: 'relative' }} className="graph-container" ref={graphContainerRef}>
6364
{superState.graphs.map((el, i) => (
6465
<Graph
6566
el={el}
@@ -78,6 +79,7 @@ const GraphComp = (props) => {
7879
authorName={el.authorName}
7980
/>
8081
))}
82+
<SearchPanel superState={superState} dispatcher={dispatcher} />
8183
<ZoomComp dispatcher={dispatcher} superState={superState} />
8284
</div>
8385
<ConfirmModal

src/component/SearchPanel.jsx

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import React, { useEffect, useRef } from 'react';
2+
import { actionType as T } from '../reducer';
3+
import './searchPanel.css';
4+
5+
const SearchPanel = ({ superState, dispatcher }) => {
6+
const inputRef = useRef();
7+
const { searchPanel, searchQuery, searchResults, searchIndex, curGraphInstance } = superState;
8+
9+
useEffect(() => {
10+
if (searchPanel && inputRef.current) inputRef.current.focus();
11+
}, [searchPanel]);
12+
13+
useEffect(() => {
14+
if (searchPanel && curGraphInstance && searchQuery) {
15+
const results = curGraphInstance.searchElements(searchQuery);
16+
dispatcher({ type: T.SET_SEARCH_RESULTS, payload: results });
17+
dispatcher({ type: T.SET_SEARCH_INDEX, payload: 0 });
18+
if (results.length > 0) curGraphInstance.flyToElement(results[0]);
19+
}
20+
}, [curGraphInstance]);
21+
22+
const runSearch = (query) => {
23+
if (!curGraphInstance) return;
24+
const results = curGraphInstance.searchElements(query);
25+
dispatcher({ type: T.SET_SEARCH_RESULTS, payload: results });
26+
dispatcher({ type: T.SET_SEARCH_INDEX, payload: 0 });
27+
if (results.length > 0) curGraphInstance.flyToElement(results[0]);
28+
};
29+
30+
const handleChange = (e) => {
31+
const q = e.target.value;
32+
dispatcher({ type: T.SET_SEARCH_QUERY, payload: q });
33+
runSearch(q);
34+
};
35+
36+
const step = (dir) => {
37+
if (!searchResults.length) return;
38+
const next = (searchIndex + dir + searchResults.length) % searchResults.length;
39+
dispatcher({ type: T.SET_SEARCH_INDEX, payload: next });
40+
curGraphInstance.flyToElement(searchResults[next]);
41+
};
42+
43+
const close = () => {
44+
if (curGraphInstance) curGraphInstance.clearSearch();
45+
dispatcher({ type: T.SET_SEARCH_PANEL, payload: false });
46+
dispatcher({ type: T.SET_SEARCH_QUERY, payload: '' });
47+
dispatcher({ type: T.SET_SEARCH_RESULTS, payload: [] });
48+
dispatcher({ type: T.SET_SEARCH_INDEX, payload: 0 });
49+
};
50+
51+
const handleKeyDown = (e) => {
52+
if (e.key === 'Escape') { close(); return; }
53+
if (e.key === 'Enter') {
54+
e.preventDefault();
55+
step(e.shiftKey ? -1 : 1);
56+
}
57+
};
58+
59+
if (!searchPanel) return null;
60+
61+
const total = searchResults.length;
62+
const current = total > 0 ? searchIndex + 1 : 0;
63+
const hasQuery = searchQuery.trim().length > 0;
64+
65+
return (
66+
<div className="search-panel">
67+
<input
68+
ref={inputRef}
69+
type="text"
70+
placeholder="Find node / edge…"
71+
value={searchQuery}
72+
onChange={handleChange}
73+
onKeyDown={handleKeyDown}
74+
/>
75+
<span className="search-counter">{hasQuery ? (total > 0 ? `${current} of ${total}` : 'No results') : ''}</span>
76+
<button type="button" onClick={() => step(-1)} disabled={total < 2} title="Previous (Shift+Enter)"></button>
77+
<button type="button" onClick={() => step(1)} disabled={total < 2} title="Next (Enter)"></button>
78+
<button type="button" onClick={close} title="Close (Esc)"></button>
79+
</div>
80+
);
81+
};
82+
83+
export default SearchPanel;

src/component/searchPanel.css

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
.search-panel {
2+
position: absolute;
3+
top: 12px;
4+
right: 16px;
5+
z-index: 10001;
6+
display: flex;
7+
align-items: center;
8+
gap: 4px;
9+
background: var(--bg-secondary, #fff);
10+
border: 1px solid var(--border-primary, #ccc);
11+
border-radius: 4px;
12+
padding: 4px 6px;
13+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
14+
}
15+
16+
.search-panel input {
17+
border: none;
18+
outline: none;
19+
font-size: 13px;
20+
width: 180px;
21+
background: transparent;
22+
color: var(--text-primary, #212529);
23+
padding: 2px 4px;
24+
}
25+
26+
.search-counter {
27+
font-size: 12px;
28+
color: var(--text-primary, #555);
29+
min-width: 42px;
30+
text-align: center;
31+
white-space: nowrap;
32+
}
33+
34+
.search-panel button {
35+
background: none;
36+
border: none;
37+
cursor: pointer;
38+
padding: 2px 5px;
39+
font-size: 13px;
40+
color: var(--text-primary, #555);
41+
border-radius: 3px;
42+
line-height: 1;
43+
}
44+
45+
.search-panel button:hover {
46+
background: var(--bg-primary, #eee);
47+
}
48+
49+
.search-panel button:disabled {
50+
opacity: 0.35;
51+
cursor: default;
52+
}

src/config/cytoscape-style.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,20 @@ const getCytoscapeStyle = (darkMode = false) => {
218218
'border-width': darkMode ? 3 : 'data(style.borderWidth)',
219219
},
220220
},
221+
{
222+
selector: '.search-match',
223+
style: {
224+
overlayColor: '#f5a623',
225+
overlayOpacity: 0.45,
226+
overlayPadding: 4,
227+
},
228+
},
229+
{
230+
selector: '.search-dim',
231+
style: {
232+
opacity: 0.2,
233+
},
234+
},
221235

222236
];
223237
};

src/graph-builder/tailored-graph-builder.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,35 @@ class TailoredGraph extends CoreGraph {
195195
return c1.edgesWith(c2);
196196
}
197197

198+
searchElements(query) {
199+
this.clearSearch();
200+
if (!query || !query.trim()) return [];
201+
202+
const q = query.trim().toLowerCase();
203+
const all = this.cy.$('node[type="ordin"], edge[type="ordin"]');
204+
const matches = all.filter((ele) => {
205+
const label = (ele.data('label') || '').toLowerCase();
206+
return label.includes(q);
207+
});
208+
const nonMatches = all.not(matches);
209+
210+
matches.addClass('search-match');
211+
nonMatches.addClass('search-dim');
212+
213+
return matches.map((ele) => ele.id());
214+
}
215+
216+
clearSearch() {
217+
this.cy.$('.search-match').removeClass('search-match');
218+
this.cy.$('.search-dim').removeClass('search-dim');
219+
}
220+
221+
flyToElement(id) {
222+
const ele = this.getById(id);
223+
if (!ele || !ele.length) return;
224+
this.cy.animate({ center: { eles: ele }, zoom: this.cy.zoom() }, { duration: 250 });
225+
}
226+
198227
getNodesEdges() {
199228
const nodes = this.cy.$('node[type="ordin"]').map((node) => ({
200229
label: node.data('label'),

src/reducer/actionType.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,10 @@ const actionType = {
4848
TOGGLE_DARK_MODE: 'TOGGLE_DARK_MODE',
4949
SET_CLIPBOARD: 'SET_CLIPBOARD',
5050
SET_CONFIRM_MODAL: 'SET_CONFIRM_MODAL',
51+
SET_SEARCH_PANEL: 'SET_SEARCH_PANEL',
52+
SET_SEARCH_QUERY: 'SET_SEARCH_QUERY',
53+
SET_SEARCH_RESULTS: 'SET_SEARCH_RESULTS',
54+
SET_SEARCH_INDEX: 'SET_SEARCH_INDEX',
5155
};
5256

5357
export default zealit(actionType);

src/reducer/initialState.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ const initialState = {
4141
darkMode: false,
4242
clipboard: [],
4343
confirmModal: { open: false, message: '', onConfirm: null },
44+
searchPanel: false,
45+
searchQuery: '',
46+
searchResults: [],
47+
searchIndex: 0,
4448
};
4549

4650
const initialGraphState = {

src/reducer/reducer.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,19 @@ const reducer = (state, action) => {
265265
return { ...state, clipboard: action.payload };
266266
}
267267

268+
case T.SET_SEARCH_PANEL: {
269+
return { ...state, searchPanel: action.payload };
270+
}
271+
case T.SET_SEARCH_QUERY: {
272+
return { ...state, searchQuery: action.payload };
273+
}
274+
case T.SET_SEARCH_RESULTS: {
275+
return { ...state, searchResults: action.payload };
276+
}
277+
case T.SET_SEARCH_INDEX: {
278+
return { ...state, searchIndex: action.payload };
279+
}
280+
268281
default:
269282
return state;
270283
}

src/toolbarActions/toolbarFunctions.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,10 @@ const viewHistory = (state, setState) => {
201201
setState({ type: T.SET_HISTORY_MODAL, payload: true });
202202
};
203203

204+
const openSearchPanel = (state, setState) => {
205+
setState({ type: T.SET_SEARCH_PANEL, payload: true });
206+
};
207+
204208
const toggleServer = (state, dispatcher) => {
205209
if (state.isWorkflowOnServer) {
206210
dispatcher({ type: T.IS_WORKFLOW_ON_SERVER, payload: false });
@@ -214,5 +218,5 @@ export {
214218
createFile, readFile, readTextFile, newProject, clearAll, editDetails, undo, redo,
215219
openShareModal, openSettingModal, viewHistory, resetAfterClear, toggleLogs,
216220
copySelected, pasteClipboard,
217-
toggleServer, optionModalToggle, contribute,
221+
toggleServer, optionModalToggle, contribute, openSearchPanel,
218222
};

src/toolbarActions/toolbarList.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import {
33
FaSave, FaUndo, FaRedo, FaTrash, FaFileImport, FaPlus, FaDownload, FaEdit, FaRegTimesCircle, FaHistory,
44
FaHammer, FaBug, FaBomb, FaToggleOn, FaThermometerEmpty, FaTrashRestore, FaCogs, FaPencilAlt, FaTerminal,
5-
FaCopy, FaPaste,
5+
FaCopy, FaPaste, FaSearch,
66
} from 'react-icons/fa';
77

88
import {
@@ -13,7 +13,7 @@ import {
1313
import {
1414
createNode, editElement, deleteElem, downloadImg, saveAction, saveGraphMLFile,
1515
createFile, readFile, clearAll, undo, redo, viewHistory, resetAfterClear,
16-
toggleServer, optionModalToggle, toggleLogs, contribute, copySelected, pasteClipboard,
16+
toggleServer, optionModalToggle, toggleLogs, contribute, copySelected, pasteClipboard, openSearchPanel,
1717
// openSettingModal,
1818
} from './toolbarFunctions';
1919

@@ -145,6 +145,15 @@ const toolbarList = (state, dispatcher) => [
145145
active: state.curGraphInstance,
146146
visibility: true,
147147
},
148+
{
149+
type: 'action',
150+
text: 'Search',
151+
icon: FaSearch,
152+
action: openSearchPanel,
153+
active: state.curGraphInstance,
154+
visibility: true,
155+
hotkey: 'Ctrl+F',
156+
},
148157
{ type: 'vsep' },
149158
// server buttons
150159
{

0 commit comments

Comments
 (0)