mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-05 14:52:09 +03:00
feat(dashboard): Done button in model picker for combo creation
Adds an opt-in `keepOpenOnSelect` prop to `ModelSelectModal`. When the combos page sets it, picking a model no longer auto-closes the modal and a full-width "Done" button is rendered in the footer (via Modal's `footer` slot) so users can add several models in a row and confirm explicitly. All other single-select callers keep the existing auto-close-on-select behaviour (the prop is off by default). When `multiSelect` is on, the existing Clear+Done footer wins to avoid two competing footers. The upstream JS variant gates the Done button on `!closeOnSelect`. TDD coverage: tests/unit/ui/model-select-modal-keep-open.test.tsx asserts the Done button is absent by default, present when `keepOpenOnSelect=true`, fires only `onClose` (not a stray `onSelect`), and that `multiSelect` still renders exactly one Done button. Inspired-by: decolua/9router#1031 Co-authored-by: Zanuar Tri Romadon <triromadon@gmail.com>
This commit is contained in:
@@ -44,6 +44,7 @@ _In development — bullets added per PR; finalized at release._
|
||||
- **feat(pricing): default pricing for Qwen `coder-model` on the `qw` provider** — the Qwen Coder Free (`qw`) registry already exposed the `coder-model` id (Qwen3.5/3.6 Coder Model) but `DEFAULT_PRICING.qw` was missing the row, so usage tracking reported `$0.00` for that model. The pricing row is now added with the same shape as the sibling `vision-model` tier, restoring non-zero cost tracking. Ported from upstream 9router PR [decolua/9router#156](https://github.com/decolua/9router/pull/156). (thanks @LinearSakana)
|
||||
- **feat(usage): Codex review-quota now surfaces the weekly window and the `additional_rate_limits` fallback shape** — the dashboard's Codex usage card showed only the **session** half of `code_review_rate_limit` and dropped review descriptors that arrived inside `additional_rate_limits` (the shape some ChatGPT Codex plans report). `buildCodexUsageQuotas` now emits the secondary window as `quotas.code_review_weekly` and, when the dedicated `code_review_rate_limit` block is empty, falls back to the matching descriptor in `additional_rate_limits` (matched on `limit_name`/`metered_feature`/`limit_id` containing `code_review` / `codex_review` / `review`). The new label `code_review_weekly → "Code Review Weekly"` is registered in `ProviderLimits/utils.tsx` so the card renders both windows side-by-side. The existing `quotas.code_review` key is preserved for back-compat. Inspired by upstream decolua/9router PR #836. (thanks @hiepau1231)
|
||||
- **feat(dashboard): per-provider dropdown filter on the quota dashboard** — the Quota dashboard now has a "Provider" dropdown alongside the existing Status / Type / Tier / Env filters. Choosing a provider narrows the visible accounts to that provider only; the selection persists in `localStorage` (`omniroute:limits:providerFilter`) and the dropdown auto-falls back to "All providers" if the persisted key no longer matches a connection in the current session. The dropdown only renders when there are at least two distinct providers in view, so single-provider setups aren't cluttered. The upstream "Expiring first" toggle is intentionally not ported — `visibleConnections` already always sorts by soonest reset within each status group, so the toggle would be redundant. Inspired by [decolua/9router#769](https://github.com/decolua/9router/pull/769) — thanks @DEYLNN.
|
||||
- **feat(dashboard): "Done" button in the model picker during combo creation** — `ModelSelectModal` now supports a `keepOpenOnSelect` prop (opt-in, off by default). When set — and the combos page now sets it — picking a model no longer auto-closes the modal, and a full-width "Done" button is rendered in the modal footer so users can add several models in a row and confirm explicitly. Single-select callers (e.g. CLI tool cards) are unchanged: the prop is opt-in, so they keep auto-close. The existing `multiSelect` mode (Clear + Done footer driven by `selectedModels`) takes precedence over `keepOpenOnSelect` to avoid two competing footers. Inspired by upstream PR [decolua/9router#1031](https://github.com/decolua/9router/pull/1031). (thanks @zanuartri)
|
||||
|
||||
### 🐛 Fixed
|
||||
|
||||
|
||||
@@ -4335,6 +4335,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
|
||||
title={t("addModelToCombo")}
|
||||
selectedModel={null}
|
||||
addedModelValues={models.map((m) => m.model)}
|
||||
keepOpenOnSelect
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -38,6 +38,15 @@ type ModelSelectModalProps = {
|
||||
multiSelect?: boolean;
|
||||
showCombos?: boolean;
|
||||
alwaysIncludeProviders?: string[] | null;
|
||||
/**
|
||||
* When true, picking a model does NOT auto-close the modal — the caller must close
|
||||
* explicitly. A "Done" button is rendered in the modal footer so the user has a clear
|
||||
* way to confirm they are finished adding entries. Useful in combo creation, where the
|
||||
* user typically adds several models in a row. Mutually exclusive with `multiSelect`
|
||||
* (which renders its own Clear + Done footer driven by `selectedModels`).
|
||||
* Inspired by upstream PR decolua/9router#1031.
|
||||
*/
|
||||
keepOpenOnSelect?: boolean;
|
||||
};
|
||||
|
||||
export default function ModelSelectModal({
|
||||
@@ -53,6 +62,7 @@ export default function ModelSelectModal({
|
||||
multiSelect = false,
|
||||
showCombos = true,
|
||||
alwaysIncludeProviders = [],
|
||||
keepOpenOnSelect = false,
|
||||
}: ModelSelectModalProps) {
|
||||
const t = useTranslations("common");
|
||||
const resolvedTitle = title ?? t("selectModel");
|
||||
@@ -328,12 +338,29 @@ export default function ModelSelectModal({
|
||||
|
||||
const handleSelect = (model: any) => {
|
||||
onSelect(model);
|
||||
if (!multiSelect) {
|
||||
if (!multiSelect && !keepOpenOnSelect) {
|
||||
onClose();
|
||||
setSearchQuery("");
|
||||
}
|
||||
};
|
||||
|
||||
// Footer "Done" button for single-select callers that opted out of auto-close
|
||||
// (e.g. combo creation, where users add several models in a row). Skipped when
|
||||
// `multiSelect` is on — that mode renders its own Clear + Done footer below the body.
|
||||
const doneFooter =
|
||||
keepOpenOnSelect && !multiSelect ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onClose();
|
||||
setSearchQuery("");
|
||||
}}
|
||||
className="w-full px-3 py-2 text-sm font-medium rounded border border-primary bg-primary text-white hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
{t("done")}
|
||||
</button>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
@@ -344,6 +371,7 @@ export default function ModelSelectModal({
|
||||
title={resolvedTitle}
|
||||
size="md"
|
||||
className="p-4!"
|
||||
footer={doneFooter}
|
||||
>
|
||||
{/* Search - compact */}
|
||||
<div className="mb-3">
|
||||
|
||||
119
tests/unit/ui/model-select-modal-keep-open.test.tsx
Normal file
119
tests/unit/ui/model-select-modal-keep-open.test.tsx
Normal file
@@ -0,0 +1,119 @@
|
||||
// @vitest-environment jsdom
|
||||
//
|
||||
// Regression coverage for the "keepOpenOnSelect" prop added in feat/port-pr-1031.
|
||||
// Mirrors the UX of upstream PR decolua/9router#1031: when a caller (e.g. combo
|
||||
// creation) opts out of the auto-close-on-select behaviour, the modal renders a
|
||||
// "Done" button in the footer so the user has a clear way to confirm they are
|
||||
// finished adding entries.
|
||||
|
||||
import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
vi.mock("next-intl", () => ({
|
||||
useTranslations: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
const { default: ModelSelectModal } = await import("@/shared/components/ModelSelectModal");
|
||||
|
||||
const containers: HTMLElement[] = [];
|
||||
|
||||
async function renderModal(props: Partial<React.ComponentProps<typeof ModelSelectModal>> = {}) {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
containers.push(container);
|
||||
|
||||
const root = createRoot(container);
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<ModelSelectModal
|
||||
isOpen={true}
|
||||
onClose={() => {}}
|
||||
onSelect={() => {}}
|
||||
showCombos={false}
|
||||
activeProviders={[]}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
await act(async () => {});
|
||||
return container;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (url: string) => {
|
||||
if (url === "/api/provider-nodes") {
|
||||
return { ok: true, json: async () => ({ nodes: [] }) };
|
||||
}
|
||||
if (url === "/api/provider-models") {
|
||||
return { ok: true, json: async () => ({ models: {} }) };
|
||||
}
|
||||
if (url === "/api/combos") {
|
||||
return { ok: true, json: async () => ({ combos: [] }) };
|
||||
}
|
||||
return { ok: true, json: async () => ({}) };
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
while (containers.length > 0) {
|
||||
containers.pop()?.remove();
|
||||
}
|
||||
document.body.innerHTML = "";
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
describe("ModelSelectModal keepOpenOnSelect", () => {
|
||||
it("does not render the Done button by default (auto-close behaviour preserved)", async () => {
|
||||
const container = await renderModal();
|
||||
const doneButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(b) => b.textContent?.trim() === "done"
|
||||
);
|
||||
expect(doneButton).toBeUndefined();
|
||||
});
|
||||
|
||||
it("renders the Done button when keepOpenOnSelect is true", async () => {
|
||||
const container = await renderModal({ keepOpenOnSelect: true });
|
||||
const doneButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(b) => b.textContent?.trim() === "done"
|
||||
);
|
||||
expect(doneButton).toBeDefined();
|
||||
});
|
||||
|
||||
it("clicking Done triggers onClose without invoking onSelect again", async () => {
|
||||
const onClose = vi.fn();
|
||||
const onSelect = vi.fn();
|
||||
const container = await renderModal({ keepOpenOnSelect: true, onClose, onSelect });
|
||||
|
||||
const doneButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(b) => b.textContent?.trim() === "done"
|
||||
);
|
||||
expect(doneButton).toBeDefined();
|
||||
|
||||
await act(async () => {
|
||||
doneButton!.click();
|
||||
});
|
||||
|
||||
expect(onClose).toHaveBeenCalledTimes(1);
|
||||
expect(onSelect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not render the Done button when multiSelect is true (multiSelect owns its own footer)", async () => {
|
||||
// multiSelect already ships a Clear + Done footer driven by selectedModels.
|
||||
// keepOpenOnSelect must defer to it to avoid two competing Done buttons.
|
||||
const container = await renderModal({ keepOpenOnSelect: true, multiSelect: true });
|
||||
const doneButtons = Array.from(container.querySelectorAll("button")).filter(
|
||||
(b) => b.textContent?.trim() === "done"
|
||||
);
|
||||
// Exactly one Done button — the one inside the multiSelect footer, not a duplicate.
|
||||
expect(doneButtons.length).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user