| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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 { |
| |
| module: PermsModule; |
| |
| |
| |
| |
| |
| |
| entries: PermsRecord; |
| |
| onBack: () => void; |
| onFilter: (next: FilterTree | null) => void; |
| onToggleHidden: (fieldKey: string) => void; |
| onSetHidden: (keys: string[]) => void; |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| userOptions?: string[]; |
| } |
|
|
| export function DatabasePermsPane({ |
| module, |
| entries, |
| onBack, |
| onFilter, |
| onToggleHidden, |
| onSetHidden, |
| userOptions, |
| }: DatabasePermsPaneProps) { |
| return ( |
| <div className="set-dbperm"> |
| <div className="set-dbperm-head"> |
| {/* β 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. */} |
| <button type="button" className="set-secondary set-dbperm-back" onClick={onBack}> |
| <svg width="14" height="14" viewBox="0 0 16 16" fill="none" aria-hidden focusable="false"> |
| <path |
| d="M9.5 4 5.5 8l4 4" |
| stroke="currentColor" |
| strokeWidth={1.3} |
| strokeLinecap="round" |
| strokeLinejoin="round" |
| /> |
| </svg> |
| Back |
| </button> |
| {/* 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. */} |
| <h4 className="set-h4 set-dbperm-title">{module.label}</h4> |
| </div> |
| |
| {/* 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. */} |
| <div className="set-dbperm-panels"> |
| <div className="cg-pop set-perm-pop"> |
| <FilterBuilderPanel |
| fields={module.fields} |
| filters={filterOf(entries, module.key)} |
| onChange={onFilter} |
| userOptions={userOptions} |
| /> |
| </div> |
| <div className="cg-pop set-perm-pop"> |
| {/* ββ 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. */} |
| <FieldsHidePanel |
| fields={module.fields} |
| hidden={hiddenSetFor(entries, module.key, module.fields)} |
| onToggle={onToggleHidden} |
| /* ββ W40-T17 (OWNER INSTRUCTION 15) β THE ONE PROP THAT MAKES THIS MOUNT DIFFER |
| FROM THE GRID'S. Owner, verbatim: *"stop displaying 'Shared with me' / 'Shared |
| with everyone' fields under Hide Fields - permission on pre-set Fields only."* |
| `false` drops both sections AND swaps the membership rule for AM-2's |
| `metric || !custom`, so this list is the database's own columns plus its bound |
| measures and nothing else. |
| β IT IS PASSED HERE AND NOWHERE ELSE. `customer-grid/Toolbar.tsx` mounts the same |
| panel and passes no `sharedSections`, so the grid keeps all three sections exactly |
| as the 2026-08-21 hotfix left them. That is the whole reason this is a prop rather |
| than a deletion: the two surfaces are supposed to differ now. */ |
| sharedSections={false} |
| // ββ BOTH HALVES OF THE TRAP, and neither is decoration. `lockedKey` disables the |
| // identity column's own checkbox; `hideableKeys` is what keeps "Hide all" from |
| // including it anyway. One without the other still ships blank rows. |
| lockedKey={identityKey(module.fields)} |
| /* ββ W40-T17 β THE BULK ACTIONS REACH EXACTLY WHAT THIS ROOM LISTS, and the |
| narrowing above is what owes them the change: a list showing four rows whose |
| "Show all" clears hidden state for seven fields would WIDEN a permission through |
| a control that never displayed the columns it just revealed. `showAllKeys` |
| subtracts the governed keys instead of emptying the record; `hideAllKeys` adds |
| them without disturbing what the record hides elsewhere. Both strip the identity |
| key, so the trap above stays closed from this side too. */ |
| onHideAll={() => onSetHidden(hideAllKeys(entries, module.key, module.fields))} |
| onShowAll={() => onSetHidden(showAllKeys(entries, module.key, module.fields))} |
| /> |
| </div> |
| </div> |
| </div> |
| ); |
| } |
|
|