mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-21 17:09:37 +01:00
Fix docs explorer cmd+click
This commit is contained in:
@@ -38,7 +38,11 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
|
||||
/>
|
||||
);
|
||||
|
||||
if (activeRequest.bodyType === 'graphql' && showGraphQLDocExplorer && graphQLSchema != null) {
|
||||
if (
|
||||
activeRequest.bodyType === 'graphql' &&
|
||||
showGraphQLDocExplorer[activeRequest.id] !== undefined &&
|
||||
graphQLSchema != null
|
||||
) {
|
||||
return (
|
||||
<SplitLayout
|
||||
name="graphql_layout"
|
||||
@@ -46,7 +50,7 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
|
||||
firstSlot={requestResponseSplit}
|
||||
secondSlot={({ style, orientation }) => (
|
||||
<GraphQLDocsExplorer
|
||||
key={activeRequest.id}
|
||||
requestId={activeRequest.id}
|
||||
schema={graphQLSchema}
|
||||
className={classNames(orientation == 'horizontal' && '!ml-0')}
|
||||
style={style}
|
||||
|
||||
@@ -37,9 +37,11 @@ import {
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import type { EnvironmentVariable } from '@yaakapp-internal/models';
|
||||
import { graphql } from 'cm6-graphql';
|
||||
import { activeRequestIdAtom } from '../../../hooks/useActiveRequestId';
|
||||
import { jotaiStore } from '../../../lib/jotai';
|
||||
import { renderMarkdown } from '../../../lib/markdown';
|
||||
import { pluralizeCount } from '../../../lib/pluralize';
|
||||
import { showInGraphQLDocsExplorer } from '../../graphql/useGraphQLDocsExplorer';
|
||||
import { showGraphQLDocExplorerAtom } from '../../graphql/graphqlAtoms';
|
||||
import type { EditorProps } from './Editor';
|
||||
import { pairs } from './pairs/extension';
|
||||
import { text } from './text/extension';
|
||||
@@ -135,8 +137,13 @@ export function getLanguageExtension({
|
||||
return span;
|
||||
},
|
||||
onShowInDocs(field, type, parentType) {
|
||||
console.log("SHOW IN DOCS", field);
|
||||
showInGraphQLDocsExplorer(field, type, parentType).catch(console.error);
|
||||
const activeRequestId = jotaiStore.get(activeRequestIdAtom);
|
||||
if (activeRequestId == null) return;
|
||||
console.log('SHOW IN DOCS', field);
|
||||
jotaiStore.set(showGraphQLDocExplorerAtom, (v) => ({
|
||||
...v,
|
||||
[activeRequestId]: { field, type, parentType },
|
||||
}));
|
||||
},
|
||||
}),
|
||||
extraExtensions,
|
||||
|
||||
@@ -21,24 +21,26 @@ import {
|
||||
isScalarType,
|
||||
isUnionType,
|
||||
} from 'graphql';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { CSSProperties, HTMLAttributes, KeyboardEvent, ReactNode } from 'react';
|
||||
import { Fragment, memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { Fragment, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useClickOutside } from '../../hooks/useClickOutside';
|
||||
import { useContainerSize } from '../../hooks/useContainerQuery';
|
||||
import { useDebouncedValue } from '../../hooks/useDebouncedValue';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
import { jotaiStore } from '../../lib/jotai';
|
||||
import { Banner } from '../core/Banner';
|
||||
import { CountBadge } from '../core/CountBadge';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { IconButton } from '../core/IconButton';
|
||||
import { PlainInput } from '../core/PlainInput';
|
||||
import { Markdown } from '../Markdown';
|
||||
import { showGraphQLDocExplorerAtom } from './graphqlAtoms';
|
||||
import { useGraphQLDocsExplorerEvent } from './useGraphQLDocsExplorer';
|
||||
|
||||
interface Props {
|
||||
style?: CSSProperties;
|
||||
schema: GraphQLSchema;
|
||||
requestId: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
@@ -51,6 +53,7 @@ type ExplorerItem =
|
||||
export const GraphQLDocsExplorer = memo(function GraphQLDocsExplorer({
|
||||
style,
|
||||
schema,
|
||||
requestId,
|
||||
className,
|
||||
}: Props) {
|
||||
const [activeItem, setActiveItem] = useState<ExplorerItem>(null);
|
||||
@@ -58,17 +61,34 @@ export const GraphQLDocsExplorer = memo(function GraphQLDocsExplorer({
|
||||
const qryType = schema.getQueryType();
|
||||
const mutType = schema.getMutationType();
|
||||
const subType = schema.getSubscriptionType();
|
||||
const showField = useAtomValue(showGraphQLDocExplorerAtom)[requestId] ?? null;
|
||||
|
||||
useGraphQLDocsExplorerEvent('gql_docs_explorer.show_in_docs', ({ field }) => {
|
||||
walkTypeGraph(schema, null, (t) => {
|
||||
if (t.name === field) {
|
||||
setActiveItem(toExplorerItem(t, null));
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
useEffect(() => {
|
||||
if (showField === null) {
|
||||
setActiveItem(null);
|
||||
} else {
|
||||
walkTypeGraph(schema, null, (t, from) => {
|
||||
const isRootParentType =
|
||||
showField.parentType === 'Query' ||
|
||||
showField.parentType === 'Mutation' ||
|
||||
showField.parentType === 'Subscription';
|
||||
if (
|
||||
showField.field === t.name &&
|
||||
// For input fields, CodeMirror seems to set parentType to the root type of the field they belong to.
|
||||
(isRootParentType || from?.name === showField.parentType)
|
||||
) {
|
||||
console.log('SET FIELD', t, from);
|
||||
setActiveItem(toExplorerItem(t, toExplorerItem(from, null)));
|
||||
return false;
|
||||
} else if (showField.type === t.name && from?.name === showField.parentType) {
|
||||
setActiveItem(toExplorerItem(t, toExplorerItem(from, null)));
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [schema, showField]);
|
||||
|
||||
const qryItem: ExplorerItem = qryType ? { kind: 'type', type: qryType, from: null } : null;
|
||||
const mutItem: ExplorerItem = mutType ? { kind: 'type', type: mutType, from: null } : null;
|
||||
@@ -83,6 +103,9 @@ export const GraphQLDocsExplorer = memo(function GraphQLDocsExplorer({
|
||||
<GraphQLExplorerHeader
|
||||
containerHeight={containerSize.height}
|
||||
item={activeItem}
|
||||
onClose={() => {
|
||||
jotaiStore.set(showGraphQLDocExplorerAtom, (v) => ({ ...v, [requestId]: undefined }));
|
||||
}}
|
||||
setItem={setActiveItem}
|
||||
schema={schema}
|
||||
/>
|
||||
@@ -140,11 +163,13 @@ function GraphQLExplorerHeader({
|
||||
item,
|
||||
setItem,
|
||||
schema,
|
||||
onClose,
|
||||
containerHeight,
|
||||
}: {
|
||||
item: ExplorerItem;
|
||||
setItem: (t: ExplorerItem) => void;
|
||||
schema: GraphQLSchema;
|
||||
onClose: () => void;
|
||||
containerHeight: number;
|
||||
}) {
|
||||
const findIt = (t: ExplorerItem): ExplorerItem[] => {
|
||||
@@ -186,14 +211,7 @@ function GraphQLExplorerHeader({
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-auto flex gap-1 [&>*]:text-text-subtle">
|
||||
<IconButton
|
||||
icon="x"
|
||||
size="sm"
|
||||
title="Close documenation explorer"
|
||||
onClick={() => {
|
||||
jotaiStore.set(showGraphQLDocExplorerAtom, false);
|
||||
}}
|
||||
/>
|
||||
<IconButton icon="x" size="sm" title="Close documenation explorer" onClick={onClose} />
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
@@ -219,6 +237,11 @@ function GqlTypeInfo({
|
||||
<GqlTypeLabel item={item} />
|
||||
</Heading>
|
||||
<DocMarkdown>{description || 'No description'}</DocMarkdown>
|
||||
{'deprecationReason' in item.type && item.type.deprecationReason && (
|
||||
<Banner color="notice">
|
||||
<DocMarkdown>{item.type.deprecationReason}</DocMarkdown>
|
||||
</Banner>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -294,6 +317,28 @@ function GqlTypeInfo({
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
} else if (item.kind === 'input_field') {
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{heading}
|
||||
|
||||
{item.type.defaultValue !== undefined && (
|
||||
<div>
|
||||
<Subheading>Default Value</Subheading>
|
||||
<div className="font-mono text-editor">{JSON.stringify(item.type.defaultValue)}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<Subheading>Type</Subheading>
|
||||
<GqlTypeRow
|
||||
className="mt-4"
|
||||
item={{ kind: 'type', type: item.type.type, from: item }}
|
||||
setItem={setItem}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (item.kind === 'field') {
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
@@ -392,7 +437,7 @@ function GqlTypeInfo({
|
||||
);
|
||||
}
|
||||
|
||||
console.log('Unknown GraphQL Type', item.type, isNonNullType(item.type));
|
||||
console.log('Unknown GraphQL Type', item);
|
||||
return <div>Unknown GraphQL type</div>;
|
||||
}
|
||||
|
||||
@@ -665,8 +710,7 @@ function GqlSchemaSearch({
|
||||
} else {
|
||||
results.push({ name: type.name, type, score: match.score, from, depth });
|
||||
}
|
||||
|
||||
return true; // Continue searching
|
||||
return true;
|
||||
});
|
||||
results.sort((a, b) => {
|
||||
if (value == '') {
|
||||
@@ -922,19 +966,25 @@ function walkTypeGraph(
|
||||
}
|
||||
}
|
||||
|
||||
function toExplorerItem(t: any, from: ExplorerItem | null): ExplorerItem {
|
||||
function toExplorerItem(t: any, from: ExplorerItem | null): ExplorerItem | null {
|
||||
if (t == null) return null;
|
||||
|
||||
// GraphQLField-like: has `args` and `type`
|
||||
if (t && typeof t === 'object' && Array.isArray(t.args) && t.type) {
|
||||
// GraphQLField-like: has `args` (array) and `type`
|
||||
if (typeof t === 'object' && Array.isArray(t.args) && t.type) {
|
||||
return { kind: 'field', type: t, from };
|
||||
}
|
||||
|
||||
// GraphQLInputField-like: has `type` and maybe `defaultValue`, but no `args`
|
||||
if (t && typeof t === 'object' && t.type && !('args' in t)) {
|
||||
// GraphQLInputField-like: has `type`, no `args`, maybe `defaultValue`, and no `resolve`
|
||||
if (
|
||||
typeof t === 'object' &&
|
||||
t.type &&
|
||||
!('args' in t) &&
|
||||
!('resolve' in t) &&
|
||||
('defaultValue' in t || 'description' in t)
|
||||
) {
|
||||
return { kind: 'input_field', type: t, from };
|
||||
}
|
||||
|
||||
// Otherwise, assume GraphQLType (object, scalar, enum, etc.)
|
||||
// Fallback: treat as GraphQLNamedType (object, scalar, enum, etc.)
|
||||
return { kind: 'type', type: t, from };
|
||||
}
|
||||
|
||||
@@ -48,7 +48,8 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
|
||||
|
||||
return { query: request.body.query ?? '', variables: request.body.variables ?? '' };
|
||||
}, [extraEditorProps.forceUpdateKey]);
|
||||
const [isDocOpen, setGraphqlDocStateAtomValue] = useAtom(showGraphQLDocExplorerAtom);
|
||||
const [isDocOpenRecord, setGraphqlDocStateAtomValue] = useAtom(showGraphQLDocExplorerAtom);
|
||||
const isDocOpen = isDocOpenRecord[request.id] !== undefined;
|
||||
|
||||
const handleChangeQuery = (query: string) => {
|
||||
const newBody = { query, variables: currentBody.variables || undefined };
|
||||
@@ -132,7 +133,10 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
|
||||
label: `${isDocOpen ? 'Hide' : 'Show'} Documentation`,
|
||||
leftSlot: <Icon icon="book_open_text" />,
|
||||
onSelect: () => {
|
||||
setGraphqlDocStateAtomValue(!isDocOpen);
|
||||
setGraphqlDocStateAtomValue((v) => ({
|
||||
...v,
|
||||
[request.id]: isDocOpen ? undefined : null,
|
||||
}));
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -178,16 +182,17 @@ export function GraphQLEditor({ request, onChange, baseRequest, ...extraEditorPr
|
||||
</div>,
|
||||
],
|
||||
[
|
||||
schema,
|
||||
clear,
|
||||
error,
|
||||
isDocOpen,
|
||||
isLoading,
|
||||
refetch,
|
||||
error,
|
||||
autoIntrospectDisabled,
|
||||
baseRequest.id,
|
||||
clear,
|
||||
schema,
|
||||
setAutoIntrospectDisabled,
|
||||
isDocOpen,
|
||||
setGraphqlDocStateAtomValue,
|
||||
request.id,
|
||||
setAutoIntrospectDisabled,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
import { atomWithKVStorage } from '../../lib/atoms/atomWithKVStorage';
|
||||
|
||||
export const showGraphQLDocExplorerAtom = atomWithKVStorage<boolean>('show_graphql_docs', false);
|
||||
export const showGraphQLDocExplorerAtom = atomWithKVStorage<
|
||||
Record<
|
||||
string,
|
||||
| {
|
||||
type?: string;
|
||||
field?: string;
|
||||
parentType?: string;
|
||||
}
|
||||
| null
|
||||
| undefined
|
||||
>
|
||||
>('show_graphql_docs', {});
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import type { DependencyList } from 'react';
|
||||
import { useEffect } from 'react';
|
||||
import { jotaiStore } from '../../lib/jotai';
|
||||
import { sleep } from '../../lib/sleep';
|
||||
import { showGraphQLDocExplorerAtom } from './graphqlAtoms';
|
||||
|
||||
type EventDataMap = {
|
||||
'gql_docs_explorer.show_in_docs': { field?: string; type?: string; parentType?: string };
|
||||
'gql_docs_explorer.focus_tab': undefined;
|
||||
};
|
||||
|
||||
export function useGraphQLDocsExplorerEvent<
|
||||
Event extends keyof EventDataMap,
|
||||
Data extends EventDataMap[Event],
|
||||
>(event: Event, fn: (data: Data) => void, deps?: DependencyList) {
|
||||
useEffect(() => {
|
||||
emitter.on(event, fn);
|
||||
return () => {
|
||||
emitter.off(event, fn);
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps);
|
||||
}
|
||||
|
||||
export async function showInGraphQLDocsExplorer(
|
||||
field: string | undefined,
|
||||
type: string | undefined,
|
||||
parentType: string | undefined,
|
||||
) {
|
||||
const isVisible = jotaiStore.get(showGraphQLDocExplorerAtom);
|
||||
if (!isVisible) {
|
||||
// Show and give some time for the explorer to start listening for events
|
||||
jotaiStore.set(showGraphQLDocExplorerAtom, true);
|
||||
await sleep(100);
|
||||
}
|
||||
emitter.emit('gql_docs_explorer.show_in_docs', { field, type, parentType });
|
||||
}
|
||||
|
||||
const emitter = new (class GraphQLDocsExplorerEventEmitter {
|
||||
#emitter: EventEmitter = new EventEmitter();
|
||||
|
||||
emit<Event extends keyof EventDataMap, Data extends EventDataMap[Event]>(
|
||||
event: Event,
|
||||
data: Data,
|
||||
) {
|
||||
this.#emitter.emit(event, data);
|
||||
}
|
||||
|
||||
on<Event extends keyof EventDataMap, Data extends EventDataMap[Event]>(
|
||||
event: Event,
|
||||
fn: (data: Data) => void,
|
||||
) {
|
||||
this.#emitter.on(event, fn);
|
||||
}
|
||||
|
||||
off<Event extends keyof EventDataMap, Data extends EventDataMap[Event]>(
|
||||
event: Event,
|
||||
fn: (data: Data) => void,
|
||||
) {
|
||||
this.#emitter.off(event, fn);
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user