mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-04 22:32:12 +03:00
fix(dashboard): preserve quota cutoff drafts (#7909)
Co-authored-by: Bryan Nathan <bryan@users.noreply.github.com>
This commit is contained in:
1
changelog.d/fixes/7889-quota-cutoff-drafts.md
Normal file
1
changelog.d/fixes/7889-quota-cutoff-drafts.md
Normal 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)).
|
||||
@@ -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"
|
||||
|
||||
@@ -1069,6 +1069,7 @@ export default function ProviderLimits({
|
||||
setCutoffModalConn(null);
|
||||
setCutoffModalWindows([]);
|
||||
}}
|
||||
connectionId={cutoffModalConn.id}
|
||||
connectionName={
|
||||
pickDisplayValue(
|
||||
[cutoffModalConn.name, cutoffModalConn.displayName, cutoffModalConn.email],
|
||||
|
||||
85
tests/unit/ui/quota-cutoff-modal-draft-reset-7889.test.tsx
Normal file
85
tests/unit/ui/quota-cutoff-modal-draft-reset-7889.test.tsx
Normal 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");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user