mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-23 18:01:08 +01:00
Fix lint errors and show docs explorer on Cmd click
This commit is contained in:
@@ -3,11 +3,11 @@ import classNames from 'classnames';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import type { CSSProperties } from 'react';
|
||||
import React from 'react';
|
||||
import { showGraphQLDocExplorerAtom } from '../atoms/graphqlSchemaAtom';
|
||||
import { useCurrentGraphQLSchema } from '../hooks/useIntrospectGraphQL';
|
||||
import type { SlotProps } from './core/SplitLayout';
|
||||
import { SplitLayout } from './core/SplitLayout';
|
||||
import { GraphQLDocsExplorer } from './GraphQLDocsExplorer';
|
||||
import { GraphQLDocsExplorer } from './graphql/GraphQLDocsExplorer';
|
||||
import { showGraphQLDocExplorerAtom } from './graphql/graphqlAtoms';
|
||||
import { HttpRequestPane } from './HttpRequestPane';
|
||||
import { HttpResponsePane } from './HttpResponsePane';
|
||||
|
||||
@@ -17,7 +17,6 @@ interface Props {
|
||||
}
|
||||
|
||||
export function HttpRequestLayout({ activeRequest, style }: Props) {
|
||||
const { bodyType } = activeRequest;
|
||||
const showGraphQLDocExplorer = useAtomValue(showGraphQLDocExplorerAtom);
|
||||
const graphQLSchema = useCurrentGraphQLSchema(activeRequest);
|
||||
|
||||
@@ -39,11 +38,11 @@ export function HttpRequestLayout({ activeRequest, style }: Props) {
|
||||
/>
|
||||
);
|
||||
|
||||
if (bodyType === 'graphql' && showGraphQLDocExplorer && graphQLSchema != null) {
|
||||
if (activeRequest.bodyType === 'graphql' && showGraphQLDocExplorer && graphQLSchema != null) {
|
||||
return (
|
||||
<SplitLayout
|
||||
name="graphql_layout"
|
||||
defaultRatio={1/3}
|
||||
defaultRatio={1 / 3}
|
||||
firstSlot={requestResponseSplit}
|
||||
secondSlot={({ style, orientation }) => (
|
||||
<GraphQLDocsExplorer
|
||||
|
||||
@@ -47,13 +47,13 @@ import type { TabItem } from './core/Tabs/Tabs';
|
||||
import { EmptyStateText } from './EmptyStateText';
|
||||
import { FormMultipartEditor } from './FormMultipartEditor';
|
||||
import { FormUrlencodedEditor } from './FormUrlencodedEditor';
|
||||
import { GraphQLEditor } from './GraphQLEditor';
|
||||
import { HeadersEditor } from './HeadersEditor';
|
||||
import { HttpAuthenticationEditor } from './HttpAuthenticationEditor';
|
||||
import { MarkdownEditor } from './MarkdownEditor';
|
||||
import { RequestMethodDropdown } from './RequestMethodDropdown';
|
||||
import { UrlBar } from './UrlBar';
|
||||
import { UrlParametersEditor } from './UrlParameterEditor';
|
||||
import { GraphQLEditor } from './graphql/GraphQLEditor';
|
||||
|
||||
interface Props {
|
||||
style: CSSProperties;
|
||||
|
||||
@@ -6,7 +6,9 @@ import {
|
||||
} from '@codemirror/autocomplete';
|
||||
import { history, historyKeymap } from '@codemirror/commands';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { markdown } from '@codemirror/lang-markdown';
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
import type { LanguageSupport } from '@codemirror/language';
|
||||
import {
|
||||
codeFolding,
|
||||
@@ -35,15 +37,15 @@ import {
|
||||
import { tags as t } from '@lezer/highlight';
|
||||
import type { EnvironmentVariable } from '@yaakapp-internal/models';
|
||||
import { graphql } from 'cm6-graphql';
|
||||
import { renderMarkdown } from '../../../lib/markdown';
|
||||
import { pluralizeCount } from '../../../lib/pluralize';
|
||||
import { showInGraphQLDocsExplorer } from '../../graphql/useGraphQLDocsExplorer';
|
||||
import type { EditorProps } from './Editor';
|
||||
import { pairs } from './pairs/extension';
|
||||
import { text } from './text/extension';
|
||||
import type { TwigCompletionOption } from './twig/completion';
|
||||
import { twig } from './twig/extension';
|
||||
import { pathParametersPlugin } from './twig/pathParameters';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
import { xml } from '@codemirror/lang-xml';
|
||||
import { pairs } from './pairs/extension';
|
||||
import { url } from './url/extension';
|
||||
|
||||
export const syntaxHighlightStyle = HighlightStyle.define([
|
||||
@@ -123,7 +125,22 @@ export function getLanguageExtension({
|
||||
|
||||
// GraphQL is a special exception
|
||||
if (language === 'graphql') {
|
||||
return [graphql(), extraExtensions];
|
||||
return [
|
||||
graphql(undefined, {
|
||||
async onCompletionInfoRender(gqlCompletionItem): Promise<Node | null> {
|
||||
if (!gqlCompletionItem.documentation) return null;
|
||||
const innerHTML = await renderMarkdown(gqlCompletionItem.documentation);
|
||||
const span = document.createElement('span');
|
||||
span.innerHTML = innerHTML;
|
||||
return span;
|
||||
},
|
||||
onShowInDocs(field, type, parentType) {
|
||||
console.log("SHOW IN DOCS", field);
|
||||
showInGraphQLDocsExplorer(field, type, parentType).catch(console.error);
|
||||
},
|
||||
}),
|
||||
extraExtensions,
|
||||
];
|
||||
}
|
||||
|
||||
const base_ = syntaxExtensions[language ?? 'text'] ?? text();
|
||||
@@ -229,7 +246,6 @@ export const multiLineExtensions = ({ hideGutter }: { hideGutter?: boolean }) =>
|
||||
}
|
||||
},
|
||||
}),
|
||||
EditorState.allowMultipleSelections.of(true),
|
||||
indentOnInput(),
|
||||
rectangularSelection(),
|
||||
crosshairCursor(),
|
||||
|
||||
@@ -64,7 +64,7 @@ export function SplitLayout({
|
||||
}
|
||||
|
||||
const size = useContainerSize(containerRef);
|
||||
const verticalBasedOnSize = size.width < STACK_VERTICAL_WIDTH;
|
||||
const verticalBasedOnSize = size.width !== 0 && size.width < STACK_VERTICAL_WIDTH;
|
||||
const vertical = layout !== 'horizontal' && (layout === 'vertical' || verticalBasedOnSize);
|
||||
|
||||
const styles = useMemo<CSSProperties>(() => {
|
||||
@@ -142,31 +142,25 @@ export function SplitLayout({
|
||||
[width, height, vertical, minHeightPx, setHeight, minWidthPx, setWidth],
|
||||
);
|
||||
|
||||
const containerQueryReady = size.width > 0 || size.height > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
style={styles}
|
||||
className={classNames(className, 'grid w-full h-full overflow-hidden')}
|
||||
>
|
||||
{containerQueryReady && (
|
||||
{firstSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })}
|
||||
{secondSlot && (
|
||||
<>
|
||||
{firstSlot({ style: areaL, orientation: vertical ? 'vertical' : 'horizontal' })}
|
||||
{secondSlot && (
|
||||
<>
|
||||
<ResizeHandle
|
||||
style={areaD}
|
||||
isResizing={isResizing}
|
||||
className={classNames(vertical ? '-translate-y-1.5' : '-translate-x-1.5')}
|
||||
onResizeStart={handleResizeStart}
|
||||
onReset={handleReset}
|
||||
side={vertical ? 'top' : 'left'}
|
||||
justify="center"
|
||||
/>
|
||||
{secondSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })}
|
||||
</>
|
||||
)}
|
||||
<ResizeHandle
|
||||
style={areaD}
|
||||
isResizing={isResizing}
|
||||
className={classNames(vertical ? '-translate-y-1.5' : '-translate-x-1.5')}
|
||||
onResizeStart={handleResizeStart}
|
||||
onReset={handleReset}
|
||||
side={vertical ? 'top' : 'left'}
|
||||
justify="center"
|
||||
/>
|
||||
{secondSlot({ style: areaR, orientation: vertical ? 'vertical' : 'horizontal' })}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -23,17 +23,18 @@ import {
|
||||
} from 'graphql';
|
||||
import type { CSSProperties, HTMLAttributes, KeyboardEvent, ReactNode } from 'react';
|
||||
import { Fragment, memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { showGraphQLDocExplorerAtom } from '../atoms/graphqlSchemaAtom';
|
||||
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 { CountBadge } from './core/CountBadge';
|
||||
import { Icon } from './core/Icon';
|
||||
import { IconButton } from './core/IconButton';
|
||||
import { PlainInput } from './core/PlainInput';
|
||||
import { Markdown } from './Markdown';
|
||||
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 { 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;
|
||||
@@ -58,6 +59,17 @@ export const GraphQLDocsExplorer = memo(function GraphQLDocsExplorer({
|
||||
const mutType = schema.getMutationType();
|
||||
const subType = schema.getSubscriptionType();
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const qryItem: ExplorerItem = qryType ? { kind: 'type', type: qryType, from: null } : null;
|
||||
const mutItem: ExplorerItem = mutType ? { kind: 'type', type: mutType, from: null } : null;
|
||||
const subItem: ExplorerItem = subType ? { kind: 'type', type: subType, from: null } : null;
|
||||
@@ -642,22 +654,20 @@ function GqlSchemaSearch({
|
||||
|
||||
const results = useMemo(() => {
|
||||
const results: SearchResult[] = [];
|
||||
walkTypeGraph(
|
||||
currentItem?.type ?? null,
|
||||
(type, from, depth) => {
|
||||
if (type === currentItem?.type) {
|
||||
return null; // Remove the current type from results
|
||||
}
|
||||
walkTypeGraph(schema, currentItem?.type ?? null, (type, from, depth) => {
|
||||
if (type === currentItem?.type) {
|
||||
return true; // Skip the current type and continue
|
||||
}
|
||||
|
||||
const match = fuzzyMatch(type.name, debouncedValue);
|
||||
if (match == null) {
|
||||
// Do nothing
|
||||
} else {
|
||||
results.push({ name: type.name, type, score: match.score, from, depth });
|
||||
}
|
||||
},
|
||||
schema,
|
||||
);
|
||||
const match = fuzzyMatch(type.name, debouncedValue);
|
||||
if (match == null) {
|
||||
// Do nothing
|
||||
} else {
|
||||
results.push({ name: type.name, type, score: match.score, from, depth });
|
||||
}
|
||||
|
||||
return true; // Continue searching
|
||||
});
|
||||
results.sort((a, b) => {
|
||||
if (value == '') {
|
||||
if (a.name.startsWith('_') && !b.name.startsWith('_')) {
|
||||
@@ -831,13 +841,13 @@ function DocMarkdown({ children, className }: { children: string | null; classNa
|
||||
}
|
||||
|
||||
function walkTypeGraph(
|
||||
schema: GraphQLSchema,
|
||||
start: GraphQLType | GraphQLField<any, any> | GraphQLInputField | null,
|
||||
cb: (
|
||||
type: GraphQLNamedType | GraphQLField<any, any> | GraphQLInputField,
|
||||
from: GraphQLNamedType | null,
|
||||
path: string[],
|
||||
) => void,
|
||||
schema: GraphQLSchema,
|
||||
) => boolean,
|
||||
) {
|
||||
const visited = new Set<string>();
|
||||
const queue: Array<{
|
||||
@@ -867,7 +877,8 @@ function walkTypeGraph(
|
||||
if (visited.has(name)) continue;
|
||||
visited.add(name);
|
||||
|
||||
cb(current, from, path);
|
||||
const cont = cb(current, from, path);
|
||||
if (!cont) break;
|
||||
|
||||
if (isObjectType(current) || isInterfaceType(current)) {
|
||||
for (const field of Object.values(current.getFields())) {
|
||||
@@ -6,19 +6,19 @@ import { formatSdl } from 'format-graphql';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import { useLocalStorage } from 'react-use';
|
||||
import { showGraphQLDocExplorerAtom } from '../atoms/graphqlSchemaAtom';
|
||||
import { useIntrospectGraphQL } from '../hooks/useIntrospectGraphQL';
|
||||
import { useStateWithDeps } from '../hooks/useStateWithDeps';
|
||||
import { showDialog } from '../lib/dialog';
|
||||
import { Banner } from './core/Banner';
|
||||
import { Button } from './core/Button';
|
||||
import type { DropdownItem } from './core/Dropdown';
|
||||
import { Dropdown } from './core/Dropdown';
|
||||
import type { EditorProps } from './core/Editor/Editor';
|
||||
import { Editor } from './core/Editor/Editor';
|
||||
import { FormattedError } from './core/FormattedError';
|
||||
import { Icon } from './core/Icon';
|
||||
import { Separator } from './core/Separator';
|
||||
import { useIntrospectGraphQL } from '../../hooks/useIntrospectGraphQL';
|
||||
import { useStateWithDeps } from '../../hooks/useStateWithDeps';
|
||||
import { showDialog } from '../../lib/dialog';
|
||||
import { Banner } from '../core/Banner';
|
||||
import { Button } from '../core/Button';
|
||||
import type { DropdownItem } from '../core/Dropdown';
|
||||
import { Dropdown } from '../core/Dropdown';
|
||||
import type { EditorProps } from '../core/Editor/Editor';
|
||||
import { Editor } from '../core/Editor/Editor';
|
||||
import { FormattedError } from '../core/FormattedError';
|
||||
import { Icon } from '../core/Icon';
|
||||
import { Separator } from '../core/Separator';
|
||||
import { showGraphQLDocExplorerAtom } from './graphqlAtoms';
|
||||
|
||||
type Props = Pick<EditorProps, 'heightMode' | 'className' | 'forceUpdateKey'> & {
|
||||
baseRequest: HttpRequest;
|
||||
@@ -1,3 +1,3 @@
|
||||
import { atomWithKVStorage } from '../lib/atoms/atomWithKVStorage';
|
||||
import { atomWithKVStorage } from '../../lib/atoms/atomWithKVStorage';
|
||||
|
||||
export const showGraphQLDocExplorerAtom = atomWithKVStorage<boolean>('show_graphql_docs', false);
|
||||
63
src-web/components/graphql/useGraphQLDocsExplorer.ts
Normal file
63
src-web/components/graphql/useGraphQLDocsExplorer.ts
Normal file
@@ -0,0 +1,63 @@
|
||||
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);
|
||||
}
|
||||
})();
|
||||
27
src-web/lib/markdown.ts
Normal file
27
src-web/lib/markdown.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import rehypeStringify from 'rehype-stringify';
|
||||
import remarkGfm from 'remark-gfm';
|
||||
import remarkParse from 'remark-parse';
|
||||
import remarkRehype from 'remark-rehype';
|
||||
import { unified } from 'unified';
|
||||
|
||||
const renderer = unified()
|
||||
.use(remarkParse)
|
||||
.use(remarkGfm)
|
||||
.use(remarkRehype, {
|
||||
// handlers: {
|
||||
// link: (state, node, parent) => {
|
||||
// return node;
|
||||
// },
|
||||
// },
|
||||
})
|
||||
.use(rehypeStringify);
|
||||
|
||||
export async function renderMarkdown(md: string): Promise<string> {
|
||||
try {
|
||||
const r = await renderer.process(md);
|
||||
return r.toString();
|
||||
} catch (err) {
|
||||
console.log('FAILED TO RENDER MARKDOWN', err);
|
||||
return 'error';
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite dev --force",
|
||||
"build": "vite build",
|
||||
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||
"lint": "eslint . --ext .ts,.tsx"
|
||||
},
|
||||
"dependencies": {
|
||||
"@codemirror/commands": "^6.8.1",
|
||||
@@ -61,6 +61,8 @@
|
||||
"react-markdown": "^10.1.0",
|
||||
"react-pdf": "^10.0.1",
|
||||
"react-use": "^17.6.0",
|
||||
"rehype-stringify": "^10.0.1",
|
||||
"remark-frontmatter": "^5.0.0",
|
||||
"remark-gfm": "^4.0.1",
|
||||
"slugify": "^1.6.6",
|
||||
"uuid": "^11.1.0",
|
||||
|
||||
Reference in New Issue
Block a user