File size: 17,456 Bytes
f546440 cf17b22 f546440 1f123e1 f546440 15e3e59 1f123e1 f546440 1f123e1 f546440 15e3e59 1f123e1 f546440 1f123e1 f546440 cf17b22 f546440 1f123e1 f546440 1f123e1 f546440 15e3e59 1f123e1 15e3e59 f546440 aef86ea f546440 1f123e1 f546440 1f123e1 f546440 1f123e1 f546440 1f123e1 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 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 | // ---------------------------------------------------------------------------
// 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<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">
{/* β 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 : (
<span className="set-perm-sum">{moduleSummary(entry, schemaless)}</span>
)}
{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.
<button
type="button"
className="set-secondary set-perm-edit"
onClick={() => onEdit(m.key)}
>
Edit
</button>
) : null}
{canDetail && !drilled ? (
<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}
{drilled ? null : headExtra?.(m) ?? null}
</span>
</div>
{/* ββ 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 ? (
<label className="set-check">
<input
type="checkbox"
checked={entry?.metrics !== false}
onChange={(e) => onMetrics(m.key, e.target.checked)}
/>
<span>Metric fields (lookback measures over this database)</span>
</label>
) : 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 ? (
<p className="set-help">
{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."}
</p>
) : 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 ? (
<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">
{/* β 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. */}
<FieldsHidePanel
fields={m.fields}
hidden={hiddenSetFor(entries, m.key, m.fields)}
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)}
/* ββ W40-T17 (owner instruction 15) β UNCONDITIONAL HERE, NOT A SECOND
PROP, AND THAT IS WHAT THIS COMPONENT IS. `ModulePermsList` IS the
permissioning list: every mount of it β the account editor's Access
section and `manage-agent/ManageAgentPane` alike β is editing the same
`perms` record through the same wall, so its panel is always a
permissioning panel and there is no caller for whom the shared sections
would be right. Owner: *"stop displaying 'Shared with me' / 'Shared with
everyone' fields under Hide Fields - permission on pre-set Fields only."*
That is a sentence about permissioning, not about one screen.
β IT IS ALSO WHAT KEEPS THE TWO ROOMS IDENTICAL. This file exists because
the owner ruled manage-agent is *"the same exact layout"* as manage user
(wave 33); gating one room and not the other would put the divergence
inside the very component extracted to prevent it β and LATENTLY, since
neither section fills up until the server ships `custom`/`shared`. */
sharedSections={false}
onHideAll={() => onSetHidden(m.key, hideAllKeys(entries, m.key, m.fields))}
onShowAll={() => onSetHidden(m.key, showAllKeys(entries, m.key, m.fields))}
/>
</div>
</div>
) : null}
</section>
);
})}
</>
);
}
|