diff --git a/apps/yaak-client/components/HeadersEditor.tsx b/apps/yaak-client/components/HeadersEditor.tsx index 790ca958..fa1d8010 100644 --- a/apps/yaak-client/components/HeadersEditor.tsx +++ b/apps/yaak-client/components/HeadersEditor.tsx @@ -1,14 +1,17 @@ import type { HttpRequestHeader } from "@yaakapp-internal/models"; -import type { GenericCompletionOption } from "@yaakapp-internal/plugins"; import { HStack } from "@yaakapp-internal/ui"; +import { acceptLanguages } from "../lib/data/acceptLanguages"; +import { cacheControlDirectives } from "../lib/data/cacheControl"; import { charsets } from "../lib/data/charsets"; import { connections } from "../lib/data/connections"; import { encodings } from "../lib/data/encodings"; +import type { HeaderValuePreset } from "../lib/data/headerValuePresets"; import { headerNames } from "../lib/data/headerNames"; import { mimeTypes } from "../lib/data/mimetypes"; +import { userAgents } from "../lib/data/userAgents"; import { CountBadge } from "./core/CountBadge"; import { DetailsBanner } from "./core/DetailsBanner"; -import type { GenericCompletionConfig } from "./core/Editor/genericCompletion"; +import type { GenericCompletion, GenericCompletionConfig } from "./core/Editor/genericCompletion"; import type { InputProps } from "./core/Input"; import type { Pair, PairEditorProps } from "./core/PairEditor"; import { PairEditorRow } from "./core/PairEditor"; @@ -105,12 +108,21 @@ export function HeadersEditor({ const MIN_MATCH = 3; -const headerOptionsMap: Record = { +const headerOptionsMap: Record = { "content-type": mimeTypes, accept: ["*/*", ...mimeTypes], "accept-encoding": encodings, + "content-encoding": encodings, connection: connections, "accept-charset": charsets, + "accept-language": acceptLanguages, + "cache-control": cacheControlDirectives, + "user-agent": userAgents, + pragma: ["no-cache"], + te: ["trailers", "compress", "deflate", "gzip"], + dnt: ["1", "0"], + "upgrade-insecure-requests": ["1"], + "x-requested-with": ["XMLHttpRequest"], }; const valueType = (pair: Pair): InputProps["type"] => { @@ -132,12 +144,22 @@ const valueType = (pair: Pair): InputProps["type"] => { const valueAutocomplete = (headerName: string): GenericCompletionConfig | undefined => { const name = headerName.toLowerCase().trim(); - const options: GenericCompletionOption[] = - headerOptionsMap[name]?.map((o) => ({ - label: o, - type: "constant", - boost: 1, // Put above other completions - })) ?? []; + const options: GenericCompletion[] = + headerOptionsMap[name]?.map((o) => + typeof o === "string" + ? { + label: o, + type: "constant", + boost: 1, // Put above other completions + } + : { + // Show the short label but insert the full value (e.g. User-Agent) + label: o.label, + apply: o.value, + type: "constant", + boost: 1, + }, + ) ?? []; return { minMatch: MIN_MATCH, options }; }; diff --git a/apps/yaak-client/components/core/Input.tsx b/apps/yaak-client/components/core/Input.tsx index 8d74eaaf..c0063274 100644 --- a/apps/yaak-client/components/core/Input.tsx +++ b/apps/yaak-client/components/core/Input.tsx @@ -22,6 +22,7 @@ import { Button } from "./Button"; import type { DropdownItem } from "./Dropdown"; import { Dropdown } from "./Dropdown"; import type { EditorProps } from "./Editor/Editor"; +import type { GenericCompletion } from "./Editor/genericCompletion"; import { Editor } from "./Editor/LazyEditor"; import { IconButton } from "./IconButton"; import { IconTooltip } from "./IconTooltip"; @@ -63,6 +64,13 @@ export type InputProps = Pick< placeholder?: string; required?: boolean; rightSlot?: ReactNode; + /** + * Show a chevron that opens the `autocomplete` options as a menu, so a value can be picked + * without knowing what to type. Opt-in, because plenty of inputs pass `autocomplete` without + * wanting the affordance (the URL bar completes from history, and a chevron there would be + * noise). Renders nothing when there are no options to show. + */ + showOptionsPicker?: boolean; size?: "2xs" | "xs" | "sm" | "md" | "auto"; stateKey: EditorProps["stateKey"]; extraExtensions?: EditorProps["extraExtensions"]; @@ -115,6 +123,7 @@ function BaseInput({ readOnly, required, rightSlot, + showOptionsPicker, size = "md", stateKey, tint, @@ -156,6 +165,17 @@ function BaseInput({ [], ); + // Replace the whole value with a picked option. Dispatching runs it through the editor's + // onChange, same as typing, so callers see one consistent change path. + const insertOption = useCallback((value: string) => { + const view = editorRef.current; + if (view == null) return; + view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: value } }); + view.focus(); + }, []); + + const pickerOptions = showOptionsPicker && !disabled ? (props.autocomplete?.options ?? []) : []; + const setEditorRef = useCallback( (h: EditorView | null) => { editorRef.current = h; @@ -350,12 +370,55 @@ function BaseInput({ onClick={() => setObscured((o) => !o)} /> )} + {pickerOptions.length > 0 && ( + + )} {rightSlot} ); } +/** + * Chevron that opens the input's autocomplete options as a menu, for browsing values rather than + * typing them. Shows `label` but inserts `apply` when set, so a long value can be listed under a + * short name (e.g. a User-Agent). + * + * Skipped in the tab order: once focus is in the input, inline autocomplete offers the same + * options, so this would only add a stop between fields. Hidden when the nearest container is + * narrow and the input needs the width more — written as a `max` query so it stays visible for + * inputs that have no container ancestor at all. + */ +function OptionsPickerDropdown({ + options, + onSelect, +}: { + options: GenericCompletion[]; + onSelect: (value: string) => void; +}) { + const items = useMemo( + () => + options.map((o) => ({ + label: o.label, + onSelect: () => onSelect(typeof o.apply === "string" ? o.apply : o.label), + })), + [options, onSelect], + ); + + return ( + + + + ); +} + function validateRequire(v: string) { return v.length > 0; } diff --git a/apps/yaak-client/components/core/PairEditor.tsx b/apps/yaak-client/components/core/PairEditor.tsx index be5f85f5..72352fc2 100644 --- a/apps/yaak-client/components/core/PairEditor.tsx +++ b/apps/yaak-client/components/core/PairEditor.tsx @@ -647,6 +647,10 @@ export function PairEditorRow({ [setDraggableRef, setDroppableRef], ); + // Skip the trailing placeholder row. It exists to start a new pair, so a picker there would + // imply it's already a real row. `Input` handles the rest, including having no options to show. + const showOptionsPicker = !isLast; + return (
{pair.isFile ? ( @@ -753,6 +758,7 @@ export function PairEditorRow({ autocomplete={valueAutocomplete?.(pair.name)} autocompleteFunctions={valueAutocompleteFunctions} autocompleteVariables={valueAutocompleteVariablesFiltered} + showOptionsPicker={showOptionsPicker} /> )}
@@ -772,8 +778,10 @@ export function PairEditorRow({ @@ -897,7 +905,8 @@ function FileActionsDropdown({ diff --git a/apps/yaak-client/lib/data/acceptLanguages.ts b/apps/yaak-client/lib/data/acceptLanguages.ts new file mode 100644 index 00000000..2f0b5a3a --- /dev/null +++ b/apps/yaak-client/lib/data/acceptLanguages.ts @@ -0,0 +1,15 @@ +// Common Accept-Language values. +export const acceptLanguages = [ + "*", + "en", + "en-US", + "en-US,en;q=0.9", + "en-GB,en;q=0.9", + "de-DE,de;q=0.9,en;q=0.8", + "fr-FR,fr;q=0.9,en;q=0.8", + "es-ES,es;q=0.9,en;q=0.8", + "ru-RU,ru;q=0.9,en;q=0.8", + "uk-UA,uk;q=0.9,en;q=0.8", + "zh-CN,zh;q=0.9,en;q=0.8", + "ja-JP,ja;q=0.9,en;q=0.8", +]; diff --git a/apps/yaak-client/lib/data/cacheControl.ts b/apps/yaak-client/lib/data/cacheControl.ts new file mode 100644 index 00000000..ea69436c --- /dev/null +++ b/apps/yaak-client/lib/data/cacheControl.ts @@ -0,0 +1,16 @@ +// Common Cache-Control directives (request and response). +export const cacheControlDirectives = [ + "no-cache", + "no-store", + "no-transform", + "max-age=0", + "max-age=3600", + "max-age=86400", + "s-maxage=3600", + "must-revalidate", + "proxy-revalidate", + "stale-while-revalidate=60", + "public", + "private", + "immutable", +]; diff --git a/apps/yaak-client/lib/data/headerValuePresets.ts b/apps/yaak-client/lib/data/headerValuePresets.ts new file mode 100644 index 00000000..7fef3abd --- /dev/null +++ b/apps/yaak-client/lib/data/headerValuePresets.ts @@ -0,0 +1,7 @@ +/** + * A suggested value for a header. A plain string is used when the displayed + * label and the inserted value are the same (e.g. mime types). The object form + * lets us show a short, readable `label` (e.g. "Chrome (Windows)") while + * inserting a longer `value` (the full User-Agent string). + */ +export type HeaderValuePreset = string | { label: string; value: string }; diff --git a/apps/yaak-client/lib/data/userAgents.ts b/apps/yaak-client/lib/data/userAgents.ts new file mode 100644 index 00000000..f7e45b2a --- /dev/null +++ b/apps/yaak-client/lib/data/userAgents.ts @@ -0,0 +1,51 @@ +import type { HeaderValuePreset } from "./headerValuePresets"; + +// Common, real-world User-Agent strings. The short `label` is shown in the +// dropdown/autocomplete, while the full UA string is what gets inserted. +// +// Browsers freeze parts of their UA: Chrome reports its minor version as 0.0.0 +// and Safari reports macOS as 10_15_7, regardless of the actual version. +export const userAgents: HeaderValuePreset[] = [ + { + label: "Chrome 151 · Windows 10/11 · x64", + value: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36", + }, + { + label: "Edge 151 · Windows 10/11 · x64", + value: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36 Edg/151.0.0.0", + }, + { + label: "Firefox 153 · Windows 10/11 · x64", + value: "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:153.0) Gecko/20100101 Firefox/153.0", + }, + { + label: "Chrome 151 · macOS", + value: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36", + }, + { + label: "Safari 27 · macOS", + value: + "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/27.0 Safari/605.1.15", + }, + { + label: "Chrome 151 · Linux · x64", + value: + "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Safari/537.36", + }, + { + label: "Safari 27 · iOS 27 · iPhone", + value: + "Mozilla/5.0 (iPhone; CPU iPhone OS 27_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/27.0 Mobile/15E148 Safari/604.1", + }, + { + label: "Chrome 151 · Android 16 · Pixel 10", + value: + "Mozilla/5.0 (Linux; Android 16; Pixel 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/151.0.0.0 Mobile Safari/537.36", + }, + { label: "curl 8.21.0 · CLI", value: "curl/8.21.0" }, + { label: "Postman 7.56 · API client", value: "PostmanRuntime/7.56.1" }, + { label: "Insomnia 13.1 · API client", value: "insomnia/13.1.0" }, +];