mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 07:12:12 +03:00
fix(dashboard): migrate ManualConfigModal copy to shared useCopyToClipboard hook (#4502)
Rebuilt onto release/v3.8.33 (squash-base-stale). Integrated into release/v3.8.33.
This commit is contained in:
committed by
GitHub
parent
89606a2bfe
commit
4cf73428f8
@@ -50,6 +50,7 @@ _In development — bullets added per PR; finalized at release._
|
||||
### 🐛 Fixed
|
||||
|
||||
- **fix(sse): combo routing now skips a provider whose credentials are all disabled instead of failing the whole request** — when a combo like `antigravity/opus → github/opus` hit a leg whose only configured connections were disabled (or where no connections existed at all), `handleNoCredentials` returned `400 BAD_REQUEST`, which the combo target loop treats as a hard stop (combo's 400-break guard from PR #4316 / issue #4279 prevents infinite fallback loops on body-specific 4xx errors). The combo therefore died on the first leg even when later targets were perfectly healthy. The no-active-credentials branch now returns `404 NOT_FOUND` with `"No active credentials for provider: <p>"` instead — `404` flows through `checkFallbackError` as `shouldFallback: true` (generic-error catch-all path in `open-sse/services/accountFallback.ts`), so the next combo target is tried. The log level for this branch also drops from `error` to `warn` because zero active credentials is an expected operator-driven state, not a server fault. Inspired-by upstream decolua/9router PR #336. (thanks @East-rayyy)
|
||||
- **fix(dashboard): Manual Config modal "Copy" button now works on HTTP / non-secure deployments** — the copy handler in `ManualConfigModal` re-implemented the Clipboard-API-with-`execCommand`-fallback inline and gated the modern path on `window.isSecureContext`, so some non-secure-context browsers (and any future drift) silently lost the fallback. Migrated to the shared `useCopyToClipboard` hook (which delegates to `src/shared/utils/clipboard.ts`), giving consistent HTTP/HTTPS behavior with the rest of the dashboard and removing the duplicated code path. (thanks @anuragg-saxenaa)
|
||||
- **fix(embeddings):** forward output dimensions to Gemini for consistent embedding dims. (thanks @nguyenha935)
|
||||
- **fix(translator):** sanitize Read tool args from non-Anthropic models to prevent retry loops. (thanks @GodrezJr2)
|
||||
- **fix(usage):** reuse Gemini CLI project ID for quota checks (avoid re-discovery). (thanks @Delcado19)
|
||||
|
||||
@@ -4,34 +4,22 @@ import { useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import Modal from "./Modal";
|
||||
import Button from "./Button";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
|
||||
export default function ManualConfigModal({ isOpen, onClose, title, configs = [] }) {
|
||||
const t = useTranslations("common");
|
||||
const resolvedTitle = title ?? t("manualConfig");
|
||||
const [copiedIndex, setCopiedIndex] = useState(null);
|
||||
const { copy } = useCopyToClipboard();
|
||||
|
||||
const copyToClipboard = async (text, index) => {
|
||||
try {
|
||||
if (navigator.clipboard && window.isSecureContext) {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} else {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.style.position = "fixed";
|
||||
textarea.style.left = "-9999px";
|
||||
textarea.style.top = "-9999px";
|
||||
textarea.style.opacity = "0";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.focus();
|
||||
textarea.select();
|
||||
document.execCommand("copy");
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
setCopiedIndex(index);
|
||||
setTimeout(() => setCopiedIndex(null), 2000);
|
||||
} catch (err) {
|
||||
console.log("Failed to copy:", err);
|
||||
}
|
||||
// Delegates to the shared useCopyToClipboard hook, which transparently
|
||||
// falls back to a hidden textarea + legacy copy command when the
|
||||
// Clipboard API is unavailable (HTTP / non-secure contexts, iframes).
|
||||
const copyConfig = async (text, index) => {
|
||||
const ok = await copy(text, `manualconfig-${index}`);
|
||||
if (!ok) return;
|
||||
setCopiedIndex(index);
|
||||
setTimeout(() => setCopiedIndex(null), 2000);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -44,7 +32,7 @@ export default function ManualConfigModal({ isOpen, onClose, title, configs = []
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => copyToClipboard(config.content, index)}
|
||||
onClick={() => copyConfig(config.content, index)}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">
|
||||
{copiedIndex === index ? "check" : "content_copy"}
|
||||
|
||||
59
tests/unit/manual-config-modal-clipboard.test.ts
Normal file
59
tests/unit/manual-config-modal-clipboard.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Source-guard test: ManualConfigModal must use the shared useCopyToClipboard
|
||||
* hook (which delegates to src/shared/utils/clipboard.ts for HTTP/HTTPS fallback)
|
||||
* rather than re-implementing the navigator.clipboard + execCommand fallback
|
||||
* inline.
|
||||
*
|
||||
* Rationale: duplicated inline fallbacks drift from the canonical helper.
|
||||
* Two known divergences in the previous inline copy:
|
||||
* 1. `window.isSecureContext` gate skipped navigator.clipboard on some
|
||||
* browsers that allow it in non-secure contexts.
|
||||
* 2. No `finally` cleanup if execCommand threw after appendChild succeeded,
|
||||
* leaking a hidden textarea in the DOM.
|
||||
*
|
||||
* The shared helper handles both correctly. This test pins the migration.
|
||||
*/
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, resolve } from "node:path";
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const FILE = resolve(__dirname, "../../src/shared/components/ManualConfigModal.tsx");
|
||||
|
||||
describe("ManualConfigModal — clipboard migration to shared hook", () => {
|
||||
const src = readFileSync(FILE, "utf8");
|
||||
|
||||
it("imports useCopyToClipboard from the shared hooks barrel", () => {
|
||||
assert.match(
|
||||
src,
|
||||
/useCopyToClipboard/,
|
||||
"expected ManualConfigModal to consume the shared useCopyToClipboard hook"
|
||||
);
|
||||
});
|
||||
|
||||
it("does not call navigator.clipboard directly (delegated to the hook)", () => {
|
||||
assert.doesNotMatch(
|
||||
src,
|
||||
/navigator\.clipboard/,
|
||||
"ManualConfigModal must not call navigator.clipboard directly; use the shared hook"
|
||||
);
|
||||
});
|
||||
|
||||
it("does not call document.execCommand('copy') inline (delegated to the hook)", () => {
|
||||
assert.doesNotMatch(
|
||||
src,
|
||||
/document\.execCommand\(\s*["']copy["']\s*\)/,
|
||||
"ManualConfigModal must not inline the execCommand fallback; use the shared hook"
|
||||
);
|
||||
});
|
||||
|
||||
it("does not gate on window.isSecureContext (the shared helper does the right thing)", () => {
|
||||
assert.doesNotMatch(
|
||||
src,
|
||||
/isSecureContext/,
|
||||
"ManualConfigModal must not gate on isSecureContext; the shared helper handles fallback correctly"
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user