Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/GraphWorkspace.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import React from 'react';
import ZoomComp from './component/ZoomSetter';
import ConfirmModal from './component/modals/ConfirmModal';
import SearchPanel from './component/SearchPanel';
import { actionType as T } from './reducer';
import './graphWorkspace.css';
// import localStorageManager from './graph-builder/local-storage-manager';
Expand Down Expand Up @@ -59,7 +60,7 @@ const GraphComp = (props) => {
}}
>
<TabBar superState={superState} dispatcher={dispatcher} />
<div style={{ flex: 1 }} className="graph-container" ref={graphContainerRef}>
<div style={{ flex: 1, position: 'relative' }} className="graph-container" ref={graphContainerRef}>
{superState.graphs.map((el, i) => (
<Graph
el={el}
Expand All @@ -78,6 +79,7 @@ const GraphComp = (props) => {
authorName={el.authorName}
/>
))}
<SearchPanel superState={superState} dispatcher={dispatcher} />
<ZoomComp dispatcher={dispatcher} superState={superState} />
</div>
<ConfirmModal
Expand Down
83 changes: 83 additions & 0 deletions src/component/SearchPanel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import React, { useEffect, useRef } from 'react';
import { actionType as T } from '../reducer';
import './searchPanel.css';

const SearchPanel = ({ superState, dispatcher }) => {
const inputRef = useRef();
const { searchPanel, searchQuery, searchResults, searchIndex, curGraphInstance } = superState;

useEffect(() => {
if (searchPanel && inputRef.current) inputRef.current.focus();
}, [searchPanel]);

useEffect(() => {
if (searchPanel && curGraphInstance && searchQuery) {
const results = curGraphInstance.searchElements(searchQuery);
dispatcher({ type: T.SET_SEARCH_RESULTS, payload: results });
dispatcher({ type: T.SET_SEARCH_INDEX, payload: 0 });
if (results.length > 0) curGraphInstance.flyToElement(results[0]);
}
}, [curGraphInstance]);

const runSearch = (query) => {
if (!curGraphInstance) return;
const results = curGraphInstance.searchElements(query);
dispatcher({ type: T.SET_SEARCH_RESULTS, payload: results });
dispatcher({ type: T.SET_SEARCH_INDEX, payload: 0 });
if (results.length > 0) curGraphInstance.flyToElement(results[0]);
};

const handleChange = (e) => {
const q = e.target.value;
dispatcher({ type: T.SET_SEARCH_QUERY, payload: q });
runSearch(q);
};

const step = (dir) => {
if (!searchResults.length) return;
const next = (searchIndex + dir + searchResults.length) % searchResults.length;
dispatcher({ type: T.SET_SEARCH_INDEX, payload: next });
curGraphInstance.flyToElement(searchResults[next]);
};

const close = () => {
if (curGraphInstance) curGraphInstance.clearSearch();
dispatcher({ type: T.SET_SEARCH_PANEL, payload: false });
dispatcher({ type: T.SET_SEARCH_QUERY, payload: '' });
dispatcher({ type: T.SET_SEARCH_RESULTS, payload: [] });
dispatcher({ type: T.SET_SEARCH_INDEX, payload: 0 });
};

const handleKeyDown = (e) => {
if (e.key === 'Escape') { close(); return; }
if (e.key === 'Enter') {
e.preventDefault();
step(e.shiftKey ? -1 : 1);
}
};

if (!searchPanel) return null;

const total = searchResults.length;
const current = total > 0 ? searchIndex + 1 : 0;
const hasQuery = searchQuery.trim().length > 0;

return (
<div className="search-panel">
<input
ref={inputRef}
type="text"
placeholder="Find node / edge…"
value={searchQuery}
onChange={handleChange}
onKeyDown={handleKeyDown}
/>
<span className="search-counter">{hasQuery ? (total > 0 ? `${current} of ${total}` : 'No results') : ''}</span>
<button type="button" onClick={() => step(-1)} disabled={total < 2} title="Previous (Shift+Enter)">▲</button>
<button type="button" onClick={() => step(1)} disabled={total < 2} title="Next (Enter)">▼</button>
<button type="button" onClick={close} title="Close (Esc)">✕</button>
</div>
);
};

export default SearchPanel;
52 changes: 52 additions & 0 deletions src/component/searchPanel.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.search-panel {
position: absolute;
top: 12px;
right: 16px;
z-index: 10001;
display: flex;
align-items: center;
gap: 4px;
background: var(--bg-secondary, #fff);
border: 1px solid var(--border-primary, #ccc);
border-radius: 4px;
padding: 4px 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);
}

.search-panel input {
border: none;
outline: none;
font-size: 13px;
width: 180px;
background: transparent;
color: var(--text-primary, #212529);
padding: 2px 4px;
}

.search-counter {
font-size: 12px;
color: var(--text-primary, #555);
min-width: 42px;
text-align: center;
white-space: nowrap;
}

.search-panel button {
background: none;
border: none;
cursor: pointer;
padding: 2px 5px;
font-size: 13px;
color: var(--text-primary, #555);
border-radius: 3px;
line-height: 1;
}

.search-panel button:hover {
background: var(--bg-primary, #eee);
}

.search-panel button:disabled {
opacity: 0.35;
cursor: default;
}
14 changes: 14 additions & 0 deletions src/config/cytoscape-style.js
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,20 @@ const getCytoscapeStyle = (darkMode = false) => {
'border-width': darkMode ? 3 : 'data(style.borderWidth)',
},
},
{
selector: '.search-match',
style: {
overlayColor: '#f5a623',
overlayOpacity: 0.45,
overlayPadding: 4,
},
},
{
selector: '.search-dim',
style: {
opacity: 0.2,
},
},

];
};
Expand Down
29 changes: 29 additions & 0 deletions src/graph-builder/tailored-graph-builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,35 @@ class TailoredGraph extends CoreGraph {
return c1.edgesWith(c2);
}

searchElements(query) {
this.clearSearch();
if (!query || !query.trim()) return [];

const q = query.trim().toLowerCase();
const all = this.cy.$('node[type="ordin"], edge[type="ordin"]');
const matches = all.filter((ele) => {
const label = (ele.data('label') || '').toLowerCase();
return label.includes(q);
});
const nonMatches = all.not(matches);

matches.addClass('search-match');
nonMatches.addClass('search-dim');

return matches.map((ele) => ele.id());
}

clearSearch() {
this.cy.$('.search-match').removeClass('search-match');
this.cy.$('.search-dim').removeClass('search-dim');
}

flyToElement(id) {
const ele = this.getById(id);
if (!ele || !ele.length) return;
this.cy.animate({ center: { eles: ele }, zoom: this.cy.zoom() }, { duration: 250 });
}

getNodesEdges() {
const nodes = this.cy.$('node[type="ordin"]').map((node) => ({
label: node.data('label'),
Expand Down
4 changes: 4 additions & 0 deletions src/reducer/actionType.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ const actionType = {
TOGGLE_DARK_MODE: 'TOGGLE_DARK_MODE',
SET_CLIPBOARD: 'SET_CLIPBOARD',
SET_CONFIRM_MODAL: 'SET_CONFIRM_MODAL',
SET_SEARCH_PANEL: 'SET_SEARCH_PANEL',
SET_SEARCH_QUERY: 'SET_SEARCH_QUERY',
SET_SEARCH_RESULTS: 'SET_SEARCH_RESULTS',
SET_SEARCH_INDEX: 'SET_SEARCH_INDEX',
};

export default zealit(actionType);
4 changes: 4 additions & 0 deletions src/reducer/initialState.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ const initialState = {
darkMode: false,
clipboard: [],
confirmModal: { open: false, message: '', onConfirm: null },
searchPanel: false,
searchQuery: '',
searchResults: [],
searchIndex: 0,
};

const initialGraphState = {
Expand Down
13 changes: 13 additions & 0 deletions src/reducer/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,19 @@ const reducer = (state, action) => {
return { ...state, clipboard: action.payload };
}

case T.SET_SEARCH_PANEL: {
return { ...state, searchPanel: action.payload };
}
case T.SET_SEARCH_QUERY: {
return { ...state, searchQuery: action.payload };
}
case T.SET_SEARCH_RESULTS: {
return { ...state, searchResults: action.payload };
}
case T.SET_SEARCH_INDEX: {
return { ...state, searchIndex: action.payload };
}

default:
return state;
}
Expand Down
6 changes: 5 additions & 1 deletion src/toolbarActions/toolbarFunctions.js
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,10 @@ const viewHistory = (state, setState) => {
setState({ type: T.SET_HISTORY_MODAL, payload: true });
};

const openSearchPanel = (state, setState) => {
setState({ type: T.SET_SEARCH_PANEL, payload: true });
};

const toggleServer = (state, dispatcher) => {
if (state.isWorkflowOnServer) {
dispatcher({ type: T.IS_WORKFLOW_ON_SERVER, payload: false });
Expand All @@ -214,5 +218,5 @@ export {
createFile, readFile, readTextFile, newProject, clearAll, editDetails, undo, redo,
openShareModal, openSettingModal, viewHistory, resetAfterClear, toggleLogs,
copySelected, pasteClipboard,
toggleServer, optionModalToggle, contribute,
toggleServer, optionModalToggle, contribute, openSearchPanel,
};
13 changes: 11 additions & 2 deletions src/toolbarActions/toolbarList.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import {
FaSave, FaUndo, FaRedo, FaTrash, FaFileImport, FaPlus, FaDownload, FaEdit, FaRegTimesCircle, FaHistory,
FaHammer, FaBug, FaBomb, FaToggleOn, FaThermometerEmpty, FaTrashRestore, FaCogs, FaPencilAlt, FaTerminal,
FaCopy, FaPaste,
FaCopy, FaPaste, FaSearch,
} from 'react-icons/fa';

import {
Expand All @@ -13,7 +13,7 @@ import {
import {
createNode, editElement, deleteElem, downloadImg, saveAction, saveGraphMLFile,
createFile, readFile, clearAll, undo, redo, viewHistory, resetAfterClear,
toggleServer, optionModalToggle, toggleLogs, contribute, copySelected, pasteClipboard,
toggleServer, optionModalToggle, toggleLogs, contribute, copySelected, pasteClipboard, openSearchPanel,
// openSettingModal,
} from './toolbarFunctions';

Expand Down Expand Up @@ -145,6 +145,15 @@ const toolbarList = (state, dispatcher) => [
active: state.curGraphInstance,
visibility: true,
},
{
type: 'action',
text: 'Search',
icon: FaSearch,
action: openSearchPanel,
active: state.curGraphInstance,
visibility: true,
hotkey: 'Ctrl+F',
},
{ type: 'vsep' },
// server buttons
{
Expand Down