mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-21 17:09:37 +01:00
Custom JSON formatter that works with template syntax (#128)
This commit is contained in:
@@ -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 },
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
28
src-web/hooks/useFormatText.ts
Normal file
28
src-web/hooks/useFormatText.ts
Normal 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;
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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 }));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user