Skip to content

Commit 733ff47

Browse files
committed
Rewrite navigation stack managment
1 parent 3aa74aa commit 733ff47

15 files changed

Lines changed: 283 additions & 268 deletions

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: 107 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,9 +52,23 @@ 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;
61+
}
62+
63+
interface NavStackType {
64+
prev: NavStack;
65+
typeGraph: TypeGraph;
66+
type: GraphQLNamedType;
67+
selectedEdgeID: string | null;
68+
searchValue: string;
69+
}
70+
71+
export type NavStack = NavStackTypeList | NavStackType;
5772

5873
export default function Voyager(props: VoyagerProps) {
5974
const initialDisplayOptions = useMemo(
@@ -81,10 +96,10 @@ export default function Voyager(props: VoyagerProps) {
8196
setDisplayOptions(initialDisplayOptions);
8297
}, [introspectionResult, initialDisplayOptions]);
8398

84-
const typeGraph = useMemo(() => {
99+
const [navStack, setNavStack] = useState<NavStack | null>(null);
100+
useEffect(() => {
85101
if (introspectionResult.loading || introspectionResult.value == null) {
86-
// FIXME: display introspectionResult.error
87-
return null;
102+
return; // FIXME: display introspectionResult.error
88103
}
89104

90105
let introspectionSchema;
@@ -95,24 +110,22 @@ export default function Voyager(props: VoyagerProps) {
95110
introspectionResult.value.errors != null ||
96111
introspectionResult.value.data == null
97112
) {
98-
// FIXME: display errors
99-
return null;
113+
return; // FIXME: display errors
100114
}
101115
introspectionSchema = buildClientSchema(introspectionResult.value.data);
102116
}
103117

104118
const schema = getSchema(introspectionSchema, displayOptions);
105-
return getTypeGraph(schema, displayOptions);
106-
}, [introspectionResult, displayOptions]);
119+
const typeGraph = getTypeGraph(schema, displayOptions);
107120

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

117130
const {
118131
allowToChangeSchema = false,
@@ -124,6 +137,70 @@ export default function Voyager(props: VoyagerProps) {
124137

125138
const viewportRef = useRef<GraphViewport>(null);
126139

140+
const handleNavigationBack = useCallback(() => {
141+
setNavStack((old) => {
142+
if (old?.prev == null) {
143+
return old;
144+
}
145+
return old.prev;
146+
});
147+
}, []);
148+
149+
const handleSearch = useCallback((searchValue: string) => {
150+
setNavStack((old) => {
151+
if (old == null) {
152+
return old;
153+
}
154+
return { ...old, searchValue };
155+
});
156+
}, []);
157+
158+
const handleSelectNode = useCallback((type: GraphQLNamedType | null) => {
159+
setNavStack((old) => {
160+
if (old == null) {
161+
return old;
162+
}
163+
if (type == null) {
164+
let first = old;
165+
while (first.prev != null) {
166+
first = first.prev;
167+
}
168+
return first;
169+
}
170+
return {
171+
prev: old,
172+
typeGraph: old.typeGraph,
173+
type,
174+
selectedEdgeID: null,
175+
searchValue: '',
176+
};
177+
});
178+
}, []);
179+
180+
const handleSelectEdge = useCallback(
181+
(edgeID: string, fromType: GraphQLNamedType, _toType: GraphQLNamedType) => {
182+
setNavStack((old) => {
183+
if (old == null) {
184+
return old;
185+
}
186+
if (fromType === old.type) {
187+
// deselect if click again
188+
return edgeID === old.selectedEdgeID
189+
? { ...old, selectedEdgeID: null }
190+
: { ...old, selectedEdgeID: edgeID };
191+
}
192+
return {
193+
prev: old,
194+
typeGraph: old.typeGraph,
195+
type: fromType,
196+
selectedEdgeID: edgeID,
197+
searchValue: '',
198+
};
199+
});
200+
},
201+
[],
202+
);
203+
127204
return (
128205
<ThemeProvider theme={theme}>
129206
<div className="graphql-voyager">
@@ -161,11 +238,12 @@ export default function Voyager(props: VoyagerProps) {
161238
{allowToChangeSchema && renderChangeSchemaButton()}
162239
{panelHeader}
163240
<DocExplorer
164-
typeGraph={typeGraph}
165-
selectedTypeID={selected.typeID}
166-
selectedEdgeID={selected.edgeID}
167-
onFocusNode={(id) => viewportRef.current?.focusNode(id)}
168-
onSelect={handleSelect}
241+
navStack={navStack}
242+
onNavigationBack={handleNavigationBack}
243+
onSearch={handleSearch}
244+
onFocusNode={(type) => viewportRef.current?.focusNode(type)}
245+
onSelectNode={handleSelectNode}
246+
onSelectEdge={handleSelectEdge}
169247
/>
170248
<PoweredBy />
171249
</div>
@@ -209,31 +287,21 @@ export default function Voyager(props: VoyagerProps) {
209287
{!hideSettings && (
210288
<Settings
211289
options={displayOptions}
212-
typeGraph={typeGraph}
290+
typeGraph={navStack?.typeGraph}
213291
onChange={(options) =>
214292
setDisplayOptions((oldOptions) => ({ ...oldOptions, ...options }))
215293
}
216294
/>
217295
)}
218296
<GraphViewport
219-
typeGraph={typeGraph}
220-
selectedTypeID={selected.typeID}
221-
selectedEdgeID={selected.edgeID}
222-
onSelect={handleSelect}
297+
navStack={navStack}
298+
onSelectNode={handleSelectNode}
299+
onSelectEdge={handleSelectEdge}
223300
ref={viewportRef}
224301
/>
225302
</Box>
226303
);
227304
}
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-
}
237305
}
238306

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

src/components/doc-explorer/Argument.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import WrappedTypeName from './WrappedTypeName.tsx';
88

99
interface ArgumentProps {
1010
arg: GraphQLArgument;
11-
filter: string | null;
11+
filter: string;
1212
expanded: boolean;
1313
onTypeLink: (type: GraphQLNamedType) => void;
1414
}

0 commit comments

Comments
 (0)