// --------------------------------------------------------------------------- // 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 or the summary line reaches both or neither — and // wave 36 is the proof: deleting the "Not set here" card (W36-T22 / C2, owner // item 11) took it out of the agent pane too, in the same edit, with nothing to // remember. // // ⛔ 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, hiddenSetFor, hideAllKeys, identityKey, moduleSummary, showAllKeys } 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; /** * ⭐⭐ W38-T19 — the per-database METRICS capability. Absent means the caller does not govern * it and no box is drawn, which is the account editor's and the agent editor's genuine * difference rather than a stub: an agent's reads run through `agent_rows` / `agent_may_read` * and never open a grid door, so a Metrics toggle in that room would be a stored rule nothing * applies, the exact class W36's contract C2 deleted. * * ⛔ OPTIONAL, AND FORCED RATHER THAN CHOSEN. `manage-agent/ManageAgentPane.tsx` mounts this * same component and is outside W38-T19's fence; a required prop stops it compiling, which * reds `web_ui` on a build break in a file that ticket may not repair. */ onMetrics?: (key: string, on: boolean) => void; /** * ⭐⭐ W40-T14 (owner instruction 7) — THE DRILL-IN. When supplied, the row is a checkbox and * ONE way in: this opens a full-pane editor of that database's Fields and Filters, and the row * renders no summary, no accordion, no inline panels, no Metrics box and no `headExtra`. Owner, * verbatim: *"show only the Database, with just a checkbox and an Edit button (put the Metrics * field toggle under there)"*. * * ⛔ OPTIONAL FOR THE SAME FORCED REASON `onMetrics` IS. `manage-agent/ManageAgentPane.tsx` * mounts this component and is outside W40-T14's fence; a required prop stops it compiling in a * file this ticket may not repair. Absent ⇒ every branch below is the accordion this file has * always rendered, byte for byte. */ onEdit?: (key: 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. Returning null is the normal case and costs the * row nothing. * * ⚠ NO CALLER SUPPLIES IT TODAY, AND THE PROP IS KEPT ANYWAY. It carried the account editor's * per-row "Copy to" door, which owner instruction 7 DELETED (W40-T14) — the account-level * "Apply this access to…" survives and is the whole copy affordance now. What is left here is * a seam, declared because `manage-agent/ManageAgentPane.tsx` mounts this same component from * outside that ticket's fence and removing a prop is a breaking change to a file it may not * repair. A drilled row ignores it regardless: `onEdit` means "a checkbox and one way in". */ headExtra?: (m: PermsModule) => ReactNode; /** Shown when the tenant governs nothing at all. */ emptyNote?: string; } export function ModulePermsList({ modules, entries, onAccess, onFilter, onToggleHidden, onSetHidden, onMetrics, onEdit, userOptions, headExtra, emptyNote, }: ModulePermsListProps) { // ⭐ W40-T14 — WHICH MODE, read ONCE. Every branch below asks this same question, and a second // spelling of it (`onEdit !== undefined` here, `!!onEdit` there) is how two of them come to // disagree about which room they are in. const drilled = onEdit !== undefined; // ⭐ 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; // ⭐⭐ WAVE 36 (W36-T22 / CONTRACT C2 / OWNER RULING R6) — THE `enforced` // BRANCH AND ITS "Not set here" CARD ARE DELETED. Owner item 11, verbatim: // *"how come the database is only toggleable for Odoo customers and Odoo // products. EVERY database should be able to be toggleable by admin. I'm // only seeing 'Not set here'."* // // That card was HONEST when it shipped — `perm_scope` was never consulted // on a `ut_*` read, so an access toggle there would have been a control // that lies. W36-T21 armed the wall over every database, so the branch's // premise is now false and rendering it would be the lie it was written to // prevent, facing the other way. Every row below is a row whose rule the // table routes apply. const canDetail = on && !schemaless; 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. */} {/* ⭐ W40-T14 — the summary, the accordion and `headExtra` are the ACCORDION room's furniture. The drill-in row is "a checkbox and one way in", so it renders the Edit button in their place and nothing else. */} {drilled ? null : ( {moduleSummary(entry, schemaless)} )} {canDetail && drilled ? ( // ⛔ `canDetail` (= `on && !schemaless`), NOT unconditional: a database nobody // may open has no rule to edit, and a schemaless surface has no fields and no // filter — the help paragraph below already says so for that second case. ) : null} {canDetail && !drilled ? ( ) : null} {drilled ? null : headExtra?.(m) ?? null}
{/* ⭐⭐ W38-T19 — THE METRICS CAPABILITY, one box per database. ⛔ GATED ON `on && !schemaless` FOR TWO DIFFERENT REASONS, not one. `on`: a capability inside a database nobody may open is a control that decides nothing, and the row already says "No access". `!schemaless`: an app SURFACE has no grid and therefore no measure door, so a box there would store a rule nothing reads — R9's rule that this editor never writes a restriction it could not honestly show, applied to a capability instead of to a filter. ⚠ It sits OUTSIDE `.set-perm-headend` deliberately. That rule is `space-between` and lives in `index.css`, which this ticket's fence does not contain; a new flex child there would need CSS that cannot be written, so the box takes its own line under the head. */} {/* ⭐ W40-T14 — `!drilled &&` is the whole change here. Owner instruction 7 moved this box INTO the database's own pane ("put the Metrics field toggle under there"), so the drill-in row must not draw a second one. ⭐⭐ W40-T16 — AND AFTER THIS TICKET THE BOX BELOW RENDERS NOWHERE AT ALL. Owner instruction 13 folded the blanket toggle into the HIDE-FIELDS list, one checkbox per metric, and `DatabasePermsPane`'s copy is deleted; `PermsEditor` passes `onEdit` for every account row, so `!drilled` suppresses this one. ⛔ AND THE OTHER CALLER DOES NOT MOUNT IT EITHER — MEASURED, not assumed: `manage-agent/ManageAgentPane.tsx` passes exactly `modules, entries, onAccess, onFilter, onToggleHidden, onSetHidden, userOptions, emptyNote`, and no `onMetrics`. So nothing in the UI writes `PermsEntry.metrics` any more. ⚠ THAT IS INSTRUCTION 13, NOT A DEFECT: a blanket control REPLACED by finer ones is the whole ask. The boolean survives as a legacy value — read by `parseEntry`, emitted by `toPutBody`, honoured on screen by `hiddenSetFor` — and every record written from here on carries it GRANTED. ⛔ THE JSX IS KEPT AS A SEAM, DELIBERATELY AND WITH ITS COST NAMED. It is what lets a future room govern the capability without re-deriving the prop, and it carries `verify_ui.py`'s `t19-unmounted` / `t19-control-gone` controls, the only two in that file built from code that actually shipped broken. Deleting it is legal — nothing passes the prop, so `tsc` would not notice — but it is a decision to take out loud, in the same change as those two gate blocks, never a tidy-up. */} {!drilled && on && !schemaless && onMetrics ? ( ) : 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 ? (

{m.surface ? "Access is the whole rule for this module. It has no fields to hide " + "and no records to filter." : "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} {/* ⚠ `!drilled` is written even though `openDetail` can never leave `null` in that mode (nothing renders the disclosure that sets it). An invariant held by an absent button is one refactor away from being false; the gate on the thing itself is not. */} {!drilled && canDetail && shown ? (
onFilter(m.key, next)} userOptions={userOptions} />
{/* ⭐ W40-T16 — `hiddenSetFor`, the same resolver the manage-user pane uses. ONE spelling of "which keys read as hidden": this room's fields carry no metric flag today (the agent editor's payload has no `measure_` pseudo- fields), so the union adds nothing here and the accordion is unchanged. It is written anyway because the alternative is two answers to one question in two files, which is the drift this component was extracted to prevent. */}
) : null}
); })} ); }