From c849619bee9c02615058d933023e7708172d2290 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 20 Oct 2026 13:15:29 +0200 Subject: [PATCH] fix(settings): one action row on the Dawarich card The connected card showed the home country field and a second row of buttons (Choose home, Save) above the card's own row of details, menu and primary action. The home editor now opens from the card menu, or from a prompt while the home place or country is missing, and carries its own Cancel and Save only while it is open. --- .../__tests__/dawarich-card.test.tsx | 99 ++++++++ .../settings/integrations/dawarich-card.tsx | 215 ++++++++++-------- 2 files changed, 222 insertions(+), 92 deletions(-) create mode 100644 src/components/settings/integrations/__tests__/dawarich-card.test.tsx diff --git a/src/components/settings/integrations/__tests__/dawarich-card.test.tsx b/src/components/settings/integrations/__tests__/dawarich-card.test.tsx new file mode 100644 index 000000000..399c3c7d7 --- /dev/null +++ b/src/components/settings/integrations/__tests__/dawarich-card.test.tsx @@ -0,0 +1,99 @@ +/** + * The connected Dawarich card has one action row. + * + * The home place and home country used to sit permanently in the card body + * with their own row of buttons (Choose home, Save) above the card's own row + * of details, menu and primary action. The editor now opens from the menu, or + * from a prompt while nothing is set, and only then carries its own Cancel and + * Save. + */ +import { describe, expect, it, vi } from "vitest"; +import { createElement } from "react"; +import { renderToStaticMarkup } from "react-dom/server"; + +const status = vi.hoisted(() => ({ + current: { homePlaceSet: true, homeCountry: "DE", daysStored: 12 }, +})); + +vi.mock("@tanstack/react-query", () => ({ + useMutation: () => ({ mutate: vi.fn(), isPending: false }), + useQueryClient: () => ({ invalidateQueries: vi.fn() }), + useQuery: ({ queryKey }: { queryKey: readonly unknown[] }) => + JSON.stringify(queryKey).includes("places") + ? { data: { places: [] }, isLoading: false, isError: false } + : { data: status.current, isLoading: false, isError: false }, +})); + +let lastCardProps: IntegrationCardProps | null = null; +vi.mock("../integration-card", async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + IntegrationCard: (props: IntegrationCardProps) => { + lastCardProps = props; + return createElement(actual.IntegrationCard, props); + }, + }; +}); + +import { I18nProvider } from "@/lib/i18n/context"; +import type { IntegrationCardProps } from "../integration-card"; +import { DawarichCard } from "../dawarich-card"; +import type { IntegrationStatusViewModel } from "../shared"; +import { healthFixture } from "./health-fixture"; + +function render() { + return renderToStaticMarkup( + + + , + ); +} + +function actionRows(html: string): number { + return ( + html.match(/data-slot="(?:settings-card-actions|integration-actions)"/g) ?? + [] + ).length; +} + +describe("DawarichCard", () => { + it("has exactly one action row while the home editor is closed", () => { + status.current = { homePlaceSet: true, homeCountry: "DE", daysStored: 12 }; + const html = render(); + expect(actionRows(html)).toBe(1); + expect(html).toContain('data-slot="integration-actions"'); + expect(html).not.toContain('data-testid="dawarich-save-home"'); + expect(html).not.toContain('id="dawarich-home-country"'); + // Nothing to prompt for: home and country are both set. + expect(html).not.toContain('data-testid="dawarich-pick-home"'); + }); + + it("opens the home editor from the menu", () => { + render(); + const home = lastCardProps?.menu?.find((item) => item.id === "home"); + expect(home?.onSelect).toBeTypeOf("function"); + expect(home?.label).toBe("Choose home"); + }); + + it("prompts inline while the home place or country is missing", () => { + status.current = { + homePlaceSet: false, + homeCountry: null as never, + daysStored: 0, + }; + const html = render(); + expect(html).toContain('data-testid="dawarich-pick-home"'); + expect(html).toContain('data-slot="inline-action"'); + expect(actionRows(html)).toBe(1); + }); +}); diff --git a/src/components/settings/integrations/dawarich-card.tsx b/src/components/settings/integrations/dawarich-card.tsx index a0ca2cf70..1bdc6f32d 100644 --- a/src/components/settings/integrations/dawarich-card.tsx +++ b/src/components/settings/integrations/dawarich-card.tsx @@ -48,6 +48,7 @@ import { } from "./setup-guide-link"; import type { IntegrationStatusViewModel } from "./shared"; import { QueryErrorRow } from "@/components/ui/query-error-row"; +import { InlineAction } from "@/components/ui/inline-action"; interface DawarichStatus { connected: boolean; @@ -91,6 +92,7 @@ export function DawarichCard({ const connected = viewModel?.connected ?? false; const test = useConnectionTest("/api/dawarich/test"); const [editing, setEditing] = useState(false); + const [homeEditing, setHomeEditing] = useState(false); const formRef = useRef(null); const refresh = () => { @@ -181,6 +183,12 @@ export function DawarichCard({ disabled: test.testing, onSelect: test.run, }, + { + id: "home", + label: t("settings.dawarichHomePlaceChange"), + icon: MapPinned, + onSelect: () => setHomeEditing(true), + }, { id: "credentials", label: t("settings.integrationCard.menu.address"), @@ -231,22 +239,35 @@ export function DawarichCard({ primary={{ onReconnect: openForm, onResume: openForm }} menu={menu} > - + {editing ? form : null} ); } -/** The home place and home country, and what is stored. */ -function DawarichHome() { +/** + * The home place and home country, and what is stored. + * + * The editor is closed by default and opens from the card menu (or from the + * prompt when nothing is set yet), like the address form beside it. Open, it + * carries its own Cancel and Save; closed, the card has exactly one action + * row, its own, at the bottom (UI standards ยง12). It used to show the + * country field and a second always-on row of buttons above the card's. + */ +function DawarichHome({ + editing, + onEditingChange, +}: { + editing: boolean; + onEditingChange: (open: boolean) => void; +}) { const { t } = useTranslations(); const queryClient = useQueryClient(); const status = useQuery({ queryKey: queryKeys.dawarichStatus(), queryFn: () => apiGet("/api/dawarich/status"), }); - const [picking, setPicking] = useState(false); const [placeId, setPlaceId] = useState(""); const [country, setCountry] = useState(""); const places = useQuery({ @@ -255,7 +276,7 @@ function DawarichHome() { apiGet<{ places: Array<{ id: number; name: string }> }>( "/api/dawarich/places", ), - enabled: picking, + enabled: editing, }); const save = useMutation({ @@ -265,7 +286,7 @@ function DawarichHome() { }) => apiPatch("/api/dawarich/settings", body), onSuccess: () => { toast.success(t("settings.dawarichHomeSaved")); - setPicking(false); + onEditingChange(false); setPlaceId(""); setCountry(""); queryClient.invalidateQueries({ queryKey: queryKeys.dawarichStatus() }); @@ -293,95 +314,105 @@ function DawarichHome() {

{t("settings.dawarichDaysStored", { count: data.daysStored })}

- {picking ? ( -
- - {places.isError ? ( - void places.refetch()} - /> - ) : ( - setPlaceId(e.target.value)} - > - - {(places.data?.places ?? []).map((place) => ( -
); }