mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-20 16:43:53 +01:00
Add GraphQL variables editor
This commit is contained in:
@@ -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 })}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]',
|
||||
)}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}),
|
||||
];
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user