Skip to content

Commit 1a6f475

Browse files
committed
Rewrite navigation stack managment
1 parent 36cc8f1 commit 1a6f475

File tree

9 files changed

+277
-257
lines changed

9 files changed

+277
-257
lines changed

src/components/GraphViewport.tsx

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
11
import Box from '@mui/material/Box';
22
import IconButton from '@mui/material/IconButton';
33
import Stack from '@mui/material/Stack';
4+
import { GraphQLNamedType } from 'graphql';
45
import { Component, createRef } from 'react';
56

67
import { renderSvg } from '../graph/svg-renderer.ts';
78
import { TypeGraph } from '../graph/type-graph.ts';
89
import { Viewport } from '../graph/viewport.ts';
10+
import { extractTypeName, typeObjToId } from '../introspection/utils.ts';
911
import ZoomInIcon from './icons/zoom-in.svg';
1012
import ZoomOutIcon from './icons/zoom-out.svg';
1113
import ZoomResetIcon from './icons/zoom-reset.svg';
1214
import LoadingAnimation from './utils/LoadingAnimation.tsx';
13-
import { GraphSelection } from './Voyager.tsx';
15+
import { type NavStack } from './Voyager.tsx';
1416

1517
interface GraphViewportProps {
16-
typeGraph: TypeGraph | null;
17-
18-
selectedTypeID: string | null;
19-
selectedEdgeID: string | null;
20-
21-
onSelect: (selection: GraphSelection) => void;
18+
navStack: NavStack | null;
19+
onSelectNode: (type: GraphQLNamedType | null) => void;
20+
onSelectEdge: (
21+
edgeID: string,
22+
fromType: GraphQLNamedType,
23+
toType: GraphQLNamedType,
24+
) => void;
2225
}
2326

2427
interface GraphViewportState {
25-
typeGraph: TypeGraph | null;
28+
typeGraph: TypeGraph | null | undefined;
2629
svgViewport: Viewport | null;
2730
}
2831

@@ -41,7 +44,7 @@ export default class GraphViewport extends Component<
4144
props: GraphViewportProps,
4245
state: GraphViewportState,
4346
): GraphViewportState | null {
44-
const { typeGraph } = props;
47+
const typeGraph = props.navStack?.typeGraph;
4548

4649
if (typeGraph !== state.typeGraph) {
4750
return { typeGraph, svgViewport: null };
@@ -51,29 +54,34 @@ export default class GraphViewport extends Component<
5154
}
5255

5356
componentDidMount() {
54-
this._renderSvgAsync(this.props.typeGraph);
57+
this._renderSvgAsync(this.props.navStack?.typeGraph);
5558
}
5659

5760
componentDidUpdate(
5861
prevProps: GraphViewportProps,
5962
prevState: GraphViewportState,
6063
) {
64+
const navStack = this.props.navStack;
65+
const prevNavStack = prevProps.navStack;
6166
const { svgViewport } = this.state;
6267

6368
if (svgViewport == null) {
64-
this._renderSvgAsync(this.props.typeGraph);
69+
this._renderSvgAsync(navStack?.typeGraph);
6570
return;
6671
}
6772

6873
const isJustRendered = prevState.svgViewport == null;
69-
const { selectedTypeID, selectedEdgeID } = this.props;
7074

71-
if (prevProps.selectedTypeID !== selectedTypeID || isJustRendered) {
72-
svgViewport.selectNodeById(selectedTypeID);
75+
if (prevNavStack?.type !== navStack?.type || isJustRendered) {
76+
const nodeId = navStack?.type == null ? null : typeObjToId(navStack.type);
77+
svgViewport.selectNodeById(nodeId);
7378
}
7479

75-
if (prevProps.selectedEdgeID !== selectedEdgeID || isJustRendered) {
76-
svgViewport.selectEdgeById(selectedEdgeID);
80+
if (
81+
prevNavStack?.selectedEdgeID !== navStack?.selectedEdgeID ||
82+
isJustRendered
83+
) {
84+
svgViewport.selectEdgeById(navStack?.selectedEdgeID);
7785
}
7886
}
7987

@@ -82,7 +90,7 @@ export default class GraphViewport extends Component<
8290
this._cleanupSvgViewport();
8391
}
8492

85-
_renderSvgAsync(typeGraph: TypeGraph | null) {
93+
_renderSvgAsync(typeGraph: TypeGraph | null | undefined) {
8694
if (typeGraph == null) {
8795
return; // Nothing to render
8896
}
@@ -93,7 +101,7 @@ export default class GraphViewport extends Component<
93101

94102
this._currentTypeGraph = typeGraph;
95103

96-
const { onSelect } = this.props;
104+
const { onSelectNode, onSelectEdge } = this.props;
97105
renderSvg(typeGraph)
98106
.then((svg) => {
99107
if (typeGraph !== this._currentTypeGraph) {
@@ -104,7 +112,22 @@ export default class GraphViewport extends Component<
104112
const svgViewport = new Viewport(
105113
svg,
106114
this._containerRef.current!,
107-
onSelect,
115+
(nodeId: string | null) => {
116+
if (nodeId == null) {
117+
return onSelectNode(null);
118+
}
119+
const type = typeGraph.nodes.get(extractTypeName(nodeId));
120+
if (type != null) {
121+
onSelectNode(type);
122+
}
123+
},
124+
(edgeID: string, toID: string) => {
125+
const fromType = typeGraph.nodes.get(extractTypeName(edgeID));
126+
const toType = typeGraph.nodes.get(extractTypeName(toID));
127+
if (fromType != null && toType != null) {
128+
onSelectEdge(edgeID, fromType, toType);
129+
}
130+
},
108131
);
109132
this.setState({ svgViewport });
110133
})
@@ -178,10 +201,10 @@ export default class GraphViewport extends Component<
178201
);
179202
}
180203

181-
focusNode(id: string) {
204+
focusNode(type: GraphQLNamedType): void {
182205
const { svgViewport } = this.state;
183206
if (svgViewport) {
184-
svgViewport.focusElement(id);
207+
svgViewport.focusElement(typeObjToId(type));
185208
}
186209
}
187210

src/components/Voyager.tsx

Lines changed: 109 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,19 @@ import Button from '@mui/material/Button';
66
import Stack from '@mui/material/Stack';
77
import { ThemeProvider } from '@mui/material/styles';
88
import { ExecutionResult } from 'graphql/execution';
9-
import { GraphQLSchema } from 'graphql/type';
9+
import { GraphQLNamedType, GraphQLSchema } from 'graphql/type';
1010
import { buildClientSchema, IntrospectionQuery } from 'graphql/utilities';
1111
import {
1212
Children,
1313
type ReactNode,
14+
useCallback,
1415
useEffect,
1516
useMemo,
1617
useRef,
1718
useState,
1819
} from 'react';
1920

20-
import { getTypeGraph } from '../graph/type-graph.ts';
21+
import { getTypeGraph, TypeGraph } from '../graph/type-graph.ts';
2122
import { getSchema } from '../introspection/introspection.ts';
2223
import { MaybePromise, usePromise } from '../utils/usePromise.ts';
2324
import DocExplorer from './doc-explorer/DocExplorer.tsx';
@@ -51,11 +52,26 @@ export interface VoyagerProps {
5152
children?: ReactNode;
5253
}
5354

54-
export type GraphSelection =
55-
| { typeID: null; edgeID: null }
56-
| { typeID: string; edgeID: string | null };
55+
interface NavStackTypeList {
56+
prev: null;
57+
typeGraph: TypeGraph;
58+
type: null;
59+
selectedEdgeID: null;
60+
searchValue: string | null;
61+
}
62+
63+
interface NavStackType {
64+
prev: NavStack;
65+
typeGraph: TypeGraph;
66+
type: GraphQLNamedType;
67+
selectedEdgeID: string | null;
68+
searchValue: string | null;
69+
}
70+
71+
export type NavStack = NavStackTypeList | NavStackType;
5772

5873
export default function Voyager(props: VoyagerProps) {
74+
console.log('Voyager');
5975
const initialDisplayOptions = useMemo(
6076
() => ({
6177
rootType: undefined,
@@ -78,13 +94,14 @@ export default function Voyager(props: VoyagerProps) {
7894
const [displayOptions, setDisplayOptions] = useState(initialDisplayOptions);
7995

8096
useEffect(() => {
97+
console.log('useEffect');
8198
setDisplayOptions(initialDisplayOptions);
8299
}, [introspectionResult, initialDisplayOptions]);
83100

84-
const typeGraph = useMemo(() => {
101+
const [navStack, setNavStack] = useState<NavStack | null>(null);
102+
useEffect(() => {
85103
if (introspectionResult.loading || introspectionResult.value == null) {
86-
// FIXME: display introspectionResult.error
87-
return null;
104+
return; // FIXME: display introspectionResult.error
88105
}
89106

90107
let introspectionSchema;
@@ -95,24 +112,22 @@ export default function Voyager(props: VoyagerProps) {
95112
introspectionResult.value.errors != null ||
96113
introspectionResult.value.data == null
97114
) {
98-
// FIXME: display errors
99-
return null;
115+
return; // FIXME: display errors
100116
}
101117
introspectionSchema = buildClientSchema(introspectionResult.value.data);
102118
}
103119

104120
const schema = getSchema(introspectionSchema, displayOptions);
105-
return getTypeGraph(schema, displayOptions);
106-
}, [introspectionResult, displayOptions]);
121+
const typeGraph = getTypeGraph(schema, displayOptions);
107122

108-
useEffect(() => {
109-
setSelected({ typeID: null, edgeID: null });
110-
}, [typeGraph]);
111-
112-
const [selected, setSelected] = useState<GraphSelection>({
113-
typeID: null,
114-
edgeID: null,
115-
});
123+
setNavStack(() => ({
124+
prev: null,
125+
typeGraph,
126+
type: null,
127+
selectedEdgeID: null,
128+
searchValue: null,
129+
}));
130+
}, [introspectionResult, displayOptions]);
116131

117132
const {
118133
allowToChangeSchema = false,
@@ -124,6 +139,70 @@ export default function Voyager(props: VoyagerProps) {
124139

125140
const viewportRef = useRef<GraphViewport>(null);
126141

142+
const handleNavigationBack = useCallback(() => {
143+
setNavStack((old) => {
144+
if (old?.prev == null) {
145+
return old;
146+
}
147+
return old.prev;
148+
});
149+
}, []);
150+
151+
const handleSearch = useCallback((searchValue: string | null) => {
152+
setNavStack((old) => {
153+
if (old == null) {
154+
return old;
155+
}
156+
return { ...old, searchValue };
157+
});
158+
}, []);
159+
160+
const handleSelectNode = useCallback((type: GraphQLNamedType | null) => {
161+
setNavStack((old) => {
162+
if (old == null) {
163+
return old;
164+
}
165+
if (type == null) {
166+
let first = old;
167+
while (first.prev != null) {
168+
first = first.prev;
169+
}
170+
return first;
171+
}
172+
return {
173+
prev: old,
174+
typeGraph: old.typeGraph,
175+
type,
176+
selectedEdgeID: null,
177+
searchValue: null,
178+
};
179+
});
180+
}, []);
181+
182+
const handleSelectEdge = useCallback(
183+
(edgeID: string, fromType: GraphQLNamedType, _toType: GraphQLNamedType) => {
184+
setNavStack((old) => {
185+
if (old == null) {
186+
return old;
187+
}
188+
if (fromType === old.type) {
189+
// deselect if click again
190+
return edgeID === old.selectedEdgeID
191+
? { ...old, selectedEdgeID: null }
192+
: { ...old, selectedEdgeID: edgeID };
193+
}
194+
return {
195+
prev: old,
196+
typeGraph: old.typeGraph,
197+
type: fromType,
198+
selectedEdgeID: edgeID,
199+
searchValue: null,
200+
};
201+
});
202+
},
203+
[],
204+
);
205+
127206
return (
128207
<ThemeProvider theme={theme}>
129208
<div className="graphql-voyager">
@@ -161,11 +240,12 @@ export default function Voyager(props: VoyagerProps) {
161240
{allowToChangeSchema && renderChangeSchemaButton()}
162241
{panelHeader}
163242
<DocExplorer
164-
typeGraph={typeGraph}
165-
selectedTypeID={selected.typeID}
166-
selectedEdgeID={selected.edgeID}
167-
onFocusNode={(id) => viewportRef.current?.focusNode(id)}
168-
onSelect={handleSelect}
243+
navStack={navStack}
244+
onNavigationBack={handleNavigationBack}
245+
onSearch={handleSearch}
246+
onFocusNode={(type) => viewportRef.current?.focusNode(type)}
247+
onSelectNode={handleSelectNode}
248+
onSelectEdge={handleSelectEdge}
169249
/>
170250
<PoweredBy />
171251
</div>
@@ -209,31 +289,21 @@ export default function Voyager(props: VoyagerProps) {
209289
{!hideSettings && (
210290
<Settings
211291
options={displayOptions}
212-
typeGraph={typeGraph}
292+
typeGraph={navStack?.typeGraph}
213293
onChange={(options) =>
214294
setDisplayOptions((oldOptions) => ({ ...oldOptions, ...options }))
215295
}
216296
/>
217297
)}
218298
<GraphViewport
219-
typeGraph={typeGraph}
220-
selectedTypeID={selected.typeID}
221-
selectedEdgeID={selected.edgeID}
222-
onSelect={handleSelect}
299+
navStack={navStack}
300+
onSelectNode={handleSelectNode}
301+
onSelectEdge={handleSelectEdge}
223302
ref={viewportRef}
224303
/>
225304
</Box>
226305
);
227306
}
228-
229-
function handleSelect(newSel: GraphSelection) {
230-
setSelected((oldSel) => {
231-
if (newSel.typeID === oldSel.typeID && newSel.edgeID === oldSel.edgeID) {
232-
return { typeID: newSel.typeID, edgeID: null }; // deselect if click again
233-
}
234-
return newSel;
235-
});
236-
}
237307
}
238308

239309
function PanelHeader(props: { children: ReactNode }) {

0 commit comments

Comments
 (0)