mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-21 19:04:05 +02:00
Adds a chevron on pair editor name and value inputs that opens the same options as inline autocomplete, so suggestions can be found by clicking instead of only by typing. Long values like User-Agent show a short label but insert the full string. Row action menus move to a vertical ellipsis so the chevron only ever means "pick a value into this field". Also restores the per-worktree Tauri config handling in run-dev.mjs, which a merge on the branch had reverted.
191 lines
5.8 KiB
TypeScript
191 lines
5.8 KiB
TypeScript
import type { HttpRequestHeader } from "@yaakapp-internal/models";
|
|
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 { GenericCompletion, GenericCompletionConfig } from "./core/Editor/genericCompletion";
|
|
import type { InputProps } from "./core/Input";
|
|
import type { Pair, PairEditorProps } from "./core/PairEditor";
|
|
import { PairEditorRow } from "./core/PairEditor";
|
|
import { ensurePairId } from "./core/PairEditor.util";
|
|
import { PairOrBulkEditor } from "./core/PairOrBulkEditor";
|
|
|
|
type Props = {
|
|
forceUpdateKey: string;
|
|
headers: HttpRequestHeader[];
|
|
inheritedHeaders?: HttpRequestHeader[];
|
|
inheritedHeadersLabel?: string;
|
|
stateKey: string;
|
|
onChange: (headers: HttpRequestHeader[]) => void;
|
|
label?: string;
|
|
};
|
|
|
|
export function HeadersEditor({
|
|
stateKey,
|
|
headers,
|
|
inheritedHeaders,
|
|
inheritedHeadersLabel = "Inherited",
|
|
onChange,
|
|
forceUpdateKey,
|
|
}: Props) {
|
|
// Get header names defined at current level (case-insensitive)
|
|
const currentHeaderNames = new Set(
|
|
headers.filter((h) => h.name).map((h) => h.name.toLowerCase()),
|
|
);
|
|
// Filter inherited headers: must be enabled, have content, and not be overridden by current level
|
|
const validInheritedHeaders =
|
|
inheritedHeaders?.filter(
|
|
(pair) =>
|
|
pair.enabled &&
|
|
(pair.name || pair.value) &&
|
|
!currentHeaderNames.has(pair.name.toLowerCase()),
|
|
) ?? [];
|
|
const hasInheritedHeaders = validInheritedHeaders.length > 0;
|
|
return (
|
|
<div
|
|
className={
|
|
hasInheritedHeaders
|
|
? "@container w-full h-full grid grid-rows-[auto_minmax(0,1fr)] gap-y-1.5"
|
|
: "@container w-full h-full"
|
|
}
|
|
>
|
|
{hasInheritedHeaders && (
|
|
<DetailsBanner
|
|
color="secondary"
|
|
className="text-sm"
|
|
summary={
|
|
<HStack>
|
|
{inheritedHeadersLabel} <CountBadge count={validInheritedHeaders.length} />
|
|
</HStack>
|
|
}
|
|
>
|
|
<div className="pb-2">
|
|
{validInheritedHeaders?.map((pair, i) => (
|
|
<PairEditorRow
|
|
key={`${pair.id}.${i}`}
|
|
index={i}
|
|
disabled
|
|
disableDrag
|
|
className="py-1"
|
|
pair={ensurePairId(pair)}
|
|
stateKey={null}
|
|
nameAutocompleteFunctions
|
|
nameAutocompleteVariables
|
|
valueAutocompleteFunctions
|
|
valueAutocompleteVariables
|
|
/>
|
|
))}
|
|
</div>
|
|
</DetailsBanner>
|
|
)}
|
|
<PairOrBulkEditor
|
|
forceUpdateKey={forceUpdateKey}
|
|
nameAutocomplete={nameAutocomplete}
|
|
nameAutocompleteFunctions
|
|
nameAutocompleteVariables
|
|
namePlaceholder="Header-Name"
|
|
nameValidate={validateHttpHeader}
|
|
onChange={onChange}
|
|
pairs={headers}
|
|
preferenceName="headers"
|
|
stateKey={stateKey}
|
|
valueType={valueType}
|
|
valueAutocomplete={valueAutocomplete}
|
|
valueAutocompleteFunctions
|
|
valueAutocompleteVariables
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const MIN_MATCH = 3;
|
|
|
|
const headerOptionsMap: Record<string, HeaderValuePreset[]> = {
|
|
"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"] => {
|
|
const name = pair.name.toLowerCase().trim();
|
|
if (
|
|
name.includes("authorization") ||
|
|
name.includes("api-key") ||
|
|
name.includes("access-token") ||
|
|
name.includes("auth") ||
|
|
name.includes("secret") ||
|
|
name.includes("token") ||
|
|
name === "cookie" ||
|
|
name === "set-cookie"
|
|
) {
|
|
return "password";
|
|
}
|
|
return "text";
|
|
};
|
|
|
|
const valueAutocomplete = (headerName: string): GenericCompletionConfig | undefined => {
|
|
const name = headerName.toLowerCase().trim();
|
|
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 };
|
|
};
|
|
|
|
const nameAutocomplete: PairEditorProps["nameAutocomplete"] = {
|
|
minMatch: MIN_MATCH,
|
|
options: headerNames.map((t) =>
|
|
typeof t === "string"
|
|
? {
|
|
label: t,
|
|
type: "constant",
|
|
boost: 1, // Put above other completions
|
|
}
|
|
: {
|
|
...t,
|
|
boost: 1, // Put above other completions
|
|
},
|
|
),
|
|
};
|
|
|
|
const validateHttpHeader = (v: string) => {
|
|
if (v === "") {
|
|
return true;
|
|
}
|
|
|
|
// Template strings are not allowed so we replace them with a valid example string
|
|
const withoutTemplateStrings = v.replace(/\$\{\[\s*[^\]\s]+\s*]}/gi, "123");
|
|
return withoutTemplateStrings.match(/^[a-zA-Z0-9-_]+$/) !== null;
|
|
};
|