File size: 8,190 Bytes
f546440 cf17b22 f546440 cf17b22 f546440 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 | // ---------------------------------------------------------------------------
// 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, 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<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>
);
})}
</>
);
}
|