// --------------------------------------------------------------------------- // settings / DatabasePermsPane.tsx — ONE DATABASE'S RULE, given the whole pane. // (wave 40, owner instruction 7 · W40-T14.) // // ⭐ WHY IT EXISTS, IN THE OWNER'S OWN WORDS: *"Manage user, Access section: show // only the Database, with just a checkbox and an Edit button (put the Metrics // field toggle under there). Edit opens the whole database's Fields and Filters // to toggle, with a Back button to return."* // // ⚠ THE PARENTHESIS IN THAT QUOTE IS SUPERSEDED, AND ONLY THE PARENTHESIS. Owner instruction 13 // (W40-T16) folded the Metrics toggle one level further in: it is no longer a box "under there" // but one CHECKBOX PER METRIC inside this pane's Hide-fields list. The room, the Back button and // the Fields/Filters pairing are instruction 7's and are unchanged. Read the quote as the reason // this file exists, never as a description of the controls it draws today. // // What it replaces is a single-row INLINE ACCORDION. That shape was right when // the list was two databases and the panels were the only thing under them; it // is wrong now, because a filter builder and a field list opened inside a row of // a list of every database the tenant has are two menus that failed to close. // A rule about one database gets a room, and the room says which database. // // ⛔ IT OWNS NO STATE AND WRITES NOTHING. Every value comes from the caller's // DRAFT and every change goes back out as a callback, exactly as // `ModulePermsList` does — `PermsEditor` holds the draft, the confirm and the // PUT. This component is a layout and a set of handlers, so the account page // keeps one source of truth for what is about to be saved. // // ⛔⛔ THE `lockedKey` / `hideableKeys` PAIR BELOW IS LOAD-BEARING AND ITS // FAILURE IS SILENT. Drop either and one click on "Hide all" hides the database's // own identity column: a record whose faithful enforcement is a table of blank // rows, which the server's PUT validation ACCEPTS because the identity column is // a perfectly known field key. Both halves ship together or the pane is a defect. // --------------------------------------------------------------------------- import { FilterBuilderPanel, FieldsHidePanel } from "../filter-kit"; import type { FilterTree } from "../customer-grid/types"; import type { PermsModule, PermsRecord } from "./permsModel"; import { filterOf, hiddenSetFor, hideAllKeys, identityKey, showAllKeys } from "./permsModel"; import "./perms.css"; export interface DatabasePermsPaneProps { /** The database being edited: its label titles the pane, its fields feed both panels. */ module: PermsModule; /** * ⚠ THE WHOLE DRAFT, keyed by module, NOT this module's entry. `filterOf` and `hiddenSetFor` * are where "an absent filter is an empty tree" and "absent hidden fields is an empty set" are * spelled, once; taking a bare entry would make this file spell them a second time, and two * spellings of one default are what those helpers exist to prevent. */ entries: PermsRecord; /** Return to the account page. The SAME function the account page's Escape handler calls. */ onBack: () => void; onFilter: (next: FilterTree | null) => void; onToggleHidden: (fieldKey: string) => void; onSetHidden: (keys: string[]) => void; /* * ⭐⭐ W40-T16 (OWNER INSTRUCTION 13) — `onMetrics` IS GONE FROM THIS PANE, AND SO IS THE BOX * IT DREW. Owner, verbatim: *"Fold 'Metrics fields (lookback measures over this database)' into * the 'Hide fields' checkboxes, one row per metric, named 'Metric - Revenue', 'Metric - Order' * and so on, so a user can check the ones the permissioning is limited to."* * * ⛔ A DELETION RATHER THAN A HIDE, because the control it replaces answered a COARSER * question. One box said "this account may build measures over this database, or may not"; * the Hide-fields list below now carries one checkbox per bound measure, so the same admin * decides WHICH ones. Leaving the blanket box beside them would put two controls on one screen * that can contradict each other, and the record has no way to express the contradiction. * * ⚠ THE CAPABILITY ITSELF IS UNTOUCHED. `PermsEntry.metrics` still rides the wire, still parses * (`parseEntry`) and is still emitted (`toPutBody`), because a record written before this * ticket may carry an explicit revocation. What honours it here is `hiddenSetFor` below, on the * READ side: a blanket `metrics: false` paints every metric row as hidden without rewriting * anybody's stored rule. See the note at that call. */ /** People this tenant can name in a `user` condition. Absent ⇒ the panel says so. */ userOptions?: string[]; } export function DatabasePermsPane({ module, entries, onBack, onFilter, onToggleHidden, onSetHidden, userOptions, }: DatabasePermsPaneProps) { return (
{/* ⛔ ONE WORD, AND THE SHORTNESS IS FORCED RATHER THAN CHOSEN. The account's own exit, "Back to accounts", is rendered by `PermsEditor` DIRECTLY ABOVE this pane and stays there while the room is open. Spelling this one "Back to account" would put two controls one letter apart on one screen, going to different places, which is a worse room than either label is a good one. Position already says where this one goes: it sits on the database title's line, so it leaves the database. `DESIGN.md` §4 asks for the shorter label wherever the longer one is narration, and here it also happens to be the only unambiguous one. */} {/* The pane says which database it is about. Without it every one of these rooms looks identical, and the reader's only clue is what they clicked a scroll ago. */}

{module.label}

{/* THE KIT'S OWN PANELS (contract C-KIT), not lookalikes: the same two the grid toolbar mounts, so an admin writes a condition with the control they already know and there is one condition grammar in the product rather than two that drift within a wave. They STACK here rather than sitting side by side: the pane is theirs alone now, and a filter and a field list read as two decisions taken in order, not one wide row. */}
{/* ⭐⭐ W40-T16 — THIS LIST IS WHERE THE METRICS RULE NOW LIVES. `module.fields` carries C3's `measure_`-namespaced pseudo-fields, `permsModel::parseField` marks each one, and the panel draws it as an ordinary checkbox named "Metric - Revenue". Checking it puts that field's key into `hiddenFields` through the same `onToggle` every other row uses, so there is no second write path for a metric and no second record shape. ⛔ `hiddenSetFor`, NOT `hiddenSet`, AND THE DIFFERENCE IS THE MIGRATION. A record written before this ticket can carry a blanket `metrics: false` (W38-T19's box), and the control that wrote it is gone from this pane. The union paints every metric row as hidden for such a record, so the old revocation is HONOURED on screen rather than silently reading as a grant. It is a read-side union on purpose: folding it into `hiddenFields` would edit somebody's stored rule as a side effect of opening a page. */}
); }