// --------------------------------------------------------------------------- // settings / ModulePermsList.tsx — THE per-database permission list. (wave 33, // owner items 10 + 11 · W33-T34 / W33-T38.) // // ⭐ WHY THIS FILE EXISTS, IN THE OWNER'S OWN WORDS: *"under manage user (and // manage agent too since its the same exact layout)"*. "The same exact layout" // has exactly one honest implementation, and it is not two components kept in // step by intention. `PermsEditor` and `ManageAgentPane` both render THIS, so a // change to the disclosure, the unenforced sentence or the summary line reaches // both or neither. // // ⛔ THE ALTERNATIVE, NAMED SO NOBODY RE-DERIVES IT. Copying the JSX into the // agent pane would have been faster today and is the exact shape of every defect // this repo keeps re-finding: `may_open` vs the automation table picker (wave // 20), `clean_fields` vs `_clean_field` (wave 28), `FILTER_OPS`' three mirrors. // Two renderings of one rule disagree in front of a user, and each looks correct // read on its own. // // PURELY PRESENTATIONAL. It owns exactly one piece of state — WHICH row is // expanded — because that is a property of this list and of nothing else. Every // value it shows and every write it makes belongs to the caller: the account // editor PUTs to `/admin/users/{u}/perms`, the agent editor to its own door, and // this component knows about neither. // --------------------------------------------------------------------------- import { useState } from "react"; import type { ReactNode } from "react"; import { FilterBuilderPanel, FieldsHidePanel } from "../filter-kit"; import type { FilterTree } from "../customer-grid/types"; import type { PermsModule, PermsRecord } from "./permsModel"; import { filterOf, hiddenSet, hideableKeys, identityKey, moduleSummary } from "./permsModel"; import "./perms.css"; export interface ModulePermsListProps { /** In server order — the order the sections render in. */ modules: PermsModule[]; /** The DRAFT being edited, keyed by module. */ entries: PermsRecord; onAccess: (key: string, on: boolean) => void; onFilter: (key: string, next: FilterTree | null) => void; onToggleHidden: (key: string, fieldKey: string) => void; onSetHidden: (key: string, keys: string[]) => void; /** People this tenant can name in a `user` condition. Absent ⇒ the panel says so. */ userOptions?: string[]; /** Anything the CALLER wants in a row's head — the account editor's "Copy to…" * door. Returning null is the normal case and costs the row nothing. */ headExtra?: (m: PermsModule) => ReactNode; /** Shown when the tenant governs nothing at all. */ emptyNote?: string; } export function ModulePermsList({ modules, entries, onAccess, onFilter, onToggleHidden, onSetHidden, userOptions, headExtra, emptyNote, }: ModulePermsListProps) { // ⭐ Owner item 11: *"the database should not show all immediately the detail // (for filter or hide fields)"*. Single-valued rather than a Set, deliberately // — the complaint was a wall of panels, and an accordion cannot become one by // accumulation. const [openDetail, setOpenDetail] = useState(null); if (modules.length === 0) { return (

{emptyNote ?? "This workspace has no databases whose access can be restricted."}

); } return ( <> {modules.map((m) => { const entry = entries[m.key]; const on = entry?.access === true; const schemaless = m.fields.length === 0; const shown = openDetail === m.key; // ⛔⛔ `!== false`, NOT truthiness. `verify_ui`'s NC // `perms-editor-renders-controls-before-the-record-arrives` injects a raw // module literal with no `enforced` key; read as truthiness that module // renders as UNENFORCED — no checkbox — and the NC stops biting for a // reason unrelated to what it guards [[gate-negative-control]]. It caught // exactly that within an hour of the flag existing. Stated the same way // `permsModel.parsePermsPayload` states it: only an explicit `false` // disarms a row, because every module a payload could carry before this // flag existed WAS enforced [[default-must-pass-its-own-guard]]. const enforced = m.enforced !== false; const canDetail = on && !schemaless && enforced; // ⛔ A database this editor does not decide gets NO access toggle — not a // disabled one, not an unchecked one. An unchecked box beside a name // reads as "denied", and nothing here denies it. if (!enforced) { return (
{m.label} Not set here

Access to this database is decided by who created it and who it has been shared with, not by this editor. A rule saved here would not be applied, so none is offered.

); } return (
{/* ⚠ Wrapped rather than left as siblings of the toggle: `.set-perm-modhead` is `space-between` and lives in `index.css`, which is lane B's under contract C4. Grouping the trailing controls keeps every new one off that rule — one flex child in, one flex child out. */} {moduleSummary(entry, schemaless)} {canDetail ? ( ) : null} {headExtra?.(m) ?? null}
{/* R9's fail-closed rendering: no readable schema means the access toggle and nothing else. The record it saves says the same thing — no filter, no hidden fields — so the editor and the payload cannot disagree (`permsModel.toPutBody`). */} {on && schemaless ? (

No field list is available for this database, so access is all this editor can set for it. Conditions and hidden fields need a schema.

) : null} {canDetail && shown ? (
onFilter(m.key, next)} userOptions={userOptions} />
) : null}
); })} ); }