import type { ReactNode } from "react"; import { Children, useCallback, useMemo } from "react"; import { Banner, HStack, Icon, InlineCode } from "@yaakapp-internal/ui"; import { useFormatText } from "../../hooks/useFormatText"; import type { ResponseFilterApi } from "../../hooks/useResponseFilter"; import { Button } from "../core/Button"; import type { EditorProps } from "../core/Editor/Editor"; import { hyperlink } from "../core/Editor/hyperlink/extension"; import { Editor } from "../core/Editor/LazyEditor"; import { IconButton } from "../core/IconButton"; import { Input } from "../core/Input"; import { RecentFiltersDropdown } from "./RecentFiltersDropdown"; const extraExtensions = [hyperlink]; interface Props { text: string; language: EditorProps["language"]; stateKey: string | null; pretty?: boolean; className?: string; footerActions?: ReactNode; filter?: ResponseFilterApi; filterResult?: { data: string | null | undefined; isPending: boolean; error: boolean; }; } export function TextViewer({ language, text, stateKey, pretty, className, footerActions, filter, filterResult, }: Props) { const canFilter = filter != null && (language === "json" || language === "xml" || language === "html"); const isSearching = filter?.isSearching ?? false; const appliedFilter = filter?.appliedFilter ?? null; const resultError = filterResult?.error ?? false; const handleFilterKeyDown = useCallback( (e: KeyboardEvent) => { if (filter == null) return; if (e.key === "Escape") { filter.toggleSearch(); } else if (e.key === "Enter" && filter.filterText != null) { filter.applyFilter(filter.filterText); } }, [filter], ); const actions = useMemo(() => { const nodes: ReactNode[] = isSearching ? [] : Children.toArray(footerActions); if (!canFilter) return nodes; if (isSearching) { nodes.push(
} rightSlot={
} /> , ); } else { nodes.push( , ); } return nodes; }, [ canFilter, footerActions, filter, filterResult?.isPending, resultError, isSearching, language, handleFilterKeyDown, ]); const formattedBody = useFormatText({ text, language, pretty: pretty ?? false }); if (formattedBody == null) { return null; } let body: string; if (appliedFilter) { if (resultError) { body = ""; } else { body = filterResult?.data != null ? filterResult.data : ""; } } else { body = formattedBody; } // Decode unicode sequences in the text to readable characters if (language === "json" && pretty) { body = decodeUnicodeLiterals(body); body = body.replace(/\\\//g, "/"); // Hide unnecessary escaping of '/' by some older frameworks } return (
{appliedFilter && filter != null ? ( filter.replaceFilter("")} /> ) : ( )}
); } /** * Shows what's actually filtering the body, which the filter box below can't convey * once it holds an edited expression that hasn't been applied yet. */ function AppliedFilterBar({ filter, error, onClear, }: { filter: string; error: boolean; onClear: () => void; }) { return ( Response filtered by {filter} {error && " (invalid expression)"} ); } /** Convert \uXXXX to actual Unicode characters */ function decodeUnicodeLiterals(text: string): string { return text.replace(/\\u([0-9a-fA-F]{4})/g, (_, hex) => { const charCode = Number.parseInt(hex, 16); return String.fromCharCode(charCode); }); }