mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 15:02:04 +00:00
fix(capture): ask each capture surface's own section before offering it
The capture FAB rides the bottom bar on every authenticated page, and its
fallback arm was section-blind:
return caps.canAdd && DELEGABLE_CAPTURE_KINDS.has(kind);
`canAdd` is bound straight to the grant's LEVEL — `grantAllows(grant, "write")`,
no scope term at all. So a WRITE grant scoped to `["labs"]` published
`canAdd: true` with `writableDomains: ["labs"]`, and a delegate standing on
`/labs` was offered a weight form and a dose that the server refuses at
`grantCoversDomain`. The worst case is a WRITE grant scoped to `["documents"]`:
the vault takes no delegated write at any level, the writable list is empty, and
both kinds were still offered.
`writableDomains` is already the level × scope × route-table intersection, so
one question per kind, asked of that kind's own section, answers the scoped
case, the vault case and the mood entry's MANAGE-only create together. The
closed kind list and the separate manage arm are both dead once that lands and
are gone. The dashboard quick-entry sheets and the header menu carried the same
shape — unreachable today, because the dashboard opens only for a whole-record
grant — and move with it rather than waiting to become reachable.
The picker's legs in the affordances suite now build their capabilities from
the same table the server publishes, and four scoped cases join them: labs-only
offers nothing, measurements-only offers the reading, documents-only offers
nothing at all, and a mind-scoped manage offers the mood entry alone.
This commit is contained in:
1 parent
3e176db5b7
commit
927496236a
4 files changed
+98
-71
No files matched your search
@@ -120,6 +120,15 @@ function grant(
|
||||
};
|
||||
}
|
||||
|
||||
/** The write answer per section, for the pure helpers that take only that. */
|
||||
function writeCaps(
|
||||
level: AccountAccessEntry["level"],
|
||||
sections: ShareDomain[] | null,
|
||||
) {
|
||||
const writable = new Set(delegatedDomains(level, sections, "write"));
|
||||
return { canWriteDomain: (domain: ShareDomain) => writable.has(domain) };
|
||||
}
|
||||
|
||||
const mockAccessRef: { value: AccountAccess } = { value: OWN_RECORD };
|
||||
|
||||
vi.mock("@/hooks/use-auth", () => ({
|
||||
@@ -486,17 +495,13 @@ describe("linking a document to an illness episode", () => {
|
||||
describe("the capture picker's kinds", () => {
|
||||
const ALL = ["measurement", "medication", "mood"] as const;
|
||||
|
||||
const OWNER_CAPS = { canAdd: true, canManageDomain: () => true };
|
||||
const WRITER_CAPS = { canAdd: true, canManageDomain: () => false };
|
||||
const READER_CAPS = { canAdd: false, canManageDomain: () => false };
|
||||
/** The picker asks one question per kind, of that kind's own section. */
|
||||
const caps = writeCaps;
|
||||
const OWNER_CAPS = { canWriteDomain: () => true };
|
||||
const WRITER_CAPS = caps("write", null);
|
||||
const READER_CAPS = caps("read", null);
|
||||
// A guardian: every section with a delegated route answers at MANAGE.
|
||||
const GUARDIAN_CAPS = {
|
||||
canAdd: true,
|
||||
canManageDomain: (domain: string) =>
|
||||
delegatedDomains("manage", null, "manage").includes(
|
||||
domain as "measurements",
|
||||
),
|
||||
};
|
||||
const GUARDIAN_CAPS = caps("manage", null);
|
||||
|
||||
it("offers everything in the caller's own record", () => {
|
||||
expect(visibleCaptureKinds(OWNER_CAPS, [...ALL])).toEqual([
|
||||
@@ -526,6 +531,36 @@ describe("the capture picker's kinds", () => {
|
||||
it("offers a read-only delegate nothing", () => {
|
||||
expect(visibleCaptureKinds(READER_CAPS, [...ALL])).toEqual([]);
|
||||
});
|
||||
|
||||
it("offers a scoped WRITE delegate only the kinds its sections cover", () => {
|
||||
// The gap this closes. `canAdd` is the grant's LEVEL with no scope term,
|
||||
// so a labs-scoped WRITE grant answered true and the picker offered a
|
||||
// weight form and a dose the server refuses at `grantCoversDomain`.
|
||||
expect(visibleCaptureKinds(caps("write", ["labs"]), [...ALL])).toEqual([]);
|
||||
expect(
|
||||
visibleCaptureKinds(caps("write", ["measurements"]), [...ALL]),
|
||||
).toEqual(["measurement"]);
|
||||
expect(
|
||||
visibleCaptureKinds(caps("write", ["measurements", "medications"]), [
|
||||
...ALL,
|
||||
]),
|
||||
).toEqual(["measurement", "medication"]);
|
||||
});
|
||||
|
||||
it("offers a documents-scoped WRITE delegate nothing at all", () => {
|
||||
// The worst case: the vault takes no delegated write at any level, so the
|
||||
// grant's writable list is empty while `canWrite` still answers true.
|
||||
expect(visibleCaptureKinds(caps("write", ["documents"]), [...ALL])).toEqual(
|
||||
[],
|
||||
);
|
||||
});
|
||||
|
||||
it("offers a scoped MANAGE delegate the mood entry only with the mind section", () => {
|
||||
expect(visibleCaptureKinds(caps("manage", ["mind"]), [...ALL])).toEqual([
|
||||
"mood",
|
||||
]);
|
||||
expect(visibleCaptureKinds(caps("manage", ["labs"]), [...ALL])).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
/* -------------------------------------------------------------------------- */
|
||||
@@ -613,7 +648,7 @@ describe("a form opened before the record answered", () => {
|
||||
});
|
||||
|
||||
it("withdraws a dashboard quick-entry sheet the delegation does not admit", () => {
|
||||
const DELEGATE = { canAdd: true, canManageDomain: () => false };
|
||||
const DELEGATE = writeCaps("write", null);
|
||||
expect(admittedQuickEntry("mood", DELEGATE)).toBe(null);
|
||||
expect(admittedQuickEntry("measurement", DELEGATE)).toBe("measurement");
|
||||
expect(admittedQuickEntry("medicationIntake", DELEGATE)).toBe(
|
||||
@@ -622,22 +657,25 @@ describe("a form opened before the record answered", () => {
|
||||
});
|
||||
|
||||
it("keeps the mood sheet for a guardian, whose mind routes answer at MANAGE", () => {
|
||||
const GUARDIAN = {
|
||||
canAdd: true,
|
||||
canManageDomain: (domain: string) => domain === "mind",
|
||||
};
|
||||
expect(admittedQuickEntry("mood", GUARDIAN)).toBe("mood");
|
||||
expect(admittedQuickEntry("mood", writeCaps("manage", null))).toBe("mood");
|
||||
});
|
||||
|
||||
it("withdraws a sheet whose section the grant's scope leaves out", () => {
|
||||
const LABS_ONLY = writeCaps("write", ["labs"]);
|
||||
for (const sheet of ["measurement", "mood", "medicationIntake"] as const) {
|
||||
expect(admittedQuickEntry(sheet, LABS_ONLY), sheet).toBe(null);
|
||||
}
|
||||
});
|
||||
|
||||
it("withdraws every quick-entry sheet from a read-only delegate", () => {
|
||||
const READER = { canAdd: false, canManageDomain: () => false };
|
||||
const READER = writeCaps("read", null);
|
||||
for (const sheet of ["measurement", "mood", "medicationIntake"] as const) {
|
||||
expect(admittedQuickEntry(sheet, READER), sheet).toBe(null);
|
||||
}
|
||||
});
|
||||
|
||||
it("leaves the owner's own sheets alone", () => {
|
||||
const OWNER_CAPS = { canAdd: true, canManageDomain: () => true };
|
||||
const OWNER_CAPS = { canWriteDomain: () => true };
|
||||
for (const sheet of ["measurement", "mood", "medicationIntake"] as const) {
|
||||
expect(admittedQuickEntry(sheet, OWNER_CAPS), sheet).toBe(sheet);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user