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>
        );
      })}
    </>
  );
}