mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-25 12:54:09 +02:00
Add dropdown to pick header names and values
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.
This commit is contained in:
@@ -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<string, string[]> = {
|
||||
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"] => {
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
|
||||
@@ -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 && (
|
||||
<OptionsPickerDropdown options={pickerOptions} onSelect={insertOption} />
|
||||
)}
|
||||
{rightSlot}
|
||||
</HStack>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<DropdownItem[]>(
|
||||
() =>
|
||||
options.map((o) => ({
|
||||
label: o.label,
|
||||
onSelect: () => onSelect(typeof o.apply === "string" ? o.apply : o.label),
|
||||
})),
|
||||
[options, onSelect],
|
||||
);
|
||||
|
||||
return (
|
||||
<Dropdown items={items}>
|
||||
<IconButton
|
||||
tabIndex={-1}
|
||||
iconSize="sm"
|
||||
size="xs"
|
||||
icon="chevron_down"
|
||||
title="Show suggestions"
|
||||
className="mr-0.5 h-auto! my-0.5 text-text-subtlest @max-[24rem]:hidden"
|
||||
/>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
function validateRequire(v: string) {
|
||||
return v.length > 0;
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
<div
|
||||
ref={handleSetRef}
|
||||
@@ -709,6 +713,7 @@ export function PairEditorRow({
|
||||
autocomplete={nameAutocomplete}
|
||||
autocompleteVariables={nameAutocompleteVariables}
|
||||
autocompleteFunctions={nameAutocompleteFunctions}
|
||||
showOptionsPicker={showOptionsPicker}
|
||||
/>
|
||||
<div className="w-full grid grid-cols-[minmax(0,1fr)_auto] gap-1 items-center">
|
||||
{pair.isFile ? (
|
||||
@@ -753,6 +758,7 @@ export function PairEditorRow({
|
||||
autocomplete={valueAutocomplete?.(pair.name)}
|
||||
autocompleteFunctions={valueAutocompleteFunctions}
|
||||
autocompleteVariables={valueAutocompleteVariablesFiltered}
|
||||
showOptionsPicker={showOptionsPicker}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -772,8 +778,10 @@ export function PairEditorRow({
|
||||
<IconButton
|
||||
iconSize="sm"
|
||||
size="xs"
|
||||
icon={isLast || disabled ? "empty" : "chevron_down"}
|
||||
title="Select form data type"
|
||||
// Ellipsis rather than a chevron: the name and value fields now carry chevrons for
|
||||
// picking a suggestion, and this menu acts on the row instead of filling in a field.
|
||||
icon={isLast || disabled ? "empty" : "ellipsis_vertical"}
|
||||
title="More actions"
|
||||
className="text-text-subtlest"
|
||||
/>
|
||||
</Dropdown>
|
||||
@@ -897,7 +905,8 @@ function FileActionsDropdown({
|
||||
<IconButton
|
||||
iconSize="sm"
|
||||
size="xs"
|
||||
icon="chevron_down"
|
||||
// Matches the plain row menu, so the actions column reads the same for file and text rows
|
||||
icon="ellipsis_vertical"
|
||||
title="Select form data type"
|
||||
className="text-text-subtlest"
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user