mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-03-24 02:11:25 +01:00
Add .oxfmtignore to skip generated bindings and wasm-pack output. Add npm format script, update DEVELOPMENT.md for Vite+ toolchain, and format all non-generated files with oxfmt. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
169 lines
5.1 KiB
TypeScript
169 lines
5.1 KiB
TypeScript
import type { HttpRequestHeader } from "@yaakapp-internal/models";
|
|
import type { GenericCompletionOption } from "@yaakapp-internal/plugins";
|
|
import { charsets } from "../lib/data/charsets";
|
|
import { connections } from "../lib/data/connections";
|
|
import { encodings } from "../lib/data/encodings";
|
|
import { headerNames } from "../lib/data/headerNames";
|
|
import { mimeTypes } from "../lib/data/mimetypes";
|
|
import { CountBadge } from "./core/CountBadge";
|
|
import { DetailsBanner } from "./core/DetailsBanner";
|
|
import type { 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";
|
|
import { HStack } from "./core/Stacks";
|
|
|
|
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, string[]> = {
|
|
"content-type": mimeTypes,
|
|
accept: ["*/*", ...mimeTypes],
|
|
"accept-encoding": encodings,
|
|
connection: connections,
|
|
"accept-charset": charsets,
|
|
};
|
|
|
|
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: GenericCompletionOption[] =
|
|
headerOptionsMap[name]?.map((o) => ({
|
|
label: o,
|
|
type: "constant",
|
|
boost: 1, // Put above other completions
|
|
})) ?? [];
|
|
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;
|
|
};
|