mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-09 04:50:59 +00:00
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.
This commit is contained in:
1 parent
ff583104a8
commit
c849619bee
2 files changed
+222
-92
No files matched your search
@@ -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<typeof import("../integration-card")>();
|
||||
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(
|
||||
<I18nProvider initialLocale="en">
|
||||
<DawarichCard
|
||||
viewModel={
|
||||
{
|
||||
integration: "dawarich",
|
||||
connected: true,
|
||||
configured: true,
|
||||
health: healthFixture(),
|
||||
} as unknown as IntegrationStatusViewModel
|
||||
}
|
||||
/>
|
||||
</I18nProvider>,
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -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<HTMLFormElement | null>(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}
|
||||
>
|
||||
<DawarichHome />
|
||||
<DawarichHome editing={homeEditing} onEditingChange={setHomeEditing} />
|
||||
<ActionResultLines provider="dawarich" test={test} />
|
||||
{editing ? form : null}
|
||||
</IntegrationCard>
|
||||
);
|
||||
}
|
||||
|
||||
/** 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<DawarichStatus>("/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() {
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.dawarichDaysStored", { count: data.daysStored })}
|
||||
</p>
|
||||
{picking ? (
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="dawarich-home-place">
|
||||
{t("settings.dawarichHomePlace")}
|
||||
</Label>
|
||||
{places.isError ? (
|
||||
<QueryErrorRow
|
||||
inset
|
||||
error={places.error}
|
||||
slot="dawarich-places-error"
|
||||
message={t(errorKeyOf(places.error))}
|
||||
onRetry={() => void places.refetch()}
|
||||
/>
|
||||
) : (
|
||||
<NativeSelect
|
||||
id="dawarich-home-place"
|
||||
value={placeId}
|
||||
disabled={places.isLoading}
|
||||
onChange={(e) => setPlaceId(e.target.value)}
|
||||
>
|
||||
<option value="">{t("settings.dawarichHomePlaceChoose")}</option>
|
||||
{(places.data?.places ?? []).map((place) => (
|
||||
<option key={place.id} value={String(place.id)}>
|
||||
{place.name}
|
||||
{!editing && (!data.homePlaceSet || !data.homeCountry) ? (
|
||||
<InlineAction
|
||||
onClick={() => onEditingChange(true)}
|
||||
data-testid="dawarich-pick-home"
|
||||
>
|
||||
<MapPinned aria-hidden="true" />
|
||||
{t("settings.dawarichHomePlaceChange")}
|
||||
</InlineAction>
|
||||
) : null}
|
||||
{editing ? (
|
||||
<div className="space-y-3" data-slot="dawarich-home-editor">
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="dawarich-home-place">
|
||||
{t("settings.dawarichHomePlace")}
|
||||
</Label>
|
||||
{places.isError ? (
|
||||
<QueryErrorRow
|
||||
inset
|
||||
error={places.error}
|
||||
slot="dawarich-places-error"
|
||||
message={t(errorKeyOf(places.error))}
|
||||
onRetry={() => void places.refetch()}
|
||||
/>
|
||||
) : (
|
||||
<NativeSelect
|
||||
id="dawarich-home-place"
|
||||
value={placeId}
|
||||
disabled={places.isLoading}
|
||||
onChange={(e) => setPlaceId(e.target.value)}
|
||||
>
|
||||
<option value="">
|
||||
{t("settings.dawarichHomePlaceChoose")}
|
||||
</option>
|
||||
))}
|
||||
</NativeSelect>
|
||||
)}
|
||||
{(places.data?.places ?? []).map((place) => (
|
||||
<option key={place.id} value={String(place.id)}>
|
||||
{place.name}
|
||||
</option>
|
||||
))}
|
||||
</NativeSelect>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="dawarich-home-country" className="flex">
|
||||
{t("settings.dawarichHomeCountry")}
|
||||
</Label>
|
||||
<Input
|
||||
id="dawarich-home-country"
|
||||
value={country}
|
||||
onChange={(e) => setCountry(e.target.value)}
|
||||
placeholder={data.homeCountry ?? "DE"}
|
||||
maxLength={2}
|
||||
autoComplete="off"
|
||||
autoCapitalize="characters"
|
||||
spellCheck={false}
|
||||
className="w-24"
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.dawarichHomeCountryHelp")}
|
||||
</p>
|
||||
</div>
|
||||
<SettingsCardActions>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={() => onEditingChange(false)}
|
||||
data-testid="dawarich-cancel-home"
|
||||
>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
disabled={
|
||||
save.isPending ||
|
||||
(placeId === "" && !/^[A-Z]{2}$/.test(countryValue))
|
||||
}
|
||||
onClick={() =>
|
||||
save.mutate({
|
||||
...(placeId !== "" ? { homePlaceId: Number(placeId) } : {}),
|
||||
...(/^[A-Z]{2}$/.test(countryValue)
|
||||
? { homeCountry: countryValue }
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
data-testid="dawarich-save-home"
|
||||
>
|
||||
{save.isPending ? (
|
||||
<Loader2 className="size-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Save className="size-3.5" aria-hidden="true" />
|
||||
)}
|
||||
{t("settings.dawarichHomeSave")}
|
||||
</Button>
|
||||
</SettingsCardActions>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="dawarich-home-country" className="flex">
|
||||
{t("settings.dawarichHomeCountry")}
|
||||
</Label>
|
||||
<Input
|
||||
id="dawarich-home-country"
|
||||
value={country}
|
||||
onChange={(e) => setCountry(e.target.value)}
|
||||
placeholder={data.homeCountry ?? "DE"}
|
||||
maxLength={2}
|
||||
autoComplete="off"
|
||||
autoCapitalize="characters"
|
||||
spellCheck={false}
|
||||
className="w-24"
|
||||
/>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
{t("settings.dawarichHomeCountryHelp")}
|
||||
</p>
|
||||
</div>
|
||||
<SettingsCardActions>
|
||||
{picking ? null : (
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
onClick={() => setPicking(true)}
|
||||
data-testid="dawarich-pick-home"
|
||||
>
|
||||
<MapPinned className="size-3.5" aria-hidden="true" />
|
||||
{t("settings.dawarichHomePlaceChange")}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="min-h-11 sm:min-h-9"
|
||||
disabled={
|
||||
save.isPending ||
|
||||
(placeId === "" && !/^[A-Z]{2}$/.test(countryValue))
|
||||
}
|
||||
onClick={() =>
|
||||
save.mutate({
|
||||
...(placeId !== "" ? { homePlaceId: Number(placeId) } : {}),
|
||||
...(/^[A-Z]{2}$/.test(countryValue)
|
||||
? { homeCountry: countryValue }
|
||||
: {}),
|
||||
})
|
||||
}
|
||||
data-testid="dawarich-save-home"
|
||||
>
|
||||
{save.isPending ? (
|
||||
<Loader2 className="size-3.5 animate-spin motion-reduce:animate-none" />
|
||||
) : (
|
||||
<Save className="size-3.5" aria-hidden="true" />
|
||||
)}
|
||||
{t("settings.dawarichHomeSave")}
|
||||
</Button>
|
||||
</SettingsCardActions>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user