| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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 { |
| |
| modules: PermsModule[]; |
| |
| 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; |
| |
| userOptions?: string[]; |
| |
| |
| headExtra?: (m: PermsModule) => ReactNode; |
| |
| emptyNote?: string; |
| } |
|
|
| export function ModulePermsList({ |
| modules, |
| entries, |
| onAccess, |
| onFilter, |
| onToggleHidden, |
| onSetHidden, |
| userOptions, |
| headExtra, |
| emptyNote, |
| }: ModulePermsListProps) { |
| |
| |
| |
| |
| const [openDetail, setOpenDetail] = useState<string | null>(null); |
|
|
| if (modules.length === 0) { |
| return ( |
| <p className="pg-empty"> |
| {emptyNote ?? "This workspace has no databases whose access can be restricted."} |
| </p> |
| ); |
| } |
|
|
| 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 ( |
| <section className="set-card set-perm-mod" key={m.key}> |
| <div className="set-perm-modhead"> |
| <label className="set-check set-perm-toggle"> |
| <input |
| type="checkbox" |
| checked={on} |
| onChange={(e) => onAccess(m.key, e.target.checked)} |
| /> |
| <span className="set-perm-modname">{m.label}</span> |
| </label> |
| {/* β 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. */} |
| <span className="set-perm-headend"> |
| <span className="set-perm-sum">{moduleSummary(entry, schemaless)}</span> |
| {canDetail ? ( |
| <button |
| type="button" |
| className="set-secondary set-perm-disclose" |
| aria-expanded={shown} |
| onClick={() => setOpenDetail((k) => (k === m.key ? null : m.key))} |
| > |
| {shown ? "Hide detail" : "Conditions and fields"} |
| </button> |
| ) : null} |
| {headExtra?.(m) ?? null} |
| </span> |
| </div> |
| |
| {/* 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 ? ( |
| <p className="set-help"> |
| 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. |
| </p> |
| ) : null} |
| |
| {canDetail && shown ? ( |
| <div className="set-perm-panels"> |
| <div className="cg-pop set-perm-pop"> |
| <FilterBuilderPanel |
| fields={m.fields} |
| filters={filterOf(entries, m.key)} |
| onChange={(next) => onFilter(m.key, next)} |
| userOptions={userOptions} |
| /> |
| </div> |
| <div className="cg-pop set-perm-pop"> |
| <FieldsHidePanel |
| fields={m.fields} |
| hidden={hiddenSet(entries, m.key)} |
| onToggle={(key) => onToggleHidden(m.key, key)} |
| // β `lockedKey` IS NOT OPTIONAL HERE, whatever the prop says. |
| // Absent, the panel locks nothing and one click on "Hide all" |
| // hides the row's own name too β a record whose faithful |
| // enforcement is a table of blank rows, which the server's PUT |
| // validation would accept because the identity column is a |
| // perfectly KNOWN field key. |
| lockedKey={identityKey(m.fields)} |
| onHideAll={() => onSetHidden(m.key, hideableKeys(m.fields))} |
| onShowAll={() => onSetHidden(m.key, [])} |
| /> |
| </div> |
| </div> |
| ) : null} |
| </section> |
| ); |
| })} |
| </> |
| ); |
| } |
|
|