From ec203e1f5b6e736746f85ff761698080fcc8529c Mon Sep 17 00:00:00 2001 From: Alexandre Brandizzi Date: Thu, 6 Aug 2026 22:39:11 -0300 Subject: [PATCH] fix(vendors): preserve the selected edit state --- .../_components/use-vendor-deactivation.ts | 7 +++- .../_components/vendor-detail-drawer.tsx | 19 +++++----- src/app/(protected)/vendors/index.tsx | 17 ++++----- .../vendors/vendor-detail-drawer.test.tsx | 23 +++++++++++ .../(protected)/vendors/vendors-list.test.tsx | 38 ++++++++++++++++++- 5 files changed, 82 insertions(+), 22 deletions(-) diff --git a/src/app/(protected)/vendors/_components/use-vendor-deactivation.ts b/src/app/(protected)/vendors/_components/use-vendor-deactivation.ts index 9b6f3b0d..b63291be 100644 --- a/src/app/(protected)/vendors/_components/use-vendor-deactivation.ts +++ b/src/app/(protected)/vendors/_components/use-vendor-deactivation.ts @@ -15,7 +15,7 @@ export interface VendorDeactivationState { confirm: () => void; } -export function useVendorDeactivation(): VendorDeactivationState { +export function useVendorDeactivation(onSuccess?: () => void): VendorDeactivationState { const [target, setTarget] = useState(null); const [error, setError] = useState(null); const deleteVendor = useDeleteVendor(); @@ -39,7 +39,10 @@ export function useVendorDeactivation(): VendorDeactivationState { if (!target || target.id == null) return; setError(null); deleteVendor.mutate(target.id, { - onSuccess: () => setTarget(null), + onSuccess: () => { + setTarget(null); + onSuccess?.(); + }, onError: (err: Error) => setError(err.message || "Failed to deactivate vendor"), }); }; diff --git a/src/app/(protected)/vendors/_components/vendor-detail-drawer.tsx b/src/app/(protected)/vendors/_components/vendor-detail-drawer.tsx index 5be1a4c6..74d6ef02 100644 --- a/src/app/(protected)/vendors/_components/vendor-detail-drawer.tsx +++ b/src/app/(protected)/vendors/_components/vendor-detail-drawer.tsx @@ -1,5 +1,5 @@ import { useEffect, useState, type ReactNode } from "react"; -import { Controller } from "react-hook-form"; +import { Controller, useWatch } from "react-hook-form"; import CloseIcon from "@mui/icons-material/Close"; import EditOutlinedIcon from "@mui/icons-material/EditOutlined"; import LaunchIcon from "@mui/icons-material/Launch"; @@ -250,13 +250,14 @@ function DrawerEditor({ onSuccess: onSaved, }); const roster = form.roster; - const selectedIndex = Math.max( - 0, - roster?.technicians.findIndex( - (technician) => technician.id != null && String(technician.id) === String(vendor.id), - ) ?? 0, + const technicians = useWatch({ control: form.control, name: "technicians" }) ?? []; + const selectedIndex = technicians.findIndex( + (technician) => technician.id != null && String(technician.id) === String(vendor.id), ); - const selected = roster?.technicians[selectedIndex]; + const selectedTotalJobs = + roster?.technicians.find( + (technician) => technician.id != null && String(technician.id) === String(vendor.id), + )?.totalJobs ?? vendor.totalJobs; if (form.isLoading) { return ; @@ -289,12 +290,12 @@ function DrawerEditor({ tradeOptions={form.trades} showTechnicianStatus={false} /> - {selected != null && ( + {selectedIndex >= 0 && ( <> - {selected.totalJobs ?? 0} + {selectedTotalJobs} Total Jobs diff --git a/src/app/(protected)/vendors/index.tsx b/src/app/(protected)/vendors/index.tsx index 25ea98cd..c81ed17f 100644 --- a/src/app/(protected)/vendors/index.tsx +++ b/src/app/(protected)/vendors/index.tsx @@ -116,7 +116,11 @@ export default function VendorsListPage() { const [createOpen, setCreateOpen] = useState(false); const [detailTarget, setDetailTarget] = useState(null); const [detailStartsInEdit, setDetailStartsInEdit] = useState(false); - const deactivation = useVendorDeactivation(); + const closeDetail = () => { + setDetailTarget(null); + setDetailStartsInEdit(false); + }; + const deactivation = useVendorDeactivation(closeDetail); const { data: facets } = useVendorFacets(filters.isActive); const { data, isLoading, isFetching, error } = useVendorsList(filters.listParams); @@ -213,15 +217,8 @@ export default function VendorsListPage() { open={Boolean(detailTarget)} vendor={detailTarget} startInEdit={detailStartsInEdit} - onRequestDeactivation={(row) => { - setDetailTarget(null); - setDetailStartsInEdit(false); - deactivation.open(row); - }} - onClose={() => { - setDetailTarget(null); - setDetailStartsInEdit(false); - }} + onRequestDeactivation={deactivation.open} + onClose={closeDetail} /> { expect(screen.getByText("0")).toBeInTheDocument(); expect(screen.queryByText("999-999-9999")).not.toBeInTheDocument(); }); + + it("does not expose a status control for a different technician", () => { + useVendorCompanyRoster.mockReturnValue( + rosterWith([ + { + id: 7, + contactName: "Different Technician", + phone: "314-555-0101", + email: "", + tradeSpecialties: "Plumbing", + isActive: true, + totalJobs: 10, + }, + ]), + ); + + renderWithProviders( + , + { route: "/vendors", withAuth: false }, + ); + + expect(screen.queryByRole("switch", { name: "Active status" })).not.toBeInTheDocument(); + }); }); diff --git a/src/test/app/(protected)/vendors/vendors-list.test.tsx b/src/test/app/(protected)/vendors/vendors-list.test.tsx index 57f467fc..a5fd7a64 100644 --- a/src/test/app/(protected)/vendors/vendors-list.test.tsx +++ b/src/test/app/(protected)/vendors/vendors-list.test.tsx @@ -1,4 +1,4 @@ -import { screen } from "@testing-library/react"; +import { screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it, vi } from "vitest"; import VendorsListPage from "@/app/(protected)/vendors/index"; @@ -220,6 +220,42 @@ describe("VendorsListPage", () => { expect(mutate).not.toHaveBeenCalled(); }); + it("preserves inline edits when deactivation is cancelled", async () => { + setupDefaults(); + useVendorCompanyRoster.mockReturnValue({ + data: activeRoster, + isLoading: false, + isError: false, + error: null, + refetch: vi.fn(), + }); + useVendorDeactivationImpact.mockReturnValue({ + data: { vendorId: 1, canDeactivate: true, openWorkOrders: [] }, + isLoading: false, + error: null, + }); + useVendorsList.mockImplementation((params: { isActive?: boolean; pageSize?: number }) => { + if (params.pageSize === 1) return result([], 1); + return params.isActive ? result([activeVendor], 1) : result([], 0); + }); + + renderWithProviders(, { route: "/vendors", withAuth: false }); + + await userEvent.click(screen.getByRole("button", { name: "Edit vendor Gateway Plumbing" })); + const company = screen.getByRole("textbox", { name: "Company" }); + await userEvent.clear(company); + await userEvent.type(company, "Draft Company Name"); + await userEvent.click(screen.getByRole("switch", { name: "Active status" })); + await userEvent.click( + within(screen.getByRole("dialog", { name: "Deactivate Vendor" })).getByRole("button", { + name: "Cancel", + }), + ); + + expect(company).toBeInTheDocument(); + expect(company).toHaveValue("Draft Company Name"); + }); + it("opens inline edit when the table Edit control is clicked", async () => { setupDefaults(); useVendorCompanyRoster.mockReturnValue({