mirror of
https://github.com/Sea-Haven-Industries/shoc-frontend-new.git
synced 2026-10-02 22:43:23 +00:00
fix(vendor-portal): hold uplift submit until the evidence check settles
Picking a video as uplift evidence reads its movie header before the file is stored in form state. A submit that landed inside that window validated the previous (usually empty) evidence, showed the missing-evidence error and dropped the request, forcing a second click. The submit button is now disabled and reads "Checking evidence…" while the latest pick is being checked, and the submit handler ignores submissions in that window.
This commit is contained in:
parent
39ee98e09b
commit
d653ca0095
2 changed files with 45 additions and 2 deletions
|
|
@ -43,12 +43,15 @@ export function UpliftRequestForm({
|
|||
const [reason, setReason] = useState(initialReason);
|
||||
const [evidenceFile, setEvidenceFile] = useState<File | undefined>();
|
||||
const [formError, setFormError] = useState("");
|
||||
const [checkingEvidence, setCheckingEvidence] = useState(false);
|
||||
const latestSelection = useRef(0);
|
||||
|
||||
const showApprovalFields = mode === "revise" || isApprovalRequired(requested, currentNte);
|
||||
|
||||
const submit = (event: React.FormEvent) => {
|
||||
event.preventDefault();
|
||||
// The picked file is not in state until its check settles; submitting now would validate the old one.
|
||||
if (checkingEvidence) return;
|
||||
const amount = Number.parseFloat(requested);
|
||||
const validationError = validateUpliftAmount(requested, currentNte, reason, {
|
||||
file: evidenceFile,
|
||||
|
|
@ -64,11 +67,13 @@ export function UpliftRequestForm({
|
|||
|
||||
const chooseFile = async (file?: File) => {
|
||||
const selection = ++latestSelection.current;
|
||||
setCheckingEvidence(Boolean(file));
|
||||
const validationMessage = file ? await validateVendorDocument(file, mediaUsage) : undefined;
|
||||
// A newer pick may have settled while this one's video metadata was still loading.
|
||||
if (selection !== latestSelection.current) return;
|
||||
setFormError(validationMessage ?? "");
|
||||
setEvidenceFile(file);
|
||||
setCheckingEvidence(false);
|
||||
};
|
||||
|
||||
const shownError = errorMessage || formError;
|
||||
|
|
@ -147,8 +152,14 @@ export function UpliftRequestForm({
|
|||
{shownError}
|
||||
</Text>
|
||||
<div className="vp-actions">
|
||||
<button type="submit" className="vp-btn vp-btn-primary" disabled={busy}>
|
||||
{busy ? "Submitting…" : mode === "revise" ? "Submit revision" : "Request uplift"}
|
||||
<button type="submit" className="vp-btn vp-btn-primary" disabled={busy || checkingEvidence}>
|
||||
{busy
|
||||
? "Submitting…"
|
||||
: checkingEvidence
|
||||
? "Checking evidence…"
|
||||
: mode === "revise"
|
||||
? "Submit revision"
|
||||
: "Request uplift"}
|
||||
</button>
|
||||
{mode === "revise" && (
|
||||
<button type="button" className="vp-btn vp-btn-ghost" onClick={onCancel} disabled={busy}>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { UpliftRequestForm } from "@/app/v/[token]/dispatch/_components/uplift-request-form";
|
||||
import { probeVideoDurationSeconds } from "@/lib/probe-video-duration";
|
||||
|
||||
vi.mock("@/lib/probe-video-duration", () => ({ probeVideoDurationSeconds: vi.fn() }));
|
||||
|
||||
function renderForm(overrides: Partial<React.ComponentProps<typeof UpliftRequestForm>> = {}) {
|
||||
return render(
|
||||
|
|
@ -73,6 +76,35 @@ describe("UpliftRequestForm revise mode", () => {
|
|||
);
|
||||
});
|
||||
|
||||
it("holds submit while a picked video is still being checked, then submits that video", async () => {
|
||||
let finishProbe: (seconds: number | null) => void = () => {};
|
||||
vi.mocked(probeVideoDurationSeconds).mockImplementationOnce(
|
||||
() => new Promise((resolve) => (finishProbe = resolve)),
|
||||
);
|
||||
const onSubmit = vi.fn();
|
||||
renderForm({ initialRequested: "1200", initialReason: "Extra parts", onSubmit });
|
||||
|
||||
const video = new File(["mov"], "IMG_0001.MOV", { type: "video/quicktime" });
|
||||
fireEvent.change(screen.getByLabelText(/evidence/i), { target: { files: [video] } });
|
||||
|
||||
const checking = await screen.findByRole("button", { name: "Checking evidence…" });
|
||||
expect(checking).toBeDisabled();
|
||||
fireEvent.submit(checking.closest("form")!);
|
||||
expect(onSubmit).not.toHaveBeenCalled();
|
||||
expect(screen.queryByText(/approval-required requests need evidence/i)).not.toBeInTheDocument();
|
||||
|
||||
finishProbe(30);
|
||||
const submitButton = await screen.findByRole("button", { name: /submit revision/i });
|
||||
expect(submitButton).toBeEnabled();
|
||||
fireEvent.click(submitButton);
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
amount: 1200,
|
||||
reason: "Extra parts",
|
||||
evidenceFile: video,
|
||||
});
|
||||
});
|
||||
|
||||
it("reports the current NTE threshold in the actionable error", () => {
|
||||
renderForm({ initialRequested: "900", currentNte: 1000 });
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue