Add GraphQL variables editor

This commit is contained in:
Gregory Schier
2023-03-14 19:56:02 -07:00
parent f4401e77bb
commit 5904b6fded
8 changed files with 89 additions and 53 deletions

View File

@@ -1,5 +1,4 @@
import classnames from 'classnames';
import { act } from 'react-dom/test-utils';
import { useActiveRequest } from '../hooks/useActiveRequest';
import { useSendRequest } from '../hooks/useSendRequest';
import { useUpdateRequest } from '../hooks/useUpdateRequest';
@@ -67,7 +66,6 @@ export function RequestPane({ fullHeight, className }: Props) {
key={activeRequest.id}
useTemplating
className="!bg-gray-50"
heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={activeRequest.body ?? ''}
contentType="application/json"
onChange={(body) => updateRequest.mutate({ body })}
@@ -78,7 +76,6 @@ export function RequestPane({ fullHeight, className }: Props) {
useTemplating
className="!bg-gray-50"
defaultValue={activeRequest?.body ?? ''}
heightMode={fullHeight ? 'full' : 'auto'}
onChange={(body) => updateRequest.mutate({ body })}
/>
) : (

View File

@@ -26,7 +26,7 @@ export function Sidebar({ className }: Props) {
<div
className={classnames(
className,
'w-[15rem] bg-gray-100 h-full border-r border-gray-200 relative grid grid-rows-[auto,1fr]',
'w-[12rem] bg-gray-100 h-full border-r border-gray-200 relative grid grid-rows-[auto,1fr]',
)}
>
<HStack as={WindowDragRegion} alignItems="center" justifyContent="end">

View File

@@ -1,5 +1,5 @@
import { Button } from './core/Button';
import { DropdownMenuRadio } from './core/Dropdown';
import { DropdownMenuRadio, DropdownMenuTrigger } from './core/Dropdown';
import { IconButton } from './core/IconButton';
import { Input } from './core/Input';
@@ -45,9 +45,11 @@ export function UrlBar({ sendRequest, loading, onMethodChange, method, onUrlChan
{ label: 'HEAD', value: 'HEAD' },
]}
>
<Button type="button" disabled={loading} size="sm" className="mx-0.5" justify="start">
{method.toUpperCase()}
</Button>
<DropdownMenuTrigger>
<Button type="button" disabled={loading} size="sm" className="mx-0.5" justify="start">
{method.toUpperCase()}
</Button>
</DropdownMenuTrigger>
</DropdownMenuRadio>
}
rightSlot={

View File

@@ -10,7 +10,7 @@ import { useActiveRequest } from '../hooks/useActiveRequest';
export default function Workspace() {
const activeRequest = useActiveRequest();
const { width } = useWindowSize();
const isH = width > 900;
const isSideBySide = width > 900;
return (
<div className="grid grid-cols-[auto_1fr] grid-rows-1 h-full text-gray-900">
@@ -26,12 +26,12 @@ export default function Workspace() {
<div
className={classnames(
'grid',
isH
? 'grid-cols-[1fr_1fr] grid-rows-1'
isSideBySide
? 'grid-cols-[1fr_1fr] grid-rows-[minmax(0,1fr)]'
: 'grid-cols-1 grid-rows-[minmax(0,auto)_minmax(0,100%)]',
)}
>
<RequestPane fullHeight={isH} className={classnames(!isH && 'pr-2')} />
<RequestPane fullHeight={isSideBySide} className={classnames(!isSideBySide && 'pr-2')} />
<ResponsePane />
</div>
</div>

View File

@@ -12,7 +12,7 @@ export function Divider({ className, orientation = 'horizontal', decorative }: P
<Separator.Root
className={classnames(
className,
'bg-gray-50',
'bg-gray-300/40',
orientation === 'horizontal' && 'w-full h-[1px]',
orientation === 'vertical' && 'h-full w-[1px]',
)}

View File

@@ -80,6 +80,13 @@
.cm-scroller {
@apply font-mono text-[0.75rem];
/*
* Round corners or they'll stick out of the editor bounds of editor is rounded.
* Could potentially be pushed up from the editor like we do with bg color but this
* is probably fine.
*/
@apply rounded-lg;
}
}
}

View File

@@ -3,10 +3,9 @@ import { Compartment, EditorState } from '@codemirror/state';
import { keymap, placeholder as placeholderExt, tooltips } from '@codemirror/view';
import classnames from 'classnames';
import { EditorView } from 'codemirror';
import { formatSdl } from 'format-graphql';
import type { MutableRefObject } from 'react';
import { useEffect, useRef } from 'react';
import { useUnmount } from 'react-use';
import { IconButton } from '../IconButton';
import './Editor.css';
import { baseExtensions, getLanguageExtension, multiLineExtensions } from './extensions';
import { singleLineExt } from './singleLine';
@@ -48,6 +47,18 @@ export function _Editor({
cm.current = null;
});
// Use ref so we can update the onChange handler without re-initializing the editor
const handleChange = useRef<_EditorProps['onChange']>(onChange);
useEffect(() => {
handleChange.current = onChange;
}, [onChange]);
// Use ref so we can update the onChange handler without re-initializing the editor
const handleFocus = useRef<_EditorProps['onFocus']>(onFocus);
useEffect(() => {
handleFocus.current = onFocus;
}, [onFocus]);
// Update language extension when contentType changes
useEffect(() => {
if (cm.current === null) return;
@@ -69,11 +80,11 @@ export function _Editor({
langHolder.of(langExt),
...getExtensions({
container: el,
onChange: handleChange,
onFocus: handleFocus,
readOnly,
placeholder,
singleLine,
onChange,
onFocus,
contentType,
useTemplating,
}),
@@ -91,6 +102,7 @@ export function _Editor({
return (
<div
ref={initDivRef}
dangerouslySetInnerHTML={{ __html: '' }}
className={classnames(
className,
'cm-wrapper text-base bg-gray-50',
@@ -98,19 +110,7 @@ export function _Editor({
singleLine ? 'cm-singleline' : 'cm-multiline',
readOnly && 'cm-readonly',
)}
>
{contentType?.includes('graphql') && (
<IconButton
icon="eye"
className="absolute right-0 bottom-0 z-10"
onClick={() => {
const doc = cm.current?.view.state.doc ?? '';
const insert = formatSdl(doc.toString());
cm.current?.view.dispatch({ changes: { from: 0, to: doc.length, insert } });
}}
/>
)}
</div>
/>
);
}
@@ -125,14 +125,12 @@ function getExtensions({
useTemplating,
}: Pick<
_EditorProps,
| 'singleLine'
| 'onChange'
| 'contentType'
| 'useTemplating'
| 'placeholder'
| 'readOnly'
| 'onFocus'
> & { container: HTMLDivElement | null }) {
'singleLine' | 'contentType' | 'useTemplating' | 'placeholder' | 'readOnly'
> & {
container: HTMLDivElement | null;
onChange: MutableRefObject<_EditorProps['onChange']>;
onFocus: MutableRefObject<_EditorProps['onFocus']>;
}) {
const ext = getLanguageExtension({ contentType, useTemplating });
// TODO: Ensure tooltips render inside the dialog if we are in one.
@@ -172,14 +170,14 @@ function getExtensions({
// Handle onFocus
EditorView.domEventHandlers({
focus: () => {
onFocus?.();
onFocus.current?.();
},
}),
// Handle onChange
EditorView.updateListener.of((update) => {
if (typeof onChange === 'function' && update.docChanged) {
onChange(update.state.doc.toString());
if (onChange && update.docChanged) {
onChange.current?.(update.state.doc.toString());
}
}),
];

View File

@@ -1,4 +1,5 @@
import { useMemo } from 'react';
import { Divider } from '../core/Divider';
import type { EditorProps } from '../core/Editor';
import { Editor } from '../core/Editor';
@@ -7,26 +8,57 @@ type Props = Pick<
'heightMode' | 'onChange' | 'defaultValue' | 'className' | 'useTemplating'
>;
export function GraphQLEditor({ defaultValue, onChange, ...props }: Props) {
const { query } = useMemo(() => {
interface GraphQLBody {
query: string;
variables?: Record<string, string | number | boolean | null>;
operationName?: string;
}
export function GraphQLEditor({ defaultValue, onChange, ...extraEditorProps }: Props) {
const { query, variables } = useMemo<GraphQLBody>(() => {
try {
const { query } = JSON.parse(defaultValue ?? '{}');
return { query };
const p = JSON.parse(defaultValue ?? '{}');
const query = p.query ?? '';
const variables = p.variables;
const operationName = p.operationName;
return { query, variables, operationName };
} catch (err) {
return { query: 'failed to parse' };
}
}, [defaultValue]);
const handleChange = (query: string) => {
onChange?.(JSON.stringify({ query }, null, 2));
const handleChange = (b: GraphQLBody) => {
onChange?.(JSON.stringify(b, null, 2));
};
const handleChangeQuery = (query: string) => {
handleChange({ query, variables });
};
const handleChangeVariables = (variables: string) => {
handleChange({ query, variables: JSON.parse(variables) });
};
return (
<Editor
defaultValue={query ?? ''}
onChange={handleChange}
contentType="application/graphql"
{...props}
/>
<div className="pb-1 h-full grid grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
<Editor
heightMode="auto"
defaultValue={query ?? ''}
onChange={handleChangeQuery}
contentType="application/graphql"
{...extraEditorProps}
/>
<div className="pl-2">
<Divider />
</div>
<p className="pl-2 pt-1 text-gray-500 text-sm">Variables</p>
<Editor
heightMode="auto"
defaultValue={JSON.stringify(variables, null, 2)}
onChange={handleChangeVariables}
contentType="application/json"
{...extraEditorProps}
/>
</div>
);
}