import { Icon } from "@yaakapp-internal/ui"; import classNames from "classnames"; import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; import type { JsonPathSegment } from "../core/Editor/json/jsonPath"; import { IconButton } from "../core/IconButton"; import { Tooltip } from "../core/Tooltip"; interface Props { segments: JsonPathSegment[]; appliedFilter: string | null; /** * How many leading segments make up the applied filter. `null` when unfiltered, or * when the filter isn't a plain path and is shown as raw text instead. */ appliedDepth: number | null; filterError: boolean; /** Called with the number of leading segments to filter to. `0` clears the filter. */ onSelect: (count: number) => void; } /** * Shows where the cursor sits inside a response (`$ > sort > [0] > id`). An applied * plain-path filter is the start of the trail, so any crumb moves it. While filtered, * the bar is tinted so a lingering filter is hard to miss. */ export function ResponseBreadcrumbBar({ segments, appliedFilter, appliedDepth, filterError, onSelect, }: Props) { const scrollRef = useRef(null); const [overflow, setOverflow] = useState({ left: false, right: false }); const measure = useCallback(() => { const el = scrollRef.current; if (el == null) return; const maxScroll = el.scrollWidth - el.clientWidth; setOverflow({ left: el.scrollLeft > 1, right: el.scrollLeft < maxScroll - 1, }); }, []); // Keep the deepest crumb (where the cursor is) in view as the path changes. useLayoutEffect(() => { const el = scrollRef.current; if (el == null) return; el.scrollLeft = el.scrollWidth; measure(); }, [segments, measure]); useEffect(() => { const el = scrollRef.current; if (el == null) return; const observer = new ResizeObserver(measure); observer.observe(el); return () => observer.disconnect(); }, [measure]); const scrollBy = useCallback((direction: -1 | 1) => { const el = scrollRef.current; if (el == null) return; el.scrollBy({ left: direction * Math.max(120, el.clientWidth * 0.6), behavior: "smooth" }); }, []); const isFiltered = appliedFilter != null; return (
{isFiltered && appliedDepth == null ? ( {appliedFilter} ) : ( 0 ? () => onSelect(0) : null} /> )} {overflow.left && ( scrollBy(-1)} className="shrink-0 h-auto!" /> )}
{segments.map((segment, i) => { const count = i + 1; const isApplied = appliedDepth != null && count <= appliedDepth; const isClickable = (!isFiltered || appliedDepth != null) && count !== appliedDepth; return (
onSelect(count) : null} />
); })}
{overflow.right && ( scrollBy(1)} className="shrink-0 h-auto!" /> )}
); } function Crumb({ label, tooltip, isApplied, onClick, }: { label: string; tooltip: string; isApplied: boolean; onClick: (() => void) | null; }) { const outerClassName = classNames("flex items-center px-0.5 py-0.5", isApplied && "text-primary"); const innerClassName = "flex items-center h-full px-1 rounded-sm"; if (onClick == null) { return ( {label} ); } return ( ); }