mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 12:32:43 +00:00
fix(body-sites): skip the site suggestions read without the visits section
A delegate who may keep the illness journal but not read the visits would be refused by GET /api/body-sites on every open of the condition form, and each refusal writes an access-denied row. The field now asks only when the grant covers the visits' section, and the condition page shows the site as text rather than a link in that case. Refs #1025
This commit is contained in:
1 parent
14f0fc4d4f
commit
645032ec97
4 files changed
+41
-7
No files matched your search
@@ -8,10 +8,14 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
*
|
||||
* - the suggestions are the server's list, in its order;
|
||||
* - the site already typed is not suggested back to itself;
|
||||
* - no list (a refused or failed read) is a plain field, not an empty list.
|
||||
* - no list (a refused or failed read) is a plain field, not an empty list;
|
||||
* - a delegate whose grant does not include the visits' section is not
|
||||
* sent to a read that would refuse them.
|
||||
*
|
||||
* Mutation check (run, seen red): drop the `list` attribute from the input →
|
||||
* "offers the sites the record holds" goes red.
|
||||
* Mutation checks (each run, each seen red): drop the `list` attribute from
|
||||
* the input → "offers the sites the record holds" goes red; enable the read
|
||||
* unconditionally in `canReadBodySites` → "does not ask for the list" goes
|
||||
* red.
|
||||
*/
|
||||
|
||||
vi.mock("@/lib/api/api-fetch", async () => {
|
||||
@@ -24,6 +28,7 @@ vi.mock("@/lib/api/api-fetch", async () => {
|
||||
import { I18nProvider } from "@/lib/i18n/context";
|
||||
import { queryKeys } from "@/lib/query-keys";
|
||||
import type { BodySiteListDTO } from "@/lib/body-sites/dto";
|
||||
import { canReadBodySites } from "@/hooks/use-body-sites";
|
||||
import { BodySiteFields } from "../body-site-fields";
|
||||
|
||||
function render(data: BodySiteListDTO | null, typed = ""): string {
|
||||
@@ -77,4 +82,12 @@ describe("<BodySiteFields>", () => {
|
||||
expect(html).not.toContain("<datalist");
|
||||
expect(html).not.toContain(" list=");
|
||||
});
|
||||
|
||||
it("does not ask for the list without the visits' section", () => {
|
||||
expect(canReadBodySites(null)).toBe(true);
|
||||
expect(canReadBodySites(undefined)).toBe(true);
|
||||
expect(canReadBodySites(["profile", "illness"])).toBe(true);
|
||||
expect(canReadBodySites(["illness"])).toBe(false);
|
||||
expect(canReadBodySites([])).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -10,14 +10,18 @@
|
||||
* types. The list comes from the server (`useBodySiteSuggestions`), which
|
||||
* decrypts and folds the sites; the browser's own `datalist` does the
|
||||
* narrowing, so a suggestion is a nudge and never a restriction. A caller whose
|
||||
* grant cannot read the list gets a plain text field.
|
||||
* grant cannot read the list gets a plain text field and no request.
|
||||
*/
|
||||
import { useId } from "react";
|
||||
|
||||
import { FieldGroup } from "@/components/ui/field-group";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { NativeSelect } from "@/components/ui/native-select";
|
||||
import { useBodySiteSuggestions } from "@/hooks/use-body-sites";
|
||||
import {
|
||||
canReadBodySites,
|
||||
useBodySiteSuggestions,
|
||||
} from "@/hooks/use-body-sites";
|
||||
import { useRecordCapabilities } from "@/hooks/use-record-capabilities";
|
||||
import { useTranslations } from "@/lib/i18n/context";
|
||||
import {
|
||||
LATERALITIES,
|
||||
@@ -46,7 +50,11 @@ export function BodySiteFields({
|
||||
const listId = `${useId()}-body-sites`;
|
||||
const typed = bodySite.trim().toLowerCase();
|
||||
// The exact site already typed is not suggested back to itself.
|
||||
const suggestions = useBodySiteSuggestions().filter(
|
||||
// The list is read in the visits' section. A delegate without it (say, one
|
||||
// who may only keep the illness journal) is not sent to a read that would
|
||||
// refuse them on every open; the field simply offers nothing.
|
||||
const { sections } = useRecordCapabilities();
|
||||
const suggestions = useBodySiteSuggestions(canReadBodySites(sections)).filter(
|
||||
(site) => site.toLowerCase() !== typed,
|
||||
);
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { PageHeader } from "@/components/ui/page-header";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { useTranslations, useFormatters } from "@/lib/i18n/context";
|
||||
import { bodySiteText } from "@/components/encounters/encounter-labels";
|
||||
import { canReadBodySites } from "@/hooks/use-body-sites";
|
||||
|
||||
import { IllnessCorrelationCard } from "./illness-correlation-card";
|
||||
import { IllnessDayTimeline } from "./illness-day-timeline";
|
||||
@@ -46,7 +47,7 @@ export function IllnessEpisodeDetail({ episodeId }: { episodeId: string }) {
|
||||
const { canManageDomain, sections } = useRecordCapabilities();
|
||||
// The body-site view is read in the visits' section; a grant without it
|
||||
// gets the site as text rather than a link that would be refused.
|
||||
const canOpenBodySites = sections === null || sections.includes("profile");
|
||||
const canOpenBodySites = canReadBodySites(sections);
|
||||
const canManageIllness = canManageDomain("illness");
|
||||
const fmt = useFormatters();
|
||||
const {
|
||||
|
||||
@@ -40,6 +40,18 @@ export function useBodySites(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the caller may ask for the list at all. It is read in the visits'
|
||||
* section (`profile`), so a grant without it would be refused on every open;
|
||||
* `null` or absent sections mean the whole record (one's own, or an
|
||||
* entire-record grant).
|
||||
*/
|
||||
export function canReadBodySites(
|
||||
sections: readonly string[] | null | undefined,
|
||||
): boolean {
|
||||
return !sections || sections.includes("profile");
|
||||
}
|
||||
|
||||
/**
|
||||
* The site names a body-site field suggests: every site the record holds,
|
||||
* most used first. A failed or refused read (a grant without the visits
|
||||
|
||||
Reference in new issue
Block a user