Better content-type detection for editor

This commit is contained in:
Gregory Schier
2024-08-29 06:07:31 -07:00
parent 690ef02a38
commit fdc60445c8
13 changed files with 53 additions and 32 deletions

View File

@@ -128,7 +128,7 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
return ( return (
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto]"> <div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto]">
<Editor <Editor
contentType="application/graphql" language="graphql"
defaultValue={query ?? ''} defaultValue={query ?? ''}
format={formatGraphQL} format={formatGraphQL}
heightMode="auto" heightMode="auto"
@@ -144,7 +144,7 @@ export function GraphQLEditor({ defaultValue, onChange, baseRequest, ...extraEdi
</Separator> </Separator>
<Editor <Editor
format={tryFormatJson} format={tryFormatJson}
contentType="application/json" language="json"
defaultValue={JSON.stringify(variables, null, 2)} defaultValue={JSON.stringify(variables, null, 2)}
heightMode="auto" heightMode="auto"
onChange={handleChangeVariables} onChange={handleChangeVariables}

View File

@@ -185,7 +185,7 @@ export function GrpcEditor({
return ( return (
<div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]"> <div className="h-full w-full grid grid-cols-1 grid-rows-[minmax(0,100%)_auto_auto_minmax(0,auto)]">
<Editor <Editor
contentType="application/json" language="json"
autocompleteVariables autocompleteVariables
useTemplating useTemplating
forceUpdateKey={request.id} forceUpdateKey={request.id}

View File

@@ -12,6 +12,7 @@ import { useRequests } from '../hooks/useRequests';
import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey'; import { useRequestUpdateKey } from '../hooks/useRequestUpdateKey';
import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest'; import { useSendAnyHttpRequest } from '../hooks/useSendAnyHttpRequest';
import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest'; import { useUpdateAnyHttpRequest } from '../hooks/useUpdateAnyHttpRequest';
import { languageFromContentType } from '../lib/contentType';
import { tryFormatJson } from '../lib/formatters'; import { tryFormatJson } from '../lib/formatters';
import { import {
AUTH_TYPE_BASIC, AUTH_TYPE_BASIC,
@@ -355,7 +356,7 @@ export const RequestPane = memo(function RequestPane({
placeholder="..." placeholder="..."
heightMode={fullHeight ? 'full' : 'auto'} heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={`${activeRequest.body?.text ?? ''}`} defaultValue={`${activeRequest.body?.text ?? ''}`}
contentType="application/json" language="json"
onChange={handleBodyTextChange} onChange={handleBodyTextChange}
format={tryFormatJson} format={tryFormatJson}
/> />
@@ -367,7 +368,7 @@ export const RequestPane = memo(function RequestPane({
placeholder="..." placeholder="..."
heightMode={fullHeight ? 'full' : 'auto'} heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={`${activeRequest.body?.text ?? ''}`} defaultValue={`${activeRequest.body?.text ?? ''}`}
contentType="text/xml" language="xml"
onChange={handleBodyTextChange} onChange={handleBodyTextChange}
/> />
) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? ( ) : activeRequest.bodyType === BODY_TYPE_GRAPHQL ? (
@@ -402,6 +403,7 @@ export const RequestPane = memo(function RequestPane({
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
useTemplating useTemplating
autocompleteVariables autocompleteVariables
language={languageFromContentType(contentType)}
placeholder="..." placeholder="..."
heightMode={fullHeight ? 'full' : 'auto'} heightMode={fullHeight ? 'full' : 'auto'}
defaultValue={`${activeRequest.body?.text ?? ''}`} defaultValue={`${activeRequest.body?.text ?? ''}`}

View File

@@ -198,7 +198,7 @@ export function SettingsAppearance() {
'};', '};',
].join('\n')} ].join('\n')}
heightMode="auto" heightMode="auto"
contentType="application/javascript" language="javascript"
/> />
</VStack> </VStack>
</VStack> </VStack>

View File

@@ -117,7 +117,7 @@ export function SettingsDesign() {
'};', '};',
].join('\n')} ].join('\n')}
heightMode="auto" heightMode="auto"
contentType="application/javascript" language="javascript"
/> />
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="flex flex-wrap gap-1"> <div className="flex flex-wrap gap-1">

View File

@@ -67,7 +67,7 @@ export const UrlBar = memo(function UrlBar({
wrapLines={isFocused} wrapLines={isFocused}
hideLabel hideLabel
useTemplating useTemplating
contentType="url" language="url"
className="pl-0 pr-1.5 py-0.5" className="pl-0 pr-1.5 py-0.5"
name="url" name="url"
label="Enter URL" label="Enter URL"

View File

@@ -36,7 +36,7 @@ export function BulkPairEditor({
forceUpdateKey={forceUpdateKey} forceUpdateKey={forceUpdateKey}
placeholder={`${namePlaceholder ?? 'name'}: ${valuePlaceholder ?? 'value'}`} placeholder={`${namePlaceholder ?? 'name'}: ${valuePlaceholder ?? 'value'}`}
defaultValue={pairsText} defaultValue={pairsText}
contentType="pairs" language="pairs"
onChange={handleChange} onChange={handleChange}
/> />
); );

View File

@@ -42,7 +42,7 @@ export interface EditorProps {
type?: 'text' | 'password'; type?: 'text' | 'password';
className?: string; className?: string;
heightMode?: 'auto' | 'full'; heightMode?: 'auto' | 'full';
contentType?: string | null; language?: 'javascript' | 'json' | 'html' | 'xml' | 'graphql' | 'url' | 'pairs' | 'text';
forceUpdateKey?: string | number; forceUpdateKey?: string | number;
autoFocus?: boolean; autoFocus?: boolean;
autoSelect?: boolean; autoSelect?: boolean;
@@ -71,7 +71,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
readOnly, readOnly,
type = 'text', type = 'text',
heightMode, heightMode,
contentType, language = 'text',
autoFocus, autoFocus,
autoSelect, autoSelect,
placeholder, placeholder,
@@ -226,12 +226,12 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
[dialog], [dialog],
); );
// Update language extension when contentType changes // Update the language extension when the language changes
useEffect(() => { useEffect(() => {
if (cm.current === null) return; if (cm.current === null) return;
const { view, languageCompartment } = cm.current; const { view, languageCompartment } = cm.current;
const ext = getLanguageExtension({ const ext = getLanguageExtension({
contentType, language,
environmentVariables, environmentVariables,
useTemplating, useTemplating,
autocomplete, autocomplete,
@@ -242,7 +242,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
}); });
view.dispatch({ effects: languageCompartment.reconfigure(ext) }); view.dispatch({ effects: languageCompartment.reconfigure(ext) });
}, [ }, [
contentType, language,
autocomplete, autocomplete,
useTemplating, useTemplating,
environmentVariables, environmentVariables,
@@ -265,7 +265,7 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
try { try {
const languageCompartment = new Compartment(); const languageCompartment = new Compartment();
const langExt = getLanguageExtension({ const langExt = getLanguageExtension({
contentType, language,
useTemplating, useTemplating,
autocomplete, autocomplete,
environmentVariables, environmentVariables,

View File

@@ -32,7 +32,7 @@ import {
} from '@codemirror/view'; } from '@codemirror/view';
import { tags as t } from '@lezer/highlight'; import { tags as t } from '@lezer/highlight';
import type { EnvironmentVariable, TemplateFunction } from '@yaakapp/api'; import type { EnvironmentVariable, TemplateFunction } from '@yaakapp/api';
import { graphql, graphqlLanguageSupport } from 'cm6-graphql'; import { graphql } from 'cm6-graphql';
import { EditorView } from 'codemirror'; import { EditorView } from 'codemirror';
import type { EditorProps } from './index'; import type { EditorProps } from './index';
import { pairs } from './pairs/extension'; import { pairs } from './pairs/extension';
@@ -64,19 +64,19 @@ export const syntaxHighlightStyle = HighlightStyle.define([
const syntaxTheme = EditorView.theme({}, { dark: true }); const syntaxTheme = EditorView.theme({}, { dark: true });
const syntaxExtensions: Record<string, LanguageSupport> = { const syntaxExtensions: Record<NonNullable<EditorProps['language']>, LanguageSupport | null> = {
'application/graphql': graphqlLanguageSupport(), graphql: null,
'application/json': json(), json: json(),
'application/javascript': javascript(), javascript: javascript(),
'text/html': xml(), // HTML as XML because HTML is oddly slow html: xml(), // HTML as XML because HTML is oddly slow
'application/xml': xml(), xml: xml(),
'text/xml': xml(),
url: url(), url: url(),
pairs: pairs(), pairs: pairs(),
text: text(),
}; };
export function getLanguageExtension({ export function getLanguageExtension({
contentType, language,
useTemplating = false, useTemplating = false,
environmentVariables, environmentVariables,
autocomplete, autocomplete,
@@ -90,12 +90,12 @@ export function getLanguageExtension({
onClickFunction: (option: TemplateFunction, tagValue: string, startPos: number) => void; onClickFunction: (option: TemplateFunction, tagValue: string, startPos: number) => void;
onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void; onClickVariable: (option: EnvironmentVariable, tagValue: string, startPos: number) => void;
onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void; onClickMissingVariable: (name: string, tagValue: string, startPos: number) => void;
} & Pick<EditorProps, 'contentType' | 'useTemplating' | 'autocomplete'>) { } & Pick<EditorProps, 'language' | 'useTemplating' | 'autocomplete'>) {
const justContentType = contentType?.split(';')[0] ?? contentType ?? ''; if (language === 'graphql') {
if (justContentType === 'application/graphql') {
return graphql(); return graphql();
} }
const base = syntaxExtensions[justContentType] ?? text();
const base = syntaxExtensions[language ?? 'text'] ?? text();
if (!useTemplating) { if (!useTemplating) {
return base; return base;
} }

View File

@@ -14,7 +14,7 @@ export type InputProps = Omit<
> & > &
Pick< Pick<
EditorProps, EditorProps,
| 'contentType' | 'language'
| 'useTemplating' | 'useTemplating'
| 'autocomplete' | 'autocomplete'
| 'forceUpdateKey' | 'forceUpdateKey'

View File

@@ -1,3 +1,4 @@
import type { HttpResponse } from '@yaakapp/api';
import classNames from 'classnames'; import classNames from 'classnames';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
@@ -8,8 +9,8 @@ import { useFilterResponse } from '../../hooks/useFilterResponse';
import { useResponseBodyText } from '../../hooks/useResponseBodyText'; import { useResponseBodyText } from '../../hooks/useResponseBodyText';
import { useSaveResponse } from '../../hooks/useSaveResponse'; import { useSaveResponse } from '../../hooks/useSaveResponse';
import { useToggle } from '../../hooks/useToggle'; import { useToggle } from '../../hooks/useToggle';
import { languageFromContentType } from '../../lib/contentType';
import { tryFormatJson, tryFormatXml } from '../../lib/formatters'; import { tryFormatJson, tryFormatXml } from '../../lib/formatters';
import type { HttpResponse } from '@yaakapp/api';
import { CopyButton } from '../CopyButton'; import { CopyButton } from '../CopyButton';
import { Banner } from '../core/Banner'; import { Banner } from '../core/Banner';
import { Button } from '../core/Button'; import { Button } from '../core/Button';
@@ -63,6 +64,8 @@ export function TextViewer({ response, pretty, className }: Props) {
} }
}, [isSearching, setFilterText]); }, [isSearching, setFilterText]);
console.log('HELLO', contentType);
const isJson = contentType?.includes('json'); const isJson = contentType?.includes('json');
const isXml = contentType?.includes('xml') || contentType?.includes('html'); const isXml = contentType?.includes('xml') || contentType?.includes('html');
const canFilter = isJson || isXml; const canFilter = isJson || isXml;
@@ -176,7 +179,7 @@ export function TextViewer({ response, pretty, className }: Props) {
className={className} className={className}
forceUpdateKey={body} forceUpdateKey={body}
defaultValue={body} defaultValue={body}
contentType={contentType} language={languageFromContentType(contentType)}
actions={actions} actions={actions}
extraExtensions={extraExtensions} extraExtensions={extraExtensions}
/> />

View File

@@ -1,5 +1,5 @@
import { useMemo } from 'react';
import type { HttpResponseHeader } from '@yaakapp/api'; import type { HttpResponseHeader } from '@yaakapp/api';
import { useMemo } from 'react';
export function useContentTypeFromHeaders(headers: HttpResponseHeader[] | null): string | null { export function useContentTypeFromHeaders(headers: HttpResponseHeader[] | null): string | null {
return useMemo( return useMemo(

View File

@@ -0,0 +1,16 @@
import type { EditorProps } from '../components/core/Editor';
export function languageFromContentType(contentType: string | null): EditorProps['language'] {
const justContentType = contentType?.split(';')[0] ?? contentType ?? '';
if (justContentType.includes('json')) {
return 'json';
} else if (justContentType.includes('xml')) {
return 'xml';
} else if (justContentType.includes('html')) {
return 'html';
} else if (justContentType.includes('javascript')) {
return 'javascript';
} else {
return 'text';
}
}