Files
opsxapi/src/components/editors/GraphqlEditor.tsx
T
logan 2bb4ef7a95 桌面 GUI 外链修复 + 主题顺序调整(含结构扁平化进行中的工作区基线)
- 集成 tauri-plugin-opener:Cargo/lib.rs/capabilities/npm 四处接线,新增 src/openExternal.ts(桌面走 opener,浏览器回退 window.open,相对路径绝对化)
- 关于页 4 个链接、活动栏官网菜单、代理错误横幅 2 处健康检查链接全部改走 openExternal,桌面端可正常跳转
- 设置→外观 主题顺序调整为 深色/浅色/跟随系统
- release 二进制重编并验证(浏览器实测关于页链接可开文档页)
- 附带工作区中未提交的扁平化重构现状(另一会话进行中)
- homepage 落地页与文档终检通过(8787 / 与 /docs 双 200)
2026-10-16 03:03:36 +08:00

105 lines
4.1 KiB
TypeScript

/**
* GraphQL 编辑器:Query / Variables / Headers / Auth + 响应。
*/
import type { GraphqlApiNode, GraphqlRequestData, KVRow } from "../../core/index";
import { useState } from "react";
import { sendGraphqlRequest } from "../../send";
import { useWorkspace } from "../../workspaceStore";
import { KVEditor, cx } from "../common";
import { Icon } from "../icons";
import { UrlPathInput } from "../UrlPathInput";
import { AuthEditor } from "./HttpEditor";
import { ResponsePanel } from "./ResponsePanel";
const TABS = ["Query", "Variables", "Headers", "Auth"] as const;
type Tab = (typeof TABS)[number];
export function GraphqlEditor({ node }: { node: GraphqlApiNode }) {
const data = node.data;
const update = useWorkspace((s) => s.updateRequestData);
const runtime = useWorkspace((s) => s.runtime[node.id]);
const [tab, setTab] = useState<Tab>("Query");
const patch = (p: Partial<GraphqlRequestData>) => update(node.id, p);
const variablesValid = (() => {
try {
JSON.parse(data.variables || "{}");
return true;
} catch {
return false;
}
})();
return (
<div className="flex min-h-0 flex-1 flex-col">
<div className="flex shrink-0 flex-wrap items-center gap-2 px-3 py-2.5" style={{ borderBottom: "1px solid var(--border)" }}>
<span className="ops-chip inline-flex items-center gap-1" style={{ padding: "2px 10px" }}>POST <Icon name="hexagon" size={12} /> GraphQL</span>
<UrlPathInput
className="ops-input flex-1 font-mono text-[13px]"
placeholder="https://api.example.com/graphql 或 /graphql"
value={data.url}
onChange={(v) => patch({ url: v })}
onKeyDown={(e) => e.key === "Enter" && sendGraphqlRequest(node)}
/>
<button
className="ops-btn ops-btn-primary min-w-[84px]"
disabled={runtime?.loading}
onClick={() => sendGraphqlRequest(node)}
>
<Icon name="play" size={13} /> {runtime?.loading ? "发送中…" : "执行"}
</button>
</div>
<div className="flex shrink-0 px-2" style={{ borderBottom: "1px solid var(--border)" }}>
{TABS.map((t) => (
<button key={t} className={cx("ops-tab", tab === t && "ops-tab-active")} onClick={() => setTab(t)}>
{t}
{t === "Headers" && data.headers.some((h: { enabled: boolean; key: string }) => h.enabled && h.key) ? (
<span className="ops-count">{data.headers.filter((h: { enabled: boolean; key: string }) => h.enabled && h.key).length}</span>
) : null}
</button>
))}
</div>
<div className="ops-scroll shrink-0 p-3.5" style={{ maxHeight: "42%", minHeight: 120 }}>
{tab === "Query" && (
<textarea
className="ops-code min-h-48 w-full resize-y font-mono"
spellCheck={false}
placeholder="query Hero($id: ID!) {\n hero(id: $id) {\n name\n }\n}"
value={data.query}
onChange={(e) => patch({ query: e.target.value })}
/>
)}
{tab === "Variables" && (
<div className="space-y-2">
<textarea
className="ops-code min-h-40 w-full resize-y font-mono"
spellCheck={false}
placeholder='{\n "id": "1"\n}'
value={data.variables}
onChange={(e) => patch({ variables: e.target.value })}
style={{ borderColor: variablesValid ? undefined : "var(--danger)" }}
/>
{!variablesValid && <div className="ops-danger text-[11px]">Variables 不是合法 JSON</div>}
</div>
)}
{tab === "Headers" && (
<KVEditor rows={data.headers} onChange={(rows: KVRow[]) => patch({ headers: rows })} />
)}
{tab === "Auth" && (
<AuthEditor
data={{ ...data, method: "POST", query: [], cookies: [], bodyMode: "json", bodyRaw: "", bodyForm: [] } as never}
patch={(p: { auth?: GraphqlRequestData["auth"] }) => {
if (p.auth) patch({ auth: p.auth });
}}
/>
)}
</div>
<ResponsePanel nodeId={node.id} />
</div>
);
}