fix(po): make cancel buttons red & visible (use defined danger tokens)
The theme only defines danger / danger-50 / danger-100 / danger-700, so bg-danger-600 / danger-500 / danger-200 generated no CSS — the modal confirm button was white-on-nothing (invisible) and the header button wasn't red. - Header "Cancel PO" button → solid red (bg-danger text-white hover:bg-danger-700) - Modal "Cancel this PO" confirm button → bg-danger (now visible) - Inputs/asterisk/banner border → defined danger tokens Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9de60200f9
commit
43d139234e
3 changed files with 51 additions and 6 deletions
|
|
@ -47,7 +47,7 @@ export function CancelPoButton({ poId, poNumber }: { poId: string; poNumber: str
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(true)}
|
||||
className="rounded-lg border border-danger-200 bg-white px-3 py-2 text-sm font-medium text-danger-600 hover:bg-danger-50 transition-colors"
|
||||
className="rounded-lg bg-danger px-3 py-2 text-sm font-semibold text-white hover:bg-danger-700 transition-colors"
|
||||
>
|
||||
Cancel PO
|
||||
</button>
|
||||
|
|
@ -65,7 +65,7 @@ export function CancelPoButton({ poId, poNumber }: { poId: string; poNumber: str
|
|||
</p>
|
||||
|
||||
<label className="mt-4 block text-xs font-medium text-neutral-700">
|
||||
Reason for cancellation <span className="text-danger-600">*</span>
|
||||
Reason for cancellation <span className="text-danger">*</span>
|
||||
</label>
|
||||
<textarea
|
||||
value={reason}
|
||||
|
|
@ -73,7 +73,7 @@ export function CancelPoButton({ poId, poNumber }: { poId: string; poNumber: str
|
|||
rows={3}
|
||||
autoFocus
|
||||
placeholder="e.g. Duplicate order — superseded by a corrected PO"
|
||||
className="mt-1 w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-danger-500 focus:outline-none focus:ring-2 focus:ring-danger-500/20"
|
||||
className="mt-1 w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm focus:border-danger focus:outline-none focus:ring-2 focus:ring-danger/20"
|
||||
/>
|
||||
|
||||
<label className="mt-3 block text-xs font-medium text-neutral-700">
|
||||
|
|
@ -83,7 +83,7 @@ export function CancelPoButton({ poId, poNumber }: { poId: string; poNumber: str
|
|||
value={confirmText}
|
||||
onChange={(e) => setConfirmText(e.target.value)}
|
||||
placeholder="cancel"
|
||||
className="mt-1 w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm font-mono focus:border-danger-500 focus:outline-none focus:ring-2 focus:ring-danger-500/20"
|
||||
className="mt-1 w-full rounded-lg border border-neutral-300 px-3 py-2 text-sm font-mono focus:border-danger focus:outline-none focus:ring-2 focus:ring-danger/20"
|
||||
/>
|
||||
|
||||
{error && <p className="mt-3 text-sm text-danger-700 bg-danger-50 rounded-lg px-3 py-2">{error}</p>}
|
||||
|
|
@ -101,7 +101,7 @@ export function CancelPoButton({ poId, poNumber }: { poId: string; poNumber: str
|
|||
type="button"
|
||||
onClick={handleCancel}
|
||||
disabled={!canSubmit}
|
||||
className="rounded-lg bg-danger-600 px-4 py-2 text-sm font-semibold text-white hover:bg-danger-700 disabled:opacity-50"
|
||||
className="rounded-lg bg-danger px-4 py-2 text-sm font-semibold text-white hover:bg-danger-700 disabled:opacity-50"
|
||||
>
|
||||
{pending ? "Cancelling…" : "Cancel this PO"}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ export async function PoDetail({ po, currentUserId, currentRole, readOnly = fals
|
|||
|
||||
{/* Cancelled banner — reason + supersede link (and the reciprocal "supersedes") */}
|
||||
{po.status === "CANCELLED" && (
|
||||
<div className="rounded-lg border border-danger-200 bg-danger-50 px-4 py-3">
|
||||
<div className="rounded-lg border border-danger-100 bg-danger-50 px-4 py-3">
|
||||
<p className="text-sm font-semibold text-danger-700">
|
||||
Cancelled{po.cancelledAt ? ` on ${formatDate(po.cancelledAt)}` : ""}
|
||||
</p>
|
||||
|
|
|
|||
45
App/tests/unit/cancel-po-controls.test.tsx
Normal file
45
App/tests/unit/cancel-po-controls.test.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { describe, it, expect, vi } from "vitest";
|
||||
import { render, screen, fireEvent } from "@testing-library/react";
|
||||
|
||||
vi.mock("next/navigation", () => ({ useRouter: () => ({ refresh: vi.fn(), push: vi.fn() }) }));
|
||||
vi.mock("@/app/(portal)/po/[id]/actions", () => ({ cancelPo: vi.fn(), supersedePo: vi.fn() }));
|
||||
|
||||
import { CancelPoButton } from "@/components/po/cancel-po-controls";
|
||||
|
||||
// Regression guard: the theme only defines danger / -50 / -100 / -700, so an
|
||||
// undefined shade like bg-danger-600 renders no background → the button was
|
||||
// invisible (white text on nothing). Both cancel buttons must use `bg-danger`.
|
||||
|
||||
describe("CancelPoButton", () => {
|
||||
it("renders the trigger as a filled red (bg-danger) button with white text", () => {
|
||||
render(<CancelPoButton poId="po1" poNumber="PO-1" />);
|
||||
const btn = screen.getByRole("button", { name: "Cancel PO" });
|
||||
// standalone `bg-danger` (a defined token), NOT `bg-danger-600` (undefined → invisible)
|
||||
expect(btn.className).toMatch(/(?:^|\s)bg-danger(?:\s|$)/);
|
||||
expect(btn.className).toContain("text-white");
|
||||
});
|
||||
|
||||
it("opens a modal whose confirm button is a visible filled danger button", () => {
|
||||
render(<CancelPoButton poId="po1" poNumber="PO-1" />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Cancel PO" }));
|
||||
|
||||
const confirm = screen.getByRole("button", { name: "Cancel this PO" });
|
||||
expect(confirm.className).toMatch(/(?:^|\s)bg-danger(?:\s|$)/);
|
||||
expect(confirm.className).toContain("text-white");
|
||||
|
||||
// Keep PO is always present as the safe default.
|
||||
expect(screen.getByRole("button", { name: "Keep PO" })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("keeps the confirm action disabled until 'cancel' is typed and a reason given", () => {
|
||||
render(<CancelPoButton poId="po1" poNumber="PO-1" />);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Cancel PO" }));
|
||||
|
||||
const confirm = screen.getByRole("button", { name: "Cancel this PO" }) as HTMLButtonElement;
|
||||
expect(confirm.disabled).toBe(true);
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText(/Duplicate order/i), { target: { value: "No longer needed" } });
|
||||
fireEvent.change(screen.getByPlaceholderText("cancel"), { target: { value: "cancel" } });
|
||||
expect(confirm.disabled).toBe(false);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue