Custom JSON formatter that works with template syntax (#128)

This commit is contained in:
Gregory Schier
2024-10-21 22:17:14 +00:00
committed by GitHub
parent aa7f18a16f
commit e216214085
17 changed files with 414 additions and 117 deletions

View File

@@ -61,7 +61,7 @@ export interface EditorProps {
onKeyDown?: (e: KeyboardEvent) => void;
singleLine?: boolean;
wrapLines?: boolean;
format?: (v: string) => string;
format?: (v: string) => Promise<string>;
autocomplete?: GenericCompletionConfig;
autocompleteVariables?: boolean;
extraExtensions?: Extension[];
@@ -387,10 +387,10 @@ export const Editor = forwardRef<EditorView | undefined, EditorProps>(function E
icon="magic_wand"
variant="border"
className={classNames(actionClassName)}
onClick={() => {
onClick={async () => {
if (cm.current === null) return;
const { doc } = cm.current.view.state;
const formatted = format(doc.toString());
const formatted = await format(doc.toString());
// Update editor and blur because the cursor will reset anyway
cm.current.view.dispatch({
changes: { from: 0, to: doc.length, insert: formatted },

View File

@@ -4,10 +4,11 @@ import type { ServerSentEvent } from '@yaakapp-internal/sse';
import classNames from 'classnames';
import { motion } from 'framer-motion';
import React, { Fragment, useMemo, useRef, useState } from 'react';
import { useFormatText } from '../../hooks/useFormatText';
import { useResponseBodyEventSource } from '../../hooks/useResponseBodyEventSource';
import { isJSON } from '../../lib/contentType';
import { tryFormatJson } from '../../lib/formatters';
import { Button } from '../core/Button';
import type { EditorProps } from '../core/Editor';
import { Editor } from '../core/Editor';
import { Icon } from '../core/Icon';
import { InlineCode } from '../core/InlineCode';
@@ -95,11 +96,7 @@ function ActualEventStreamViewer({ response }: Props) {
</div>
</VStack>
) : (
<Editor
readOnly
defaultValue={tryFormatJson(activeEvent.data)}
language={language}
/>
<FormattedEditor language={language} text={activeEvent.data} />
)}
</div>
</div>
@@ -110,6 +107,12 @@ function ActualEventStreamViewer({ response }: Props) {
);
}
function FormattedEditor({ text, language }: { text: string; language: EditorProps['language'] }) {
const formatted = useFormatText({ text, language, pretty: true });
if (formatted.data == null) return null;
return <Editor readOnly defaultValue={formatted.data} language={language} />;
}
function EventStreamEventsVirtual({
events,
activeEventIndex,

View File

@@ -31,7 +31,7 @@ export function HTMLOrTextViewer({ response, pretty, textViewerClassName }: Prop
}
if (language === 'html' && pretty) {
return <WebPageViewer response={response} />;
return <WebPageViewer response={response}/>;
} else {
return (
<TextViewer

View File

@@ -5,8 +5,8 @@ import { createGlobalState } from 'react-use';
import { useCopy } from '../../hooks/useCopy';
import { useDebouncedValue } from '../../hooks/useDebouncedValue';
import { useFilterResponse } from '../../hooks/useFilterResponse';
import { useFormatText } from '../../hooks/useFormatText';
import { useToggle } from '../../hooks/useToggle';
import { tryFormatJson, tryFormatXml } from '../../lib/formatters';
import { CopyButton } from '../CopyButton';
import { Banner } from '../core/Banner';
import { Button } from '../core/Button';
@@ -117,6 +117,8 @@ export function TextViewer({
toggleSearch,
]);
const formattedBody = useFormatText({ text, language, pretty });
if (!showLargeResponse && text.length > LARGE_RESPONSE_BYTES) {
return (
<Banner color="primary" className="h-full flex flex-col gap-3">
@@ -140,12 +142,9 @@ export function TextViewer({
);
}
const formattedBody =
pretty && language === 'json'
? tryFormatJson(text)
: pretty && (language === 'xml' || language === 'html')
? tryFormatXml(text)
: text;
if (formattedBody.isFetching) {
return null;
}
let body;
if (isSearching && filterText?.length > 0) {
@@ -155,7 +154,7 @@ export function TextViewer({
body = filteredResponse.data != null ? filteredResponse.data : '';
}
} else {
body = formattedBody;
body = formattedBody.data;
}
return (

View File

@@ -0,0 +1,28 @@
import { useQuery } from '@tanstack/react-query';
import type { EditorProps } from '../components/core/Editor';
import { tryFormatJson, tryFormatXml } from '../lib/formatters';
export function useFormatText({
text,
language,
pretty,
}: {
text: string;
language: EditorProps['language'];
pretty: boolean;
}) {
return useQuery({
queryKey: [text],
queryFn: async () => {
if (text === '' || !pretty) {
return text;
} else if (language === 'json') {
return tryFormatJson(text);
} else if (language === 'xml' || language === 'html') {
return tryFormatXml(text);
} else {
return text;
}
},
});
}

View File

@@ -23,6 +23,7 @@ export function useSyncWorkspaceChildModels() {
const workspaceId = workspace?.id ?? 'n/a';
useEffect(() => {
(async function () {
console.log('Syncing model stores', { workspaceId });
// Set the things we need first, first
setHttpRequests(await invokeCmd('cmd_list_http_requests', { workspaceId }));
setGrpcRequests(await invokeCmd('cmd_list_grpc_requests', { workspaceId }));

View File

@@ -1,20 +1,14 @@
import xmlFormat from 'xml-formatter';
import { invokeCmd } from './tauri';
const INDENT = ' ';
export function tryFormatJson(text: string, pretty = true): string {
export async function tryFormatJson(text: string): Promise<string> {
if (text === '') return text;
try {
if (pretty) return JSON.stringify(JSON.parse(text), null, INDENT);
else return JSON.stringify(JSON.parse(text));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (err) {
return text;
}
return invokeCmd('cmd_format_json', { text });
}
export function tryFormatXml(text: string): string {
export async function tryFormatXml(text: string): Promise<string> {
if (text === '') return text;
try {

View File

@@ -28,6 +28,7 @@ type TauriCmd =
| 'cmd_duplicate_http_request'
| 'cmd_export_data'
| 'cmd_filter_response'
| 'cmd_format_json'
| 'cmd_get_cookie_jar'
| 'cmd_get_environment'
| 'cmd_get_folder'