fix(vendors): complete structured address map flows

This commit is contained in:
Codex Review Integration 2026-09-14 12:31:15 -03:00
parent 57fe7aa08a
commit 86c0748f21
18 changed files with 441 additions and 44 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 137 KiB

After

Width:  |  Height:  |  Size: 137 KiB

View file

@ -27,7 +27,6 @@ const EDITABLE_COMPANY_FIELDS = [
"city",
"state",
"zip",
"googleMapsUrl",
"notes",
] as const;

View file

@ -40,7 +40,8 @@ export function toFormValues(roster: VendorCompanyRoster): VendorCompanyRosterFo
city: roster.city,
state: roster.state,
zip: roster.zip,
googleMapsUrl: roster.googleMapsUrl,
// The deprecated stored Maps URL is intentionally not seeded: the form has
// no input for it, and carrying it would re-emit a legacy value on write.
notes: roster.notes,
technicians: roster.technicians.map((technician) => ({
id: technician.id,

View file

@ -205,6 +205,7 @@ function CompanyFields({
city: cityField.value ?? "",
state: stateField.value ?? "",
}}
placeholderWhenIncomplete
/>
<Controller
control={control}

View file

@ -11,6 +11,13 @@ import type { WorkOrderFrozenCompany } from "@/domain/work-orders/types/work-ord
export type { VendorPatch } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-types";
/**
* Height bound for the dialog surface. The company card grew with the map
* preview, so the dialog itself — not just the card — must cap its height and
* scroll internally or it outgrows short viewports.
*/
export const VENDOR_DIALOG_SURFACE_MAX_HEIGHT = "55vh";
type VendorDialogProps = {
open: boolean;
onOpenChange: (open: boolean) => void;
@ -56,12 +63,22 @@ export function VendorDialog({
...dialogBlurBackdropSlotProps,
paper: {
className: "w-full",
style: { borderRadius: 12, maxWidth: 560 },
style: {
borderRadius: 12,
maxWidth: 560,
maxHeight: VENDOR_DIALOG_SURFACE_MAX_HEIGHT,
display: "flex",
flexDirection: "column",
},
onClick: (e: MouseEvent<HTMLDivElement>) => e.stopPropagation(),
},
}}
>
<DialogContent className="p-6" onClick={(e) => e.stopPropagation()}>
<DialogContent
className="p-6"
onClick={(e) => e.stopPropagation()}
style={{ overflowY: "auto", minHeight: 0 }}
>
<DialogTitle className="p-0" style={{ fontSize: 16, color: "var(--color-text-primary)" }}>
Vendor
</DialogTitle>

View file

@ -49,11 +49,21 @@ export function AddressAutocompleteField({
// typed text.
filterOptions={(options) => options}
inputValue={value}
onInputChange={(_event, next) => onInputChange(next)}
onInputChange={(_event, next, reason) => {
// Genuine typing and the clear control update the street. MUI also
// fires this callback with reason "reset" after a selection, carrying
// the option's full "<street>, <city>, <state>" label — forwarding
// that would overwrite the just-picked street with the label.
if (reason !== "input" && reason !== "clear") return;
onInputChange(next);
}}
onChange={(_event, selected) => {
if (selected && typeof selected !== "string") {
onSelect({ street: selected.street, city: selected.city, state: selected.state });
return;
}
if (selected === null) onSelect({ street: "", city: "", state: "" });
}}
getOptionLabel={(option) => (typeof option === "string" ? option : option.label)}
renderInput={(params) => (

View file

@ -1,4 +1,5 @@
import { Box } from "@mui/material";
import { Text } from "@/components/ui/text";
import {
buildAddressQuery,
hasCompleteAddress,
@ -16,8 +17,17 @@ type AddressMapPreviewProps = {
*/
query?: string;
height?: number;
/**
* Render a compact accessible placeholder while the structured address is
* incomplete, instead of nothing. Meant for Add/Edit forms, where the empty
* state tells the dispatcher which parts the preview is still waiting on.
*/
placeholderWhenIncomplete?: boolean;
};
const INCOMPLETE_PLACEHOLDER_MESSAGE =
"Map preview appears once Street, City, and State are all filled.";
/**
* Small inline map for a structured address.
*
@ -25,19 +35,47 @@ type AddressMapPreviewProps = {
* preview with no API key, so there is no Maps JavaScript API, no Places
* request and no billing account behind this.
*
* Renders nothing until Street, City and State are all present — a preview of a
* half-typed address points somewhere misleading, which is worse than no map.
* Structured input renders nothing until Street, City and State are all
* present — a preview of a half-typed address points somewhere misleading,
* which is worse than no map. Add/Edit forms opt into a compact placeholder
* for that incomplete state; a pre-composed saved line is map-only and renders
* only when non-empty.
*/
export function AddressMapPreview({
address,
query: rawQuery,
height = 160,
placeholderWhenIncomplete = false,
}: AddressMapPreviewProps) {
// Structured input is held to the completeness rule; a pre-composed line is
// saved data and only has to be non-empty.
const query = address ? buildAddressQuery(address) : (rawQuery ?? "").trim();
const renderable = address ? hasCompleteAddress(address) : Boolean(query);
if (!renderable) return null;
const complete = address ? hasCompleteAddress(address) : Boolean(query);
if (!complete) {
if (!(address != null && placeholderWhenIncomplete)) return null;
return (
<Box
role="status"
sx={{
borderRadius: 1,
border: "1px dashed",
borderColor: "divider",
minHeight: 56,
display: "flex",
alignItems: "center",
justifyContent: "center",
px: 1.5,
py: 1,
}}
>
<Text variant="caption" tone="muted" sx={{ textAlign: "center" }}>
{INCOMPLETE_PLACEHOLDER_MESSAGE}
</Text>
</Box>
);
}
return (
<Box
sx={{

View file

@ -123,7 +123,8 @@ export function mapVendorRosterToBackend(values: unknown): Record<string, unknow
city: readString(item, "city", "City"),
state: readString(item, "state", "State"),
zip: readString(item, "zip", "Zip", "zipCode", "ZipCode"),
googleMapsUrl: readString(item, "googleMapsUrl", "GoogleMapsUrl"),
// The stored Maps URL is deprecated: reads keep it for legacy records, but
// roster writes must not carry it back to the backend.
notes: readString(item, "notes", "Notes"),
technicians: Array.isArray(techniciansRaw)
? techniciansRaw.map(mapRosterTechnicianToBackend)

View file

@ -8,21 +8,6 @@ const preferredContactEnum = z.enum(
VENDOR_PREFERRED_CONTACT_VALUES as [VendorPreferredContact, ...VendorPreferredContact[]],
);
export function isAbsoluteHttpsUrl(value: string): boolean {
if (value.trim() === "") return true;
try {
const parsed = new URL(value);
return parsed.protocol === "https:";
} catch {
return false;
}
}
const httpsUrl = z
.string()
.trim()
.refine(isAbsoluteHttpsUrl, "Google Maps URL must be a valid HTTPS link");
export function isValidNorthAmericanPhone(value: string): boolean {
if (value === "") return true;
const digits = value.replace(/\D+/g, "");
@ -58,13 +43,14 @@ const baseCompanyFields = {
companyPhone: northAmericanPhone,
email: optionalEmail,
// SH-271: the single combined Address line became three structured parts,
// all required. `zip` stays optional and is not shown in the form — the
// ticket scopes the visible set to Street/City/State.
// all required, and the stored Google Maps URL was retired from the form.
// `googleMapsUrl` stays on the read models for legacy records but is not a
// form value: loading it would validate and re-emit a deprecated field the
// user can no longer see or edit.
address: z.string().trim().min(1, "Street Address is required"),
city: z.string().trim().min(1, "City is required"),
state: z.string().trim().min(1, "State is required"),
zip: z.string(),
googleMapsUrl: httpsUrl,
notes: z
.string()
.max(VENDOR_NOTES_MAX_LENGTH, `Notes must be ${VENDOR_NOTES_MAX_LENGTH} characters or fewer`),
@ -120,7 +106,6 @@ export const emptyVendorCompanyRosterForm: VendorCompanyRosterFormValues = {
city: "",
state: "",
zip: "",
googleMapsUrl: "",
notes: "",
technicians: [],
};

View file

@ -26,6 +26,9 @@ export interface SaveVendorCompanyRosterContext {
conflict?: unknown;
}
// `googleMapsUrl` is deliberately absent: it is a deprecated read-only field,
// so neither the additive companyFields patch nor the status-only diff may
// ever transmit it.
const COMPANY_FIELDS = [
"name",
"companyPhone",
@ -34,7 +37,6 @@ const COMPANY_FIELDS = [
"city",
"state",
"zip",
"googleMapsUrl",
"notes",
] as const;

View file

@ -1,6 +1,6 @@
import { act, renderHook, waitFor } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import type { ReactNode } from "react";
import type { FormEvent, ReactNode } from "react";
import { useController, useFieldArray } from "react-hook-form";
import { toast } from "react-toastify";
import { beforeEach, describe, expect, it, vi } from "vitest";
@ -133,7 +133,6 @@ function submitValues(
city: "St. Louis",
state: "MO",
zip: "63101",
googleMapsUrl: "",
notes: "",
technicians,
};
@ -300,7 +299,6 @@ describe("useVendorRosterForm prototype defaults", () => {
city: "",
state: "",
zip: "",
googleMapsUrl: "",
notes: "",
technicians: [
{
@ -322,6 +320,46 @@ describe("useVendorRosterForm prototype defaults", () => {
});
});
describe("useVendorRosterForm legacy stored Maps URL", () => {
beforeEach(() => {
rosterGet.mockReset();
saveMutate.mockReset();
});
it("saves an edit of a legacy roster whose stored Maps URL is plain HTTP", async () => {
const legacyRoster: VendorCompanyRoster = {
...roster,
googleMapsUrl: "http://maps.example.test/legacy",
};
rosterQueryResult = {
data: legacyRoster,
isLoading: false,
isError: false,
error: null,
refetch: vi.fn(),
};
const { result } = renderHook(() => useVendorRosterForm({ mode: "update", vendorId: 7 }), {
wrapper: makeWrapper(createClient()),
});
// Exercise the real resolver: while the form schema validated the hidden
// loaded URL, this submit never reached the save path.
await act(async () => {
await result.current.handleSubmit(result.current.submit)({
preventDefault: () => undefined,
} as unknown as FormEvent<HTMLFormElement>);
});
expect(saveMutate).toHaveBeenCalledTimes(1);
const input = saveMutate.mock.calls[0]?.[0] as {
mode: string;
values: VendorCompanyRosterFormValues;
};
expect(input.mode).toBe("update");
expect(input.values).not.toHaveProperty("googleMapsUrl");
});
});
describe("useVendorRosterForm technician removal", () => {
it("keeps a valid roster saveable after an existing technician is removed", async () => {
rosterQueryResult = {

View file

@ -0,0 +1,142 @@
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { describe, expect, it, vi } from "vitest";
import { VendorRosterFormFields } from "@/app/(protected)/vendors/_components/vendor-roster-form-fields";
import {
emptyVendorCompanyRosterForm,
vendorCompanyRosterSchema,
type VendorCompanyRosterFormValues,
} from "@/domain/vendors/schemas/vendor-roster-schema";
import { suggestAddresses } from "@/lib/address/vendor-address";
import { renderWithProviders } from "@/test/test-utils";
const TYPED_STREET = "415 Murphy Rd";
const FIRST_SUGGESTION = suggestAddresses(TYPED_STREET)[0];
if (FIRST_SUGGESTION == null) throw new Error("expected a suggestion for the typed street");
function AddressFormHarness({
onValid,
}: {
onValid: (values: VendorCompanyRosterFormValues) => void;
}) {
const { control, handleSubmit, formState } = useForm<VendorCompanyRosterFormValues>({
defaultValues: emptyVendorCompanyRosterForm,
resolver: zodResolver(vendorCompanyRosterSchema),
mode: "onChange",
});
return (
<form onSubmit={handleSubmit(onValid)}>
<VendorRosterFormFields control={control} errors={formState.errors} />
<button type="submit">Submit</button>
</form>
);
}
function fillCompanyBasics(): void {
fireEvent.change(screen.getByRole("textbox", { name: "Company (required)" }), {
target: { value: "Gateway Plumbing" },
});
fireEvent.change(screen.getByRole("textbox", { name: "Company Phone (optional)" }), {
target: { value: "3145550100" },
});
}
describe("VendorRosterFormFields address autocomplete", () => {
it("keeps the picked street when a suggestion whose label differs is clicked", async () => {
const user = userEvent.setup();
renderWithProviders(<AddressFormHarness onValid={vi.fn()} />, { withAuth: false });
const street = screen.getByRole("combobox", { name: "Street Address (required)" });
await user.type(street, TYPED_STREET);
const listbox = await screen.findByRole("listbox");
const option = within(listbox)
.getAllByRole("option")
.find((candidate) => candidate.textContent === FIRST_SUGGESTION.label);
expect(option).toBeDefined();
expect(FIRST_SUGGESTION.label).not.toBe(FIRST_SUGGESTION.street);
await user.click(option as HTMLElement);
await waitFor(() => {
expect(street).toHaveValue(FIRST_SUGGESTION.street);
});
expect(screen.getByRole("textbox", { name: "City (required)" })).toHaveValue(
FIRST_SUGGESTION.city,
);
expect(screen.getByRole("textbox", { name: "State (required)" })).toHaveValue(
FIRST_SUGGESTION.state,
);
});
it("submits the settled structured parts, not the option label", async () => {
const user = userEvent.setup();
const onValid = vi.fn();
renderWithProviders(<AddressFormHarness onValid={onValid} />, { withAuth: false });
const street = screen.getByRole("combobox", { name: "Street Address (required)" });
await user.type(street, TYPED_STREET);
const listbox = await screen.findByRole("listbox");
await user.click(within(listbox).getAllByRole("option")[0] as HTMLElement);
fillCompanyBasics();
await user.click(screen.getByRole("button", { name: "Submit" }));
await waitFor(() => expect(onValid).toHaveBeenCalledTimes(1));
const values = onValid.mock.calls[0]?.[0] as VendorCompanyRosterFormValues;
expect(values.address).toBe(FIRST_SUGGESTION.street);
expect(values.city).toBe(FIRST_SUGGESTION.city);
expect(values.state).toBe(FIRST_SUGGESTION.state);
});
it("clears every structured address part from the autocomplete clear control", async () => {
const user = userEvent.setup();
renderWithProviders(<AddressFormHarness onValid={vi.fn()} />, { withAuth: false });
const street = screen.getByRole("combobox", { name: "Street Address (required)" });
await user.type(street, TYPED_STREET);
const listbox = await screen.findByRole("listbox");
await user.click(within(listbox).getAllByRole("option")[0] as HTMLElement);
expect(screen.getByRole("textbox", { name: "City (required)" })).toHaveValue(
FIRST_SUGGESTION.city,
);
expect(screen.getByRole("textbox", { name: "State (required)" })).toHaveValue(
FIRST_SUGGESTION.state,
);
await user.click(screen.getByRole("button", { name: "Clear" }));
expect(street).toHaveValue("");
expect(screen.getByRole("textbox", { name: "City (required)" })).toHaveValue("");
expect(screen.getByRole("textbox", { name: "State (required)" })).toHaveValue("");
});
});
describe("VendorRosterFormFields map preview empty state", () => {
it("shows the accessible placeholder while incomplete, then the map once complete", async () => {
const user = userEvent.setup();
renderWithProviders(<AddressFormHarness onValid={vi.fn()} />, { withAuth: false });
expect(screen.getByRole("status")).toHaveTextContent(/street, city, and state/i);
expect(screen.queryByTitle(/^Map of /)).not.toBeInTheDocument();
await user.type(
screen.getByRole("combobox", { name: "Street Address (required)" }),
TYPED_STREET,
);
expect(screen.getByRole("status")).toBeInTheDocument();
fireEvent.change(screen.getByRole("textbox", { name: "City (required)" }), {
target: { value: "Theodore" },
});
fireEvent.change(screen.getByRole("textbox", { name: "State (required)" }), {
target: { value: "AL" },
});
expect(await screen.findByTitle(`Map of ${TYPED_STREET} Theodore AL`)).toBeInTheDocument();
expect(screen.queryByRole("status")).not.toBeInTheDocument();
});
});

View file

@ -0,0 +1,90 @@
import { screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import {
VENDOR_DIALOG_SURFACE_MAX_HEIGHT,
VendorDialog,
} from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog";
import { VendorDialogCompanyCard } from "@/app/(protected)/workorders/_components/list/table/cells/vendor-dialog-company-card";
import type { VendorDropdownItem } from "@/domain/work-orders/types/work-order";
import { renderWithProviders } from "@/test/test-utils";
vi.mock("@/domain/vendors/use-cases/use-create-vendor", () => ({
useCreateVendor: () => ({
mutateAsync: vi.fn(),
isPending: false,
}),
}));
const LIVE_VENDOR: VendorDropdownItem = {
id: 10,
companyName: "Gateway Plumbing",
contactName: "Jordan Lee",
tradeSpecialties: "Plumbing",
address: "900 Live St",
distanceMiles: 4.2,
email: "dispatch@gateway.test",
};
describe("VendorDialogCompanyCard map preview queries", () => {
it("embeds the live vendor address", () => {
renderWithProviders(
<VendorDialogCompanyCard company="Gateway Plumbing" vendor={LIVE_VENDOR} />,
{ withAuth: false },
);
const frame = screen.getByTitle("Map of 900 Live St");
expect(frame).toHaveAttribute(
"src",
"https://www.google.com/maps?q=900%20Live%20St&output=embed",
);
});
it("embeds the frozen company's composed snapshot address, not the live one", () => {
renderWithProviders(
<VendorDialogCompanyCard
company="Frozen Vendor"
vendor={LIVE_VENDOR}
frozenCompany={{
name: "Frozen Vendor",
address: "200 Vendor Ave",
city: "Chicago",
state: "IL",
zip: "60602",
}}
/>,
{ withAuth: false },
);
const frame = screen.getByTitle("Map of 200 Vendor Ave, Chicago, IL, 60602");
expect(frame).toHaveAttribute(
"src",
"https://www.google.com/maps?q=200%20Vendor%20Ave%2C%20Chicago%2C%20IL%2C%2060602&output=embed",
);
expect(screen.queryByTitle("Map of 900 Live St")).not.toBeInTheDocument();
});
});
describe("VendorDialog scroll boundary", () => {
it("bounds the dialog surface and scrolls internally", () => {
renderWithProviders(
<VendorDialog
open
onOpenChange={vi.fn()}
vendorId={10}
company="Gateway Plumbing"
tech="Jordan Lee"
vendors={[LIVE_VENDOR]}
onSave={vi.fn()}
/>,
);
const surface = screen.getByRole("dialog");
expect(VENDOR_DIALOG_SURFACE_MAX_HEIGHT).toBe("55vh");
expect(surface.style.maxHeight).toBe(VENDOR_DIALOG_SURFACE_MAX_HEIGHT);
expect(surface.style.display).toBe("flex");
const content = screen.getByRole("dialog").querySelector(".MuiDialogContent-root");
expect(content).not.toBeNull();
expect(content).toHaveStyle({ overflowY: "auto", minHeight: "0px" });
});
});

View file

@ -0,0 +1,68 @@
import { screen } from "@testing-library/react";
import { describe, expect, it } from "vitest";
import { AddressMapPreview } from "@/components/common/address-map-preview";
import { renderWithProviders } from "@/test/test-utils";
const complete = { street: "6735 Trippel Rd", city: "Theodore", state: "AL" };
function mapFrame() {
return screen.getByTitle("Map of 6735 Trippel Rd Theodore AL");
}
describe("AddressMapPreview structured input", () => {
it("renders a compact accessible placeholder while the address is incomplete", () => {
renderWithProviders(
<AddressMapPreview address={{ street: "6735 Trippel Rd" }} placeholderWhenIncomplete />,
{ withAuth: false },
);
const placeholder = screen.getByRole("status");
expect(placeholder).toHaveTextContent(/street, city, and state/i);
expect(screen.queryByTitle(/^Map of /)).not.toBeInTheDocument();
});
it("transitions from the placeholder to the iframe once the address completes", () => {
const { rerender } = renderWithProviders(
<AddressMapPreview
address={{ street: "6735 Trippel Rd", city: "Theodore" }}
placeholderWhenIncomplete
/>,
{ withAuth: false },
);
expect(screen.getByRole("status")).toBeInTheDocument();
rerender(<AddressMapPreview address={complete} placeholderWhenIncomplete />);
const frame = mapFrame();
expect(frame).toBeInTheDocument();
expect(frame).toHaveAttribute(
"src",
"https://www.google.com/maps?q=6735%20Trippel%20Rd%20Theodore%20AL&output=embed",
);
expect(screen.queryByRole("status")).not.toBeInTheDocument();
});
it("stays blank for incomplete input when the placeholder is not opted into", () => {
const { container } = renderWithProviders(
<AddressMapPreview address={{ street: "6735 Trippel Rd" }} />,
{ withAuth: false },
);
expect(container.querySelector("iframe")).toBeNull();
expect(screen.queryByRole("status")).not.toBeInTheDocument();
});
});
describe("AddressMapPreview pre-composed query", () => {
it("renders the map only when the saved query is non-empty", () => {
const { container, rerender } = renderWithProviders(<AddressMapPreview query="900 Live St" />, {
withAuth: false,
});
expect(screen.getByTitle("Map of 900 Live St")).toBeInTheDocument();
rerender(<AddressMapPreview query=" " />);
expect(container.querySelector("iframe")).toBeNull();
expect(screen.queryByRole("status")).not.toBeInTheDocument();
});
});

View file

@ -66,7 +66,7 @@ describe("vendor roster mapper", () => {
name: "Gateway Plumbing",
companyPhone: "314-555-0100",
email: "dispatch@gateway.test",
googleMapsUrl: "https://maps.google.com/x",
googleMapsUrl: "http://maps.google.com/legacy",
tradeSpecialties: "Plumbing",
rowVersion: "rv-2",
technicians: [{ contactName: "Adam", phone: "3145550198", preferredContact: "Phone" }],
@ -82,6 +82,9 @@ describe("vendor roster mapper", () => {
expect.objectContaining({ contactName: "Adam", phone: "(314) 555-0198" }),
]);
expect(payload).not.toHaveProperty("tradeSpecialties");
// The stored Maps URL is deprecated: a legacy value reaching the mapper
// (from stale callers or old records) must not be emitted to the backend.
expect(payload).not.toHaveProperty("googleMapsUrl");
});
it("omits rowVersion from the create payload when absent", () => {

View file

@ -3,7 +3,6 @@ import {
VENDOR_COMPANY_CONTACT_REQUIRED_MESSAGE,
emptyRosterTechnician,
emptyVendorCompanyRosterForm,
isAbsoluteHttpsUrl,
VENDOR_NOTES_MAX_LENGTH,
vendorCompanyRosterSchema,
vendorCompanyRosterUpdateSchema,
@ -105,21 +104,26 @@ describe("vendorCompanyRosterSchema", () => {
expect(result.success).toBe(true);
});
it("rejects malformed emails, non-HTTPS map URLs, and partial phones", () => {
it("rejects malformed emails and partial phones", () => {
const result = vendorCompanyRosterSchema.safeParse({
...baseCompany,
email: "not-an-email",
googleMapsUrl: "http://maps.google.com/x",
technicians: [{ ...emptyRosterTechnician, contactName: "Adam", phone: "(314)" }],
});
expect(result.success).toBe(false);
});
it("rejects a maps URL that is a non-absolute https prefix string", () => {
expect(isAbsoluteHttpsUrl("https://")).toBe(false);
expect(isAbsoluteHttpsUrl("maps.google.com/x")).toBe(false);
expect(isAbsoluteHttpsUrl("https://maps.google.com/x")).toBe(true);
expect(isAbsoluteHttpsUrl("")).toBe(true);
it("tolerates a legacy stored Maps URL instead of failing HTTPS validation", () => {
// Legacy records carry a stored googleMapsUrl — often plain HTTP. The form
// no longer holds the field, so leftover values loaded from old data must
// neither fail validation nor re-enter the parsed output.
const result = vendorCompanyRosterSchema.safeParse({
...baseCompany,
googleMapsUrl: "http://maps.google.com/legacy",
});
expect(result.success).toBe(true);
if (result.success) expect(result.data).not.toHaveProperty("googleMapsUrl");
});
it("rejects an unknown preferred contact", () => {

View file

@ -74,7 +74,6 @@ function formValues(
city: roster.city,
state: roster.state,
zip: roster.zip,
googleMapsUrl: roster.googleMapsUrl,
notes: roster.notes,
technicians: [newTechnician],
...overrides,

View file

@ -36,7 +36,6 @@ const values = {
city: roster.city,
state: roster.state,
zip: roster.zip,
googleMapsUrl: roster.googleMapsUrl,
notes: roster.notes,
technicians: roster.technicians.map((technician) => ({
id: technician.id,