Skip to content

Commit 4a8bd4f

Browse files
authored
CONSOLE-1958: Operation picker (when multiple exists in document) (#7963)
1 parent e3d9750 commit 4a8bd4f

7 files changed

Lines changed: 221 additions & 41 deletions

File tree

.changeset/evil-results-rhyme.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@graphql-hive/laboratory': patch
3+
'@graphql-hive/render-laboratory': patch
4+
---
5+
6+
Implemented functionality that allows to have multiple queries in same operation while working only
7+
with focused one (run button, query builder)

packages/libraries/laboratory/src/components/laboratory/builder.tsx

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ export const BuilderArgument = (props: {
4949
path: string[];
5050
isReadOnly?: boolean;
5151
operation?: LaboratoryOperation | null;
52+
operationName?: string | null;
5253
}) => {
5354
const {
5455
schema,
@@ -90,9 +91,18 @@ export const BuilderArgument = (props: {
9091
}
9192

9293
if (checked) {
93-
addArgToActiveOperation(props.path.join('.'), props.field.name, schema);
94+
addArgToActiveOperation(
95+
props.path.join('.'),
96+
props.field.name,
97+
schema,
98+
props.operationName,
99+
);
94100
} else {
95-
deleteArgFromActiveOperation(props.path.join('.'), props.field.name);
101+
deleteArgFromActiveOperation(
102+
props.path.join('.'),
103+
props.field.name,
104+
props.operationName,
105+
);
96106
}
97107
}}
98108
/>
@@ -112,6 +122,7 @@ export const BuilderScalarField = (props: {
112122
isSearchActive?: boolean;
113123
isReadOnly?: boolean;
114124
operation?: LaboratoryOperation | null;
125+
operationName?: string | null;
115126
searchValue?: string;
116127
label?: React.ReactNode;
117128
disableChildren?: boolean;
@@ -141,16 +152,18 @@ export const BuilderScalarField = (props: {
141152
);
142153

143154
const isInQuery = useMemo(() => {
144-
return isPathInQuery(operation?.query ?? '', path);
145-
}, [operation?.query, path]);
155+
return isPathInQuery(operation?.query ?? '', path, props.operationName);
156+
}, [operation?.query, path, props.operationName]);
146157

147158
const args = useMemo(() => {
148159
return (props.field as GraphQLField<unknown, unknown, unknown>).args ?? [];
149160
}, [props.field]);
150161

151162
const hasArgs = useMemo(() => {
152-
return args.some(arg => isArgInQuery(operation?.query ?? '', path, arg.name));
153-
}, [operation?.query, args, path]);
163+
return args.some(arg =>
164+
isArgInQuery(operation?.query ?? '', path, arg.name, props.operationName),
165+
);
166+
}, [operation?.query, args, path, props.operationName]);
154167

155168
const shouldHighlight = useMemo(() => {
156169
const splittedName = splitIdentifier(props.field.name);
@@ -186,9 +199,9 @@ export const BuilderScalarField = (props: {
186199
onCheckedChange={checked => {
187200
if (checked) {
188201
setIsOpen(true);
189-
addPathToActiveOperation(path);
202+
addPathToActiveOperation(path, props.operationName);
190203
} else {
191-
deletePathFromActiveOperation(path);
204+
deletePathFromActiveOperation(path, props.operationName);
192205
}
193206
}}
194207
/>
@@ -238,9 +251,9 @@ export const BuilderScalarField = (props: {
238251
onCheckedChange={checked => {
239252
if (checked) {
240253
setIsOpen(true);
241-
addPathToActiveOperation(path);
254+
addPathToActiveOperation(path, props.operationName);
242255
} else {
243-
deletePathFromActiveOperation(path);
256+
deletePathFromActiveOperation(path, props.operationName);
244257
}
245258
}}
246259
/>
@@ -322,9 +335,9 @@ export const BuilderScalarField = (props: {
322335
disabled={activeTab?.type !== 'operation'}
323336
onCheckedChange={checked => {
324337
if (checked) {
325-
addPathToActiveOperation(props.path.join('.'));
338+
addPathToActiveOperation(props.path.join('.'), props.operationName);
326339
} else {
327-
deletePathFromActiveOperation(props.path.join('.'));
340+
deletePathFromActiveOperation(props.path.join('.'), props.operationName);
328341
}
329342
}}
330343
/>
@@ -353,6 +366,7 @@ export const BuilderObjectField = (props: {
353366
isSearchActive?: boolean;
354367
isReadOnly?: boolean;
355368
operation?: LaboratoryOperation | null;
369+
operationName?: string | null;
356370
searchValue?: string;
357371
label?: React.ReactNode;
358372
disableChildren?: boolean;
@@ -442,9 +456,9 @@ export const BuilderObjectField = (props: {
442456
onCheckedChange={checked => {
443457
if (checked) {
444458
setIsOpen(true);
445-
addPathToActiveOperation(path);
459+
addPathToActiveOperation(path, props.operationName);
446460
} else {
447-
deletePathFromActiveOperation(path);
461+
deletePathFromActiveOperation(path, props.operationName);
448462
}
449463
}}
450464
/>
@@ -493,9 +507,9 @@ export const BuilderObjectField = (props: {
493507
onCheckedChange={checked => {
494508
if (checked) {
495509
setIsOpen(true);
496-
addPathToActiveOperation(path);
510+
addPathToActiveOperation(path, props.operationName);
497511
} else {
498-
deletePathFromActiveOperation(path);
512+
deletePathFromActiveOperation(path, props.operationName);
499513
}
500514
}}
501515
/>
@@ -565,6 +579,7 @@ export const BuilderObjectField = (props: {
565579
isSearchActive={props.isSearchActive}
566580
isReadOnly={props.isReadOnly}
567581
operation={operation}
582+
operationName={props.operationName}
568583
searchValue={props.searchValue}
569584
/>
570585
))}
@@ -584,6 +599,7 @@ export const BuilderField = (props: {
584599
forcedOpenPaths?: Set<string> | null;
585600
isSearchActive?: boolean;
586601
operation?: LaboratoryOperation | null;
602+
operationName?: string | null;
587603
isReadOnly?: boolean;
588604
searchValue?: string;
589605
label?: React.ReactNode;
@@ -610,6 +626,7 @@ export const BuilderField = (props: {
610626
isSearchActive={props.isSearchActive}
611627
isReadOnly={props.isReadOnly}
612628
operation={props.operation}
629+
operationName={props.operationName}
613630
searchValue={props.searchValue}
614631
label={props.label}
615632
disableChildren={props.disableChildren}
@@ -628,6 +645,7 @@ export const BuilderField = (props: {
628645
isSearchActive={props.isSearchActive}
629646
isReadOnly={props.isReadOnly}
630647
operation={props.operation}
648+
operationName={props.operationName}
631649
searchValue={props.searchValue}
632650
label={props.label}
633651
disableChildren={props.disableChildren}
@@ -652,6 +670,7 @@ export const BuilderSearchResults = (props: {
652670
mode: BuilderSearchResultMode;
653671
isReadOnly: boolean;
654672
operation: LaboratoryOperation | null;
673+
operationName?: string | null;
655674
searchValue: string;
656675
schema: GraphQLSchema;
657676
tab: OperationTypeNode;
@@ -676,6 +695,7 @@ export const BuilderSearchResults = (props: {
676695
isSearchActive={props.isSearchActive}
677696
isReadOnly={props.isReadOnly}
678697
operation={props.operation}
698+
operationName={props.operationName}
679699
searchValue={props.searchValue}
680700
disableChildren
681701
label={
@@ -727,6 +747,7 @@ export const BuilderSearchResults = (props: {
727747
isSearchActive={props.isSearchActive}
728748
isReadOnly={props.isReadOnly}
729749
operation={props.operation}
750+
operationName={props.operationName}
730751
searchValue={props.searchValue}
731752
/>
732753
);
@@ -735,6 +756,7 @@ export const BuilderSearchResults = (props: {
735756

736757
export const Builder = (props: {
737758
operation?: LaboratoryOperation | null;
759+
operationName?: string | null;
738760
isReadOnly?: boolean;
739761
}) => {
740762
const { schema, activeOperation, endpoint, setEndpoint, defaultEndpoint } = useLaboratory();
@@ -980,6 +1002,7 @@ export const Builder = (props: {
9801002
isSearchActive={isSearchActive}
9811003
isReadOnly={props.isReadOnly}
9821004
operation={operation}
1005+
operationName={props.operationName}
9831006
searchValue={deferredSearchValue}
9841007
/>
9851008
))
@@ -1016,6 +1039,7 @@ export const Builder = (props: {
10161039
isSearchActive={isSearchActive}
10171040
isReadOnly={props.isReadOnly}
10181041
operation={operation}
1042+
operationName={props.operationName}
10191043
searchValue={deferredSearchValue}
10201044
/>
10211045
))
@@ -1052,6 +1076,7 @@ export const Builder = (props: {
10521076
isSearchActive={isSearchActive}
10531077
isReadOnly={props.isReadOnly}
10541078
operation={operation}
1079+
operationName={props.operationName}
10551080
searchValue={deferredSearchValue}
10561081
/>
10571082
))

packages/libraries/laboratory/src/components/laboratory/editor.tsx

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1-
import { forwardRef, useEffect, useId, useImperativeHandle, useRef, useState } from 'react';
2-
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
1+
import {
2+
forwardRef,
3+
useCallback,
4+
useEffect,
5+
useId,
6+
useImperativeHandle,
7+
useLayoutEffect,
8+
useRef,
9+
useState,
10+
} from 'react';
11+
import { OperationDefinitionNode, parse } from 'graphql';
12+
import * as monaco from 'monaco-editor';
313
import { MonacoGraphQLAPI } from 'monaco-graphql/esm/api.js';
414
import { initializeMode } from 'monaco-graphql/initializeMode';
515
import MonacoEditor, { loader } from '@monaco-editor/react';
@@ -122,6 +132,7 @@ export type EditorProps = React.ComponentProps<typeof MonacoEditor> & {
122132
uri?: monaco.Uri;
123133
variablesUri?: monaco.Uri;
124134
extraLibs?: string[];
135+
onOperationNameChange?: (operationName: string | null) => void;
125136
};
126137

127138
const EditorInner = forwardRef<EditorHandle, EditorProps>((props, ref) => {
@@ -231,6 +242,109 @@ const EditorInner = forwardRef<EditorHandle, EditorProps>((props, ref) => {
231242
[],
232243
);
233244

245+
const setupDecorationsHandler = useCallback(
246+
(editor: monaco.editor.IStandaloneCodeEditor) => {
247+
let decorationsCollection: monaco.editor.IEditorDecorationsCollection | null = null;
248+
249+
const handler = () => {
250+
decorationsCollection?.clear();
251+
252+
try {
253+
const value = editor.getValue();
254+
const doc = parse(value);
255+
256+
const definition = doc.definitions.find(definition => {
257+
if (definition.kind !== 'OperationDefinition') {
258+
return false;
259+
}
260+
261+
if (!definition.loc) {
262+
return false;
263+
}
264+
265+
const cursorPosition = editor.getPosition();
266+
267+
if (cursorPosition) {
268+
return (
269+
definition.loc.startToken.line <= cursorPosition.lineNumber &&
270+
definition.loc.endToken.line >= cursorPosition.lineNumber
271+
);
272+
}
273+
});
274+
275+
if (definition?.loc) {
276+
const decorations: monaco.editor.IModelDeltaDecoration[] = [];
277+
278+
if (definition.loc.startToken.line > 1) {
279+
decorations.push({
280+
range: new monaco.Range(
281+
0,
282+
0,
283+
definition.loc.startToken.line - 1,
284+
definition.loc.startToken.column,
285+
),
286+
options: {
287+
isWholeLine: true,
288+
inlineClassName: 'inactive-line',
289+
},
290+
});
291+
}
292+
293+
const lineCount = editor.getModel()?.getLineCount() ?? 0;
294+
const lastLineMaxColumn = editor.getModel()?.getLineMaxColumn(lineCount) ?? 0;
295+
296+
if (definition.loc.endToken.line < lineCount) {
297+
decorations.push({
298+
range: new monaco.Range(
299+
definition.loc.endToken.line + 1,
300+
definition.loc.endToken.column,
301+
lineCount,
302+
lastLineMaxColumn,
303+
),
304+
options: {
305+
isWholeLine: true,
306+
inlineClassName: 'inactive-line',
307+
},
308+
});
309+
}
310+
311+
decorationsCollection = editor.createDecorationsCollection(decorations);
312+
313+
props.onOperationNameChange?.(
314+
(definition as OperationDefinitionNode).name?.value ?? null,
315+
);
316+
}
317+
} catch (error) {}
318+
};
319+
320+
editor.onDidChangeCursorPosition(handler);
321+
322+
handler();
323+
},
324+
[props.onOperationNameChange],
325+
);
326+
327+
const handleMount = useCallback(
328+
(editor: monaco.editor.IStandaloneCodeEditor) => {
329+
editorRef.current = editor;
330+
setupDecorationsHandler(editor);
331+
},
332+
[setupDecorationsHandler],
333+
);
334+
335+
const recentCursorPosition = useRef<{ lineNumber: number; column: number } | null>(null);
336+
337+
useLayoutEffect(() => {
338+
recentCursorPosition.current = editorRef.current?.getPosition() ?? null;
339+
}, [props.value]);
340+
341+
useEffect(() => {
342+
if (editorRef.current && recentCursorPosition.current) {
343+
editorRef.current.setPosition(recentCursorPosition.current);
344+
recentCursorPosition.current = null;
345+
}
346+
}, [props.value]);
347+
234348
if (!typescriptReady && props.language === 'typescript') {
235349
return null;
236350
}
@@ -245,9 +359,7 @@ const EditorInner = forwardRef<EditorHandle, EditorProps>((props, ref) => {
245359
className="size-full"
246360
{...props}
247361
theme={theme === 'dark' ? 'hive-laboratory-dark' : 'hive-laboratory-light'}
248-
onMount={editor => {
249-
editorRef.current = editor;
250-
}}
362+
onMount={handleMount}
251363
loading={null}
252364
options={{
253365
...props.options,

0 commit comments

Comments
 (0)