fix(dashboard): preserve quota cutoff drafts (#7909)

Co-authored-by: Bryan Nathan <bryan@users.noreply.github.com>
This commit is contained in:
Nathan
2026-07-21 09:22:36 +08:00
committed by GitHub
parent d3f8bbe555
commit 62cbbcd2c0
4 changed files with 100 additions and 3 deletions

View File

@@ -0,0 +1 @@
- **fix(dashboard):** Provider Quota cutoff inputs preserve unsaved values across quota refreshes instead of reverting while the operator is typing ([#7889](https://github.com/diegosouzapw/OmniRoute/issues/7889)).

View File

@@ -1,6 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { useEffect, useRef, useState } from "react";
import { useTranslations } from "next-intl";
import Modal from "@/shared/components/Modal";
import Button from "@/shared/components/Button";
@@ -16,6 +16,8 @@ export interface QuotaCutoffModalWindow {
interface QuotaCutoffModalProps {
isOpen: boolean;
onClose: () => void;
/** Stable identity used to distinguish refreshes from connection changes. */
connectionId: string;
/** Label shown in the modal title. */
connectionName: string;
/** Used in the modal title for context (e.g. "(codex)"). */
@@ -44,6 +46,7 @@ interface QuotaCutoffModalProps {
export default function QuotaCutoffModal({
isOpen,
onClose,
connectionId,
connectionName,
provider,
windows,
@@ -59,10 +62,17 @@ export default function QuotaCutoffModal({
const [drafts, setDrafts] = useState<Record<string, string>>({});
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null);
const wasOpenRef = useRef(false);
const seededConnectionIdRef = useRef<string | null>(null);
// Reset drafts whenever the modal opens against a new connection.
useEffect(() => {
if (!isOpen) return;
const shouldSeed =
isOpen && (!wasOpenRef.current || seededConnectionIdRef.current !== connectionId);
wasOpenRef.current = isOpen;
if (!shouldSeed) return;
seededConnectionIdRef.current = connectionId;
const initial: Record<string, string> = {};
for (const w of windows) {
const persisted = current?.[w.key];
@@ -70,7 +80,7 @@ export default function QuotaCutoffModal({
}
setDrafts(initial);
setError(null);
}, [isOpen, windows, current]);
}, [isOpen, connectionId, windows, current]);
const resolveDefaultFor = (windowKey: string): number =>
typeof providerDefaults[windowKey] === "number"

View File

@@ -1069,6 +1069,7 @@ export default function ProviderLimits({
setCutoffModalConn(null);
setCutoffModalWindows([]);
}}
connectionId={cutoffModalConn.id}
connectionName={
pickDisplayValue(
[cutoffModalConn.name, cutoffModalConn.displayName, cutoffModalConn.email],

View File

@@ -0,0 +1,85 @@
// @vitest-environment jsdom
/**
* Regression guard for #7889: quota polling recreates `windows` and `current`
* while the cutoff modal is open. Those identity-only prop changes must not
* overwrite an operator's unsaved input; opening another connection still
* seeds that connection's persisted values.
*/
import React, { 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: QuotaCutoffModal } =
await import("../../../src/app/(dashboard)/dashboard/usage/components/ProviderLimits/QuotaCutoffModal");
const cleanupCallbacks: Array<() => void> = [];
function props(connectionId: string, persisted: number) {
return {
isOpen: true,
onClose: () => {},
connectionId,
connectionName: connectionId,
provider: "codex",
windows: [{ key: "session", displayName: "Session" }],
current: { session: persisted },
providerDefaults: { session: 2 },
globalDefaultPercent: 2,
onSave: async () => {},
};
}
function setInputValue(input: HTMLInputElement, value: string) {
const setter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")!.set!;
act(() => {
setter.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
});
}
beforeEach(() => {
(
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
).IS_REACT_ACT_ENVIRONMENT = true;
});
afterEach(() => {
while (cleanupCallbacks.length) cleanupCallbacks.pop()!();
});
describe("QuotaCutoffModal draft lifetime (#7889)", () => {
it("preserves an unsaved draft across same-connection prop refreshes and resets for a new connection", async () => {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
cleanupCallbacks.push(() => {
act(() => root.unmount());
container.remove();
});
await act(async () => {
root.render(<QuotaCutoffModal {...props("connection-a", 2)} />);
});
const input = container.querySelector<HTMLInputElement>('input[type="number"]')!;
expect(input.value).toBe("2");
setInputValue(input, "10");
expect(input.value).toBe("10");
// Quota polling reconstructs both objects without changing the active connection.
await act(async () => {
root.render(<QuotaCutoffModal {...props("connection-a", 2)} />);
});
expect(input.value).toBe("10");
// Switching the same mounted modal to another connection must seed its persisted value.
await act(async () => {
root.render(<QuotaCutoffModal {...props("connection-b", 7)} />);
});
expect(input.value).toBe("7");
});
});