@micha.bigler/ui-core-micha 3.12.0 → 3.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +6 -0
- package/WORK_ORDERS.md +2 -0
- package/dist/layout/SectionNav.js +3 -3
- package/dist/pages/AccountPage.js +2 -2
- package/docs/TYPE-SCALE.md +12 -0
- package/package.json +1 -1
- package/src/layout/SectionNav.jsx +22 -18
- package/src/pages/AccountPage.jsx +2 -0
- package/tests/AccountPage.profileStatements.test.jsx +162 -0
- package/tests/SectionNav.test.jsx +40 -0
- package/work-orders/UCM-AUTH-12.md +98 -0
- package/work-orders/UCM-SHELL-8.md +113 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.13.1 - UCM-SHELL-8
|
|
4
|
+
|
|
5
|
+
`SectionNav` now renders grouped and overview navigation buttons inside list items on
|
|
6
|
+
desktop and in the mobile drawer. Selection, callbacks, labels and spacing are preserved.
|
|
7
|
+
|
|
8
|
+
## 3.13.0 - UCM-AUTH-12
|
|
9
|
+
|
|
10
|
+
`AccountPage` accepts `showProfileStatements` (default `true`). Set it to `false` in consumers
|
|
11
|
+
without privacy/cookie statement endpoints to suppress the statement panels and their requests.
|
|
12
|
+
Both existing consent controls and their submitted values remain available.
|
|
13
|
+
|
|
3
14
|
## 3.12.0 - UCM-THEME-18
|
|
4
15
|
|
|
5
16
|
The shared type scale now uses h1–h5 sizes of 36/32/28/24/20px, h4 steps to 23px below `sm`, and subtitle1 is 13px; every adopted app changes on its next bump, with hram, jg-ferien, kerzenziehen, spesix, and fitness-monitor moving screen titles from h5 to h4, while survey_app should remove its 800/700 weights and h4 `2.125rem`/400 override and decide its h3 page titles (24px to 28px), closing the held `SVA-THEME-1` typography revert. An app h4 `fontSize` override retains the baseline’s 23px mobile step (survey_app currently sets `2.125rem`); heading weights are fixed at 600, so apps should not override them per level. All other adopted apps need only bump; webshop-guenter, Kira, Cinevia, and Gustav are unaffected.
|
package/README.md
CHANGED
|
@@ -10,6 +10,12 @@ To add an entry, create a small component in `dev/entries.jsx` and append it to
|
|
|
10
10
|
|
|
11
11
|
Spread `uiCoreTranslations` into the default i18n namespace as the supported aggregate; per-feature translation exports remain available to selective apps that accept responsibility for pairing every adopted feature with its bundle.
|
|
12
12
|
|
|
13
|
+
## Account profile statements
|
|
14
|
+
|
|
15
|
+
`AccountPage` defaults `showProfileStatements` to `true`. Pass `false` when the consumer does not
|
|
16
|
+
offer privacy/cookie statement text endpoints; this hides their panels and prevents their requests.
|
|
17
|
+
The privacy and cookie consent controls and their submitted values remain available.
|
|
18
|
+
|
|
13
19
|
## Consumer integration check
|
|
14
20
|
|
|
15
21
|
```js
|
package/WORK_ORDERS.md
CHANGED
|
@@ -28,6 +28,8 @@ prefixed ID; never reuse a bare flat number across workstreams.
|
|
|
28
28
|
|
|
29
29
|
| ID | Titel | Beschreibung | Datum | Status | Commit(s) | Notiz |
|
|
30
30
|
|---|---|---|---|---|---|---|
|
|
31
|
+
| UCM-AUTH-12 | [Optional account profile statement text](work-orders/UCM-AUTH-12.md) | AccountPage showProfileStatements defaults true; false suppresses statement panels/requests while preserving both consent controls and their payload. Minor release 3.13.0, consumed by innoservice INNO-FIX-13. | 2026-10-13 | done | 97b7d83 | Tier 3 (shared-core account surface). Gate #1 explicitly approved in the innoservice Codex chat on 2026-10-13, including release and app pin bump. Separate configured Luna implementation and independent reviews; existing default and consent behavior are invariants. review: codex/gpt-6-luna · lenses: envelope, regression, duplication, tests, ui, security · 0 raised · 0 accepted · worst accepted: none. Parent scoped tests: AccountPage.profileStatements.test.jsx + AccountPage.test.jsx, 6 passed, correct kit cwd, maxWorkers2. Native author attempt EPERM before collection; parent gate green. Reviewed additive API/default and package release metadata; main push triggers the existing release build/suite/publish. Released 3.13.0: CI38422682968 build, release suite and npm Trusted Publishing all succeeded. App consumption/capture remains tracked under INNO-FIX-13. |
|
|
32
|
+
| UCM-SHELL-8 | SectionNav lists contain direct button children | Grouped and overview entries need genuine list items containing the existing navigation buttons, preserving selection, drawer behavior and presentation. | 2026-10-13 | in-review | `d399f51` | Tier 3. Gate #1 approved by the operator on 2026-10-13: structural correction, tests/review, patch release and innoservice pin bump. Full scope and context: [work-orders/UCM-SHELL-8.md](work-orders/UCM-SHELL-8.md). Original CI38387014900/source9937adb evidence remains in innoservice/work-orders/assets/INNO-FIX-11-axe-nodes.json; CI38425506007/sourceebc35e1 reproduces the three account list violations on 3.13.0, recorded in INNO-FIX-11-candidate-b-axe-nodes.json. Initial native author /root/kit_sectionnav requested codex/gpt-6-luna; effective native metadata unexposed. Source and two focused test cases delivered; author run EPERM before collection, zero tests. Parent tests and independent review pending. review: codex/gpt-6-luna · lenses: envelope, regression, duplication, tests + UI · 1 raised · 0 accepted · worst accepted: none. Disposition in WO Part C: fabricated class-stub identity demand rejected against the real revert failure and preserved existing behavior checks. Parent 21 scoped tests passed (maxWorkers=2), TypeScript build exit 0. Core source accepted; publication, app pin and fresh consumer capture pending. |
|
|
31
33
|
| UCM-THEME-17 | Decide a less compressed baseline type scale | Design round (design-language process): inventory, instrument with 2-3 candidate scales next to today's baseline and MUI on real screens (hram, a survey site, jg-ferien, kerzenziehen), value rounds, frozen sheet. Measured 2026-10-04: page/card/section titles differ by 1-3 px, hierarchy rests on weight alone. Full order [`work-orders/UCM-THEME-17.md`](work-orders/UCM-THEME-17.md) | 2026-10-04 | done | `68dfc6a`, `998e36c`, `cb9dd89`, `c452224` | Tier 2 (document only) · operator decision 2026-10-04: baseline finding, not per-app overrides · implementation order follows the freeze · survey_app SVA-THEME-1 typography revert held until then · **frozen 2026-10-04** after two value rounds: candidate B, mobile h4 23, h1-h3 36/32/28; value source [`docs/TYPE-SCALE.md`](docs/TYPE-SCALE.md) · review: none, document order, the operator freeze is the gate · implementation UCM-THEME-18 |
|
|
32
34
|
| UCM-THEME-18 | Ship the frozen baseline type scale | `tokens.js` typography per `docs/TYPE-SCALE.md`: h1-h3 36/32/28, h4 24 (23 below `sm`), h5 20, subtitle1 13; light fixture typography entries; minor release 3.12.0. Full order [`work-orders/UCM-THEME-18.md`](work-orders/UCM-THEME-18.md) | 2026-10-04 | 5be83d5 | | Tier 3 (shared-core) · reviewer + ui_reviewer · downstream per-app orders (h5 page titles -> h4, per-level weight overrides) written after the release - 2026-10-04: Codex implemented (gpt-6-luna; probe of stale 2026-10-03 record succeeded, available) - orchestrator fixed review findings - review: codex/gpt-6-luna - lenses: envelope,regression,duplication,tests + ui_review - 3 raised - 2 accepted - worst accepted: defect (h4 mobile key now follows an app-overridden sm, test added; completeness test strengthened; rejected: breakpointDownQuery vs theme.breakpoints.down, no theme exists at token time) - light fixture diff limited to typography entries - no rendered side-by-side (no prototype; values asserted from the resolved theme in light and dark) - release 3.12.0 |
|
|
33
35
|
| UCM-I18N-5 | [User-list pagination range text is hard-coded English](work-orders/UCM-I18N-5.md) | `UserListComponent`'s `TablePagination` sets `labelRowsPerPage` but no `labelDisplayedRows`, so German shows "1–4 of 4". | 2026-10-01 | done | 6b9d557 | Found in jg-ferien's rendered check 2026-10-01. Follows `UCM-I18N-4`. Tier 3. Done = released AND jg-ferien pin bumped. Envelope written 2026-10-01; operator: hand to the jg Paket-4 session. Implementation map filled 2026-10-02 (`a732025`): the translation bundle carries FOUR locales (de/fr/en/sw), not three — confirmed by reading `authTranslations.ts` directly; proposed two keys (exact-count + "more than" form) with a best-effort, not native-verified, Swahili wording. Implemented by codex/gpt-5.6-luna exactly per the map: `labelDisplayedRows` resolves `UserList.DISPLAYED_ROWS`/`UserList.DISPLAYED_ROWS_MORE_THAN` through `t()`, both keys added in all four locales. review: codex/gpt-5.6-luna · lenses: envelope,regression,duplication,tests,ui_reviewer (one concurrent batch, diff inline) · 4 raised · 3 accepted (deduplicated) · worst accepted: defect. Accepted: the rendered English test would still pass against a reverted implementation, since the EN wording was deliberately made identical to MUI's own hardcoded default — added a second test swapping in a distinctive marker string for just that key, proving the render genuinely resolves through i18next, not MUI's fallback; `authTranslations.test.js`'s new key-parity check only asserted non-empty strings, not that each locale actually carries the `{{from}}`/`{{to}}`/`{{count}}` placeholders the others do — strengthened with an explicit per-locale placeholder check; self-identified (corroborated by the `tests` lens's own closing remark) that the `count === -1` ("more than") branch can never be reached through a render of `UserListComponent` itself (its own `count` prop is always a real array length) — extracted the inline callback into an exported, directly unit-tested `formatDisplayedRowsLabel(t)` rather than leaving it permanently untested dead code. Rejected: `regression`'s claim that hard-coding `labelDisplayedRows` "overrides any consuming app's MUI theme customisation" — speculative, no actual consumer override exists (same explicit-prop design as `labelRowsPerPage` since `UCM-I18N-4`), and the reviewer's own repo search found zero consumer snapshots/assertions relying on the old English text. `envelope`'s "release + pin bump missing from the diff" was correct but not a code defect — addressed by the remaining steps below, not a fix to the reviewed diff. `duplication`: 0 findings. `ui_reviewer`: 0 blocking findings; confirmed placeholder consistency across all 8 strings; Swahili wording flagged as advisory-only ("structurally plausible to a non-native reviewer, native-speaker verification advisable" — not re-asked of a native speaker here, recorded as open). Tests: 37/37 green (`userScreenTranslations.test.jsx`, `authTranslations.test.js`, `UserListComponent.test.jsx`). Released as `3.11.1` (patch — continues `UCM-I18N-4`'s i18n-gap category, not a new capability area), commit `6b9d557`; npm Trusted Publishing confirmed via the Actions job log (not a registry re-query — provenance publishes can lag), tarball `@micha.bigler/ui-core-micha@3.11.1`. jg-ferien's pin bumped 3.7.0 -> 3.11.1 same day, closed as jg's own `JG-DEPS-3` (`2c1d6e3`) — see that repo's `WORK_ORDERS.md` for the empirical safety checks (status-colour palette diff, `UserListComponent`-unused confirmation, full-suite run) this multi-version jump required. |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useId, useState } from 'react';
|
|
3
3
|
import KeyboardArrowDownIcon from '@mui/icons-material/KeyboardArrowDown';
|
|
4
|
-
import { Box, ButtonBase, Drawer, List, ListItemButton, ListItemText, Paper, Stack, Typography, } from '@mui/material';
|
|
4
|
+
import { Box, ButtonBase, Drawer, List, ListItem, ListItemButton, ListItemText, Paper, Stack, Typography, } from '@mui/material';
|
|
5
5
|
import { useTheme } from '@mui/material/styles';
|
|
6
6
|
import { useTranslation } from 'react-i18next';
|
|
7
7
|
const defaultZIndex = (theme) => theme.zIndex.drawer + 3;
|
|
@@ -82,11 +82,11 @@ const SECTION_NAV_DRAWER_SCROLL_SX = {
|
|
|
82
82
|
};
|
|
83
83
|
function SectionNavList({ groups, activeKey, rememberedKey, overviewItem, onSelect, rememberedLabel, }) {
|
|
84
84
|
const headingId = useId();
|
|
85
|
-
return (_jsxs(Stack, { spacing: 2, children: [overviewItem && (_jsx(Paper, { variant: "outlined", sx: SECTION_NAV_GROUP_PAPER_SX, children: _jsx(List, { disablePadding: true, children: _jsx(ListItemButton, { selected: activeKey == null, "aria-current": activeKey == null ? 'page' : undefined, onClick: () => onSelect === null || onSelect === void 0 ? void 0 : onSelect(null), children: _jsx(ListItemText, { primary: overviewItem.label }) }) }) })), groups.map((group, groupIndex) => {
|
|
85
|
+
return (_jsxs(Stack, { spacing: 2, children: [overviewItem && (_jsx(Paper, { variant: "outlined", sx: SECTION_NAV_GROUP_PAPER_SX, children: _jsx(List, { disablePadding: true, children: _jsx(ListItem, { disablePadding: true, children: _jsx(ListItemButton, { selected: activeKey == null, "aria-current": activeKey == null ? 'page' : undefined, onClick: () => onSelect === null || onSelect === void 0 ? void 0 : onSelect(null), children: _jsx(ListItemText, { primary: overviewItem.label }) }) }) }) })), groups.map((group, groupIndex) => {
|
|
86
86
|
const groupHeadingId = `${headingId}-${groupIndex}`;
|
|
87
87
|
return (_jsxs(Paper, { variant: "outlined", sx: SECTION_NAV_GROUP_PAPER_SX, children: [_jsx(Box, { id: groupHeadingId, sx: SECTION_NAV_GROUP_HEADER_SX, children: _jsx(Typography, { variant: "subtitle2", fontWeight: 700, children: group.label }) }), _jsx(List, { disablePadding: true, "aria-labelledby": groupHeadingId, children: group.items.map((item) => {
|
|
88
88
|
const selected = item.key === activeKey;
|
|
89
|
-
return (_jsx(ListItemButton, { selected: selected, "aria-current": selected ? 'page' : undefined, onClick: () => onSelect === null || onSelect === void 0 ? void 0 : onSelect(item.key), children: _jsx(ListItemText, { primary: item.label, secondary: !selected && item.key === rememberedKey ? rememberedLabel : null }) }, item.key));
|
|
89
|
+
return (_jsx(ListItem, { disablePadding: true, children: _jsx(ListItemButton, { selected: selected, "aria-current": selected ? 'page' : undefined, onClick: () => onSelect === null || onSelect === void 0 ? void 0 : onSelect(item.key), children: _jsx(ListItemText, { primary: item.label, secondary: !selected && item.key === rememberedKey ? rememberedLabel : null }) }) }, item.key));
|
|
90
90
|
}) })] }, group.key));
|
|
91
91
|
})] }));
|
|
92
92
|
}
|
|
@@ -26,7 +26,7 @@ import { QrSignupValidityManager } from '../components/QrSignupValidityManager';
|
|
|
26
26
|
import { SupportRecoveryRequestsTab } from '../components/SupportRecoveryRequestsTab';
|
|
27
27
|
import { BulkInviteCsvTab } from '../components/BulkInviteCsvTab';
|
|
28
28
|
import { fetchAuthPolicy, updateUserProfile } from '../auth/authApi'; // Ggf. Pfad anpassen
|
|
29
|
-
export function AccountPage({ userListExtraColumns = [], userListExtraRowActions = [], userListExtraContext = null, userListRefreshTrigger = 0, userListCanEditUser = null, userListShowNewColumn = true, userListShowSuccessfulLoginColumn = true, userListShowRoleColumn = true, userListOnChangeRole = null, userListShowDeleteAction = true, userListCanDeleteUser = null, userListOnDeleteUser = null, showBulkInviteCsvTab = false, bulkInviteCsvProps = {}, extraTabs = [], }) {
|
|
29
|
+
export function AccountPage({ userListExtraColumns = [], userListExtraRowActions = [], userListExtraContext = null, userListRefreshTrigger = 0, userListCanEditUser = null, userListShowNewColumn = true, userListShowSuccessfulLoginColumn = true, userListShowRoleColumn = true, userListOnChangeRole = null, userListShowDeleteAction = true, userListCanDeleteUser = null, userListOnDeleteUser = null, showBulkInviteCsvTab = false, bulkInviteCsvProps = {}, extraTabs = [], showProfileStatements = true, }) {
|
|
30
30
|
var _a;
|
|
31
31
|
const { t, i18n } = useTranslation();
|
|
32
32
|
const { user, login, loading } = useContext(AuthContext);
|
|
@@ -174,5 +174,5 @@ export function AccountPage({ userListExtraColumns = [], userListExtraRowActions
|
|
|
174
174
|
const activeExtraTab = builtInTabValues.has(safeTab)
|
|
175
175
|
? null
|
|
176
176
|
: extraTabs.find((tab) => tab.value === safeTab);
|
|
177
|
-
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsxs(SectionNav, { mode: isMobile ? 'mobile' : 'desktop', groups: groups, activeKey: safeTab, onSelect: (key) => handleTabChange(null, key), children: [safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser, showNewColumn: userListShowNewColumn, showSuccessfulLoginColumn: userListShowSuccessfulLoginColumn, showRoleColumn: userListShowRoleColumn, onChangeRole: userListOnChangeRole, showDeleteAction: userListShowDeleteAction, canDeleteUser: userListCanDeleteUser, onDeleteUser: userListOnDeleteUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AuthFactorRequirementCard, { canEdit: canWriteAuthPolicy, policy: authPolicy }) })), canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(RegistrationMethodsManager, { policy: authPolicy, error: authPolicyError, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canSendInvites && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(UserInviteComponent, {}) })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsxs(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.ACCESS_CODE_MANAGER_TITLE', 'Access Codes') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AccessCodeSingleUseToggle, { canEdit: canWriteAuthPolicy, policy: authPolicy, onPolicyChange: setAuthPolicy }) })), canSendInvites && showBulkInviteCsvTab && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AllowedEmailDomainsManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain), domains: (authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allowed_email_domains) || [], onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(QrSignupValidityManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canManageSignupQr && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(QrSignupManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days }) }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] })] }));
|
|
177
|
+
return (_jsxs(WidePage, { title: t('Account.TITLE', 'Account & Administration'), children: [_jsx(Helmet, { children: _jsxs("title", { children: [t('Account.PAGE_TITLE', 'Account'), " \u2013 ", user.email] }) }), _jsxs(SectionNav, { mode: isMobile ? 'mobile' : 'desktop', groups: groups, activeKey: safeTab, onSelect: (key) => handleTabChange(null, key), children: [safeTab === 'profile' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(ProfileComponent, { onSubmit: handleProfileSubmit, showName: true, showPrivacy: true, showCookies: true, showStatements: showProfileStatements }) })), safeTab === 'security' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SecurityComponent, { fromRecovery: fromRecovery, fromWeakLogin: fromWeakLogin }) })), safeTab === 'users' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(UserListComponent, { roles: activeRoles, currentUser: user, extraColumns: userListExtraColumns, extraRowActions: userListExtraRowActions, extraContext: userListExtraContext, refreshTrigger: userListRefreshTrigger, canEditUser: userListCanEditUser, showNewColumn: userListShowNewColumn, showSuccessfulLoginColumn: userListShowSuccessfulLoginColumn, showRoleColumn: userListShowRoleColumn, onChangeRole: userListOnChangeRole, showDeleteAction: userListShowDeleteAction, canDeleteUser: userListCanDeleteUser, onDeleteUser: userListOnDeleteUser }) })), safeTab === 'invite' && (_jsx(Box, { sx: { mt: 2 }, children: _jsxs(Stack, { spacing: 2.5, children: [canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AuthFactorRequirementCard, { canEdit: canWriteAuthPolicy, policy: authPolicy }) })), canViewAuthPolicy && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(RegistrationMethodsManager, { policy: authPolicy, error: authPolicyError, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canSendInvites && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(UserInviteComponent, {}) })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsxs(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: [_jsx(Typography, { variant: "h6", gutterBottom: true, children: t('Auth.ACCESS_CODE_MANAGER_TITLE', 'Access Codes') }), _jsx(Typography, { variant: "body2", sx: { mb: 2, color: 'text.secondary' }, children: t('Account.ACCESS_CODES_HINT', 'Manage access codes for self-registration.') }), _jsx(AccessCodeManager, {})] })), canManageAccessCodes && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_access_code) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AccessCodeSingleUseToggle, { canEdit: canWriteAuthPolicy, policy: authPolicy, onPolicyChange: setAuthPolicy }) })), canSendInvites && showBulkInviteCsvTab && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_admin_invite) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(BulkInviteCsvTab, Object.assign({}, bulkInviteCsvProps)) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(AllowedEmailDomainsManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_email_domain), domains: (authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allowed_email_domains) || [], onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canViewAuthPolicy && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(QrSignupValidityManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days, onPolicyChange: setAuthPolicy, canEdit: canWriteAuthPolicy }) })), canManageSignupQr && Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr) && (_jsx(Paper, { variant: "outlined", sx: ACCOUNT_PAGE_SECTION_PAPER_SX, children: _jsx(QrSignupManager, { enabled: Boolean(authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.allow_self_signup_qr), expiryDays: authPolicy === null || authPolicy === void 0 ? void 0 : authPolicy.signup_qr_expiry_days }) }))] }) })), safeTab === 'support' && (_jsx(Box, { sx: { mt: 2 }, children: _jsx(SupportRecoveryRequestsTab, {}) })), activeExtraTab && (_jsx(Box, { sx: { mt: 2 }, children: (_a = activeExtraTab.render) === null || _a === void 0 ? void 0 : _a.call(activeExtraTab, { user, perms, isSuperUser, t }) }))] })] }));
|
|
178
178
|
}
|
package/docs/TYPE-SCALE.md
CHANGED
|
@@ -90,6 +90,18 @@ These are inputs for the per-app follow-up after the release, not part of the ba
|
|
|
90
90
|
1 page h5; survey_app uses h3 on 7 pages, which grows from 24 to 28.
|
|
91
91
|
- **Per-level weight overrides** contradict decision 3: survey_app (h1 to h6 800/700, site builder 800),
|
|
92
92
|
hram (500), spesix (weights). survey_app's held typography revert (`SVA-THEME-1`) is decided by this sheet.
|
|
93
|
+
- **subtitle1 also serves as a title, not only as a section label** (added 2026-10-04, after the freeze; the
|
|
94
|
+
inventory missed it, because it read subtitle1 only in its section-label role on hram Research). Uses in
|
|
95
|
+
`src/`: hram 72, jg-ferien 39, ui-core-micha itself 12 (`UserMenu` user name, `SectionNav` active item,
|
|
96
|
+
announcement titles), innoservice 9, survey_app 7, reimbursements 6, spesix 5. As item or panel titles they
|
|
97
|
+
fall below body size. **Decided at the jg-ferien pilot `JG-DEPS-4`, 2026-10-04: subtitle1 stays at 13 px.**
|
|
98
|
+
The operator looked at jg-ferien on the 3.12.0 build, including `FinanceSummaryPanel` with subtitle1 as a
|
|
99
|
+
panel section title, and accepted it without any remapping. Rollout orders therefore do not remap
|
|
100
|
+
subtitle1 by default; a remap happens only where an app's own look rejects a specific screen.
|
|
101
|
+
- **Exception to decision 3: spesix keeps its own heading weights** (operator, 2026-10-04, at `SPX-DEP-3`). Its
|
|
102
|
+
`DESIGN` block (h1 800, h2-h5 700, h6 600, button 700) belongs to a decided design (`SPX-THEME-1`, identity
|
|
103
|
+
bucket), and the new sizes separate the levels on their own. survey_app's 800/700 weights are not covered by
|
|
104
|
+
this exception: there the operator found the bold levels themselves to be the problem.
|
|
93
105
|
- webshop-guenter keeps its handoff scale (decided brand, identity bucket). Kira, Cinevia and Gustav use no MUI
|
|
94
106
|
heading variants and are unaffected.
|
|
95
107
|
|
package/package.json
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
ButtonBase,
|
|
6
6
|
Drawer,
|
|
7
7
|
List,
|
|
8
|
+
ListItem,
|
|
8
9
|
ListItemButton,
|
|
9
10
|
ListItemText,
|
|
10
11
|
Paper,
|
|
@@ -118,13 +119,15 @@ function SectionNavList({
|
|
|
118
119
|
{overviewItem && (
|
|
119
120
|
<Paper variant="outlined" sx={SECTION_NAV_GROUP_PAPER_SX}>
|
|
120
121
|
<List disablePadding>
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
122
|
+
<ListItem disablePadding>
|
|
123
|
+
<ListItemButton
|
|
124
|
+
selected={activeKey == null}
|
|
125
|
+
aria-current={activeKey == null ? 'page' : undefined}
|
|
126
|
+
onClick={() => onSelect?.(null)}
|
|
127
|
+
>
|
|
128
|
+
<ListItemText primary={overviewItem.label} />
|
|
129
|
+
</ListItemButton>
|
|
130
|
+
</ListItem>
|
|
128
131
|
</List>
|
|
129
132
|
</Paper>
|
|
130
133
|
)}
|
|
@@ -143,17 +146,18 @@ function SectionNavList({
|
|
|
143
146
|
{group.items.map((item) => {
|
|
144
147
|
const selected = item.key === activeKey;
|
|
145
148
|
return (
|
|
146
|
-
<
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
149
|
+
<ListItem key={item.key} disablePadding>
|
|
150
|
+
<ListItemButton
|
|
151
|
+
selected={selected}
|
|
152
|
+
aria-current={selected ? 'page' : undefined}
|
|
153
|
+
onClick={() => onSelect?.(item.key)}
|
|
154
|
+
>
|
|
155
|
+
<ListItemText
|
|
156
|
+
primary={item.label}
|
|
157
|
+
secondary={!selected && item.key === rememberedKey ? rememberedLabel : null}
|
|
158
|
+
/>
|
|
159
|
+
</ListItemButton>
|
|
160
|
+
</ListItem>
|
|
157
161
|
);
|
|
158
162
|
})}
|
|
159
163
|
</List>
|
|
@@ -53,6 +53,7 @@ export function AccountPage({
|
|
|
53
53
|
showBulkInviteCsvTab = false,
|
|
54
54
|
bulkInviteCsvProps = {},
|
|
55
55
|
extraTabs = [],
|
|
56
|
+
showProfileStatements = true,
|
|
56
57
|
}) {
|
|
57
58
|
const { t, i18n } = useTranslation();
|
|
58
59
|
const { user, login, loading } = useContext(AuthContext);
|
|
@@ -253,6 +254,7 @@ export function AccountPage({
|
|
|
253
254
|
showName
|
|
254
255
|
showPrivacy
|
|
255
256
|
showCookies
|
|
257
|
+
showStatements={showProfileStatements}
|
|
256
258
|
/>
|
|
257
259
|
</Box>
|
|
258
260
|
)}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
5
|
+
import { ThemeProvider, createTheme } from '@mui/material/styles';
|
|
6
|
+
import { MemoryRouter, Route, Routes } from 'react-router-dom';
|
|
7
|
+
|
|
8
|
+
vi.mock('react-i18next', () => {
|
|
9
|
+
const KEYS = {
|
|
10
|
+
'Account.TITLE': 'Account & Administration',
|
|
11
|
+
'Account.PAGE_TITLE': 'Account',
|
|
12
|
+
'Account.TAB_PROFILE': 'Profile',
|
|
13
|
+
'Account.TAB_SECURITY': 'Security',
|
|
14
|
+
'Account.GROUP_MY_ACCOUNT': 'My account',
|
|
15
|
+
'Profile.ACCEPT_PRIVACY_LABEL': 'Accept privacy statement',
|
|
16
|
+
'Profile.ACCEPT_COOKIES_LABEL': 'Accept convenience cookies',
|
|
17
|
+
'Profile.PRIVACY_STATEMENT_TITLE': 'Privacy statement',
|
|
18
|
+
'Profile.COOKIES_STATEMENT_TITLE': 'Cookie statement',
|
|
19
|
+
'Profile.SAVE_BUTTON': 'Save',
|
|
20
|
+
'Profile.PRIVACY_SECTION_TITLE': 'Privacy and cookies',
|
|
21
|
+
'Profile.USERNAME_LABEL': 'Username',
|
|
22
|
+
'Profile.EMAIL_LABEL': 'Email',
|
|
23
|
+
'Profile.FIRST_NAME_LABEL': 'First name',
|
|
24
|
+
'Profile.LAST_NAME_LABEL': 'Last name',
|
|
25
|
+
};
|
|
26
|
+
const t = (key, fallback) => KEYS[key] ?? fallback ?? key;
|
|
27
|
+
return {
|
|
28
|
+
useTranslation: () => ({
|
|
29
|
+
t,
|
|
30
|
+
i18n: { exists: (key) => Object.hasOwn(KEYS, key), t },
|
|
31
|
+
}),
|
|
32
|
+
};
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
vi.mock('../src/auth/authApi', () => ({
|
|
36
|
+
fetchAuthPolicy: vi.fn(),
|
|
37
|
+
fetchPrivacyStatement: vi.fn(),
|
|
38
|
+
fetchCookieStatement: vi.fn(),
|
|
39
|
+
updateUserProfile: vi.fn(),
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
vi.mock('../src/components/SecurityComponent', () => ({ SecurityComponent: () => null }));
|
|
43
|
+
vi.mock('../src/components/UserListComponent', () => ({ UserListComponent: () => null }));
|
|
44
|
+
vi.mock('../src/components/UserInviteComponent', () => ({ UserInviteComponent: () => null }));
|
|
45
|
+
vi.mock('../src/components/AccessCodeManager', () => ({ AccessCodeManager: () => null }));
|
|
46
|
+
vi.mock('../src/components/AllowedEmailDomainsManager', () => ({ AllowedEmailDomainsManager: () => null }));
|
|
47
|
+
vi.mock('../src/components/RegistrationMethodsManager', () => ({ RegistrationMethodsManager: () => null }));
|
|
48
|
+
vi.mock('../src/components/AuthFactorRequirementCard', () => ({ AuthFactorRequirementCard: () => null }));
|
|
49
|
+
vi.mock('../src/components/AccessCodeSingleUseToggle', () => ({ AccessCodeSingleUseToggle: () => null }));
|
|
50
|
+
vi.mock('../src/components/QrSignupManager', () => ({ QrSignupManager: () => null }));
|
|
51
|
+
vi.mock('../src/components/QrSignupValidityManager', () => ({ QrSignupValidityManager: () => null }));
|
|
52
|
+
vi.mock('../src/components/SupportRecoveryRequestsTab', () => ({ SupportRecoveryRequestsTab: () => null }));
|
|
53
|
+
vi.mock('../src/components/BulkInviteCsvTab', () => ({ BulkInviteCsvTab: () => null }));
|
|
54
|
+
|
|
55
|
+
import { AuthContext } from '../src/auth/AuthContext';
|
|
56
|
+
import {
|
|
57
|
+
fetchCookieStatement,
|
|
58
|
+
fetchPrivacyStatement,
|
|
59
|
+
updateUserProfile,
|
|
60
|
+
} from '../src/auth/authApi';
|
|
61
|
+
import { AccountPage } from '../src/pages/AccountPage';
|
|
62
|
+
|
|
63
|
+
const theme = createTheme();
|
|
64
|
+
const user = {
|
|
65
|
+
email: 'person@example.com',
|
|
66
|
+
username: 'person',
|
|
67
|
+
first_name: 'Pat',
|
|
68
|
+
last_name: 'Example',
|
|
69
|
+
accepted_privacy_statement: false,
|
|
70
|
+
accepted_convenience_cookies: true,
|
|
71
|
+
is_superuser: false,
|
|
72
|
+
ui_permissions: {},
|
|
73
|
+
available_roles: [],
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function setViewportWidth(width) {
|
|
77
|
+
window.matchMedia = vi.fn().mockImplementation((query) => ({
|
|
78
|
+
matches: query.includes('max-width') ? width <= 900 : width > 900,
|
|
79
|
+
media: query,
|
|
80
|
+
onchange: null,
|
|
81
|
+
addEventListener: vi.fn(),
|
|
82
|
+
removeEventListener: vi.fn(),
|
|
83
|
+
addListener: vi.fn(),
|
|
84
|
+
removeListener: vi.fn(),
|
|
85
|
+
dispatchEvent: vi.fn(),
|
|
86
|
+
}));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function renderAccount({ showProfileStatements } = {}) {
|
|
90
|
+
const login = vi.fn();
|
|
91
|
+
return {
|
|
92
|
+
login,
|
|
93
|
+
...render(
|
|
94
|
+
<ThemeProvider theme={theme}>
|
|
95
|
+
<AuthContext.Provider value={{ user, login, loading: false }}>
|
|
96
|
+
<MemoryRouter initialEntries={['/account']}>
|
|
97
|
+
<Routes>
|
|
98
|
+
<Route
|
|
99
|
+
path="/account"
|
|
100
|
+
element={<AccountPage showProfileStatements={showProfileStatements} />}
|
|
101
|
+
/>
|
|
102
|
+
</Routes>
|
|
103
|
+
</MemoryRouter>
|
|
104
|
+
</AuthContext.Provider>
|
|
105
|
+
</ThemeProvider>,
|
|
106
|
+
),
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
describe('AccountPage profile statements', () => {
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
setViewportWidth(1280);
|
|
113
|
+
vi.mocked(fetchPrivacyStatement).mockResolvedValue('Privacy text');
|
|
114
|
+
vi.mocked(fetchCookieStatement).mockResolvedValue('Cookie text');
|
|
115
|
+
vi.mocked(updateUserProfile).mockResolvedValue({ ...user, accepted_privacy_statement: true });
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
afterEach(() => {
|
|
119
|
+
cleanup();
|
|
120
|
+
vi.clearAllMocks();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('fetches and presents both statement panels when the prop is omitted', async () => {
|
|
124
|
+
renderAccount();
|
|
125
|
+
|
|
126
|
+
expect(await screen.findByText('Privacy statement')).toBeTruthy();
|
|
127
|
+
expect(screen.getByText('Cookie statement')).toBeTruthy();
|
|
128
|
+
await waitFor(() => {
|
|
129
|
+
expect(fetchPrivacyStatement).toHaveBeenCalledOnce();
|
|
130
|
+
expect(fetchCookieStatement).toHaveBeenCalledOnce();
|
|
131
|
+
});
|
|
132
|
+
expect(await screen.findByText('Privacy text')).toBeTruthy();
|
|
133
|
+
expect(screen.getByText('Cookie text')).toBeTruthy();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('hides statements and skips requests while retaining editable consent submission', async () => {
|
|
137
|
+
const { login } = renderAccount({ showProfileStatements: false });
|
|
138
|
+
|
|
139
|
+
expect(screen.queryByText('Privacy statement')).toBeNull();
|
|
140
|
+
expect(screen.queryByText('Cookie statement')).toBeNull();
|
|
141
|
+
expect(fetchPrivacyStatement).not.toHaveBeenCalled();
|
|
142
|
+
expect(fetchCookieStatement).not.toHaveBeenCalled();
|
|
143
|
+
|
|
144
|
+
const privacyConsent = screen.getByRole('checkbox', { name: 'Accept privacy statement' });
|
|
145
|
+
const cookieConsent = screen.getByRole('checkbox', { name: 'Accept convenience cookies' });
|
|
146
|
+
expect(privacyConsent.checked).toBe(false);
|
|
147
|
+
expect(cookieConsent.checked).toBe(true);
|
|
148
|
+
fireEvent.click(privacyConsent);
|
|
149
|
+
fireEvent.click(cookieConsent);
|
|
150
|
+
fireEvent.click(screen.getByRole('button', { name: 'Save' }));
|
|
151
|
+
|
|
152
|
+
await waitFor(() => {
|
|
153
|
+
expect(updateUserProfile).toHaveBeenCalledWith({
|
|
154
|
+
first_name: 'Pat',
|
|
155
|
+
last_name: 'Example',
|
|
156
|
+
accepted_privacy_statement: true,
|
|
157
|
+
accepted_convenience_cookies: false,
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
expect(login).toHaveBeenCalledOnce();
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -68,6 +68,17 @@ function renderNav(props = {}, currentTheme = theme) {
|
|
|
68
68
|
);
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
function expectListsToContainListItemButtons(lists) {
|
|
72
|
+
expect(lists.length).toBeGreaterThan(0);
|
|
73
|
+
lists.forEach((list) => {
|
|
74
|
+
expect(Array.from(list.children).every((child) => child.tagName === 'LI')).toBe(true);
|
|
75
|
+
expect(list.querySelectorAll(':scope > li')).toHaveLength(list.children.length);
|
|
76
|
+
list.querySelectorAll(':scope > li').forEach((item) => {
|
|
77
|
+
expect(item.querySelectorAll(':scope > .MuiListItemButton-root')).toHaveLength(1);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
71
82
|
describe('SectionNav', () => {
|
|
72
83
|
afterEach(() => {
|
|
73
84
|
cleanup();
|
|
@@ -133,6 +144,35 @@ describe('SectionNav', () => {
|
|
|
133
144
|
expect(container.querySelectorAll('.MuiPaper-root')).toHaveLength(groups.length);
|
|
134
145
|
});
|
|
135
146
|
|
|
147
|
+
it('renders overview and grouped controls inside list items on desktop and mobile', () => {
|
|
148
|
+
const overviewItem = { label: 'Overview' };
|
|
149
|
+
const { container } = renderNav({ overviewItem });
|
|
150
|
+
|
|
151
|
+
expectListsToContainListItemButtons(container.querySelectorAll('nav ul'));
|
|
152
|
+
expect(container.querySelectorAll('nav ul')).toHaveLength(groups.length + 1);
|
|
153
|
+
|
|
154
|
+
cleanup();
|
|
155
|
+
renderNav({ mode: 'mobile', overviewItem, open: true });
|
|
156
|
+
const dialog = screen.getByRole('dialog', { name: 'Switch section' });
|
|
157
|
+
const lists = dialog.querySelectorAll('ul');
|
|
158
|
+
|
|
159
|
+
expectListsToContainListItemButtons(lists);
|
|
160
|
+
expect(lists).toHaveLength(groups.length + 1);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('passes null for overview selection and closes the mobile drawer', async () => {
|
|
164
|
+
const onSelect = vi.fn();
|
|
165
|
+
renderNav({ mode: 'mobile', onSelect, overviewItem: { label: 'Overview' } });
|
|
166
|
+
|
|
167
|
+
fireEvent.click(screen.getByRole('button', { name: /Section Profile/i }));
|
|
168
|
+
expect(screen.getByRole('dialog', { name: 'Switch section' })).toBeTruthy();
|
|
169
|
+
fireEvent.click(screen.getByRole('button', { name: 'Overview' }));
|
|
170
|
+
|
|
171
|
+
expect(onSelect).toHaveBeenCalledOnce();
|
|
172
|
+
expect(onSelect).toHaveBeenCalledWith(null);
|
|
173
|
+
await waitFor(() => expect(screen.queryByRole('dialog', { name: 'Switch section' })).toBeNull());
|
|
174
|
+
});
|
|
175
|
+
|
|
136
176
|
it('shows the remembered secondary line only on its unselected item', () => {
|
|
137
177
|
renderNav({ activeKey: 'profile', rememberedKey: 'security' });
|
|
138
178
|
expect(screen.getByText('Last opened')).toBeTruthy();
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# UCM-AUTH-12: Optional profile statement text on the account page
|
|
2
|
+
|
|
3
|
+
# A. Envelope — operator-approved scope
|
|
4
|
+
|
|
5
|
+
## Goal and expected outcome
|
|
6
|
+
|
|
7
|
+
Expose `AccountPage.showProfileStatements` so an app without privacy/cookie statement
|
|
8
|
+
endpoints can suppress the explanatory statement panels and their requests. The default
|
|
9
|
+
is `true`, preserving every existing consumer's behavior. With `false`, the account profile
|
|
10
|
+
still renders and submits both existing consent fields.
|
|
11
|
+
|
|
12
|
+
The operator explicitly approved this switch, its release and the innoservice pin bump on
|
|
13
|
+
2026-10-13. innoservice CI38387014900 records privacy/cookie 404s; auth policy returns 200.
|
|
14
|
+
|
|
15
|
+
## Scope and non-goals
|
|
16
|
+
|
|
17
|
+
- Add the optional prop and pass it to the existing ProfileComponent statement switch.
|
|
18
|
+
- Publish a minor release 3.13.0; innoservice consumes it under INNO-FIX-13.
|
|
19
|
+
- Do not change consent defaults, payloads, validation, permissions, authentication,
|
|
20
|
+
account navigation, statement fetching implementation, theme tokens or other consumers.
|
|
21
|
+
- Do not fix UCM-SHELL-8 or invent statement endpoints/content.
|
|
22
|
+
- No new visual design or prototype; this config uses the existing ProfileComponent surface.
|
|
23
|
+
|
|
24
|
+
## Tier, risk and required tests
|
|
25
|
+
|
|
26
|
+
Tier 3: source inside shared-core on an account surface. Compatibility rests on default true.
|
|
27
|
+
Tests to write: render the real AccountPage and ProfileComponent; omission of the new prop
|
|
28
|
+
fetches both statements and presents the existing panels; false performs neither request and
|
|
29
|
+
hides the panels, while both consent controls remain editable and their values reach the
|
|
30
|
+
existing updateUserProfile submission. Keep the existing account-navigation tests.
|
|
31
|
+
|
|
32
|
+
# B. Implementation map — ADDRESSED TO THE IMPLEMENTER
|
|
33
|
+
|
|
34
|
+
## Target and ownership
|
|
35
|
+
|
|
36
|
+
Target: `C:/Users/Micha Bigler/Documents/webapps/ui-core-micha`.
|
|
37
|
+
Native bootstrap retains workspace-root cwd; use explicit absolute paths and workdirs.
|
|
38
|
+
You are not alone in the workspace. Own only:
|
|
39
|
+
- `src/pages/AccountPage.jsx`
|
|
40
|
+
- new `tests/AccountPage.profileStatements.test.jsx`
|
|
41
|
+
|
|
42
|
+
## Context package
|
|
43
|
+
|
|
44
|
+
`src/pages/AccountPage.jsx:39` exports named AccountPage with destructured optional props.
|
|
45
|
+
Its profile branch near line228 renders ProfileComponent with showName/showPrivacy/showCookies.
|
|
46
|
+
Its existing handleProfileSubmit calls updateUserProfile(payload), then AuthContext.login(updated).
|
|
47
|
+
`src/components/ProfileComponent.jsx:23` exports named ProfileComponent; showStatements defaults
|
|
48
|
+
true, independently of showPrivacy/showCookies. Its statement-loading effect at83 returns before
|
|
49
|
+
either fetch when showStatements is false. Panels at255 are also controlled by showStatements.
|
|
50
|
+
Consent controls at230/242 and payload at130/133 depend only on showPrivacy/showCookies.
|
|
51
|
+
Use that established switch; leave this component untouched.
|
|
52
|
+
|
|
53
|
+
Read only these targets plus `tests/AccountPage.test.jsx` for theme/router/AuthContext setup,
|
|
54
|
+
and `src/auth/authApi.jsx` for the four API export names if necessary. Do not explore broadly.
|
|
55
|
+
The existing navigation test mocks ProfileComponent, so it cannot prove the no-request and
|
|
56
|
+
consent invariant: write a separate test file rendering both real components. Stub network API
|
|
57
|
+
functions and unrelated account children only. Relevant real accessible labels are translation
|
|
58
|
+
keys Profile.ACCEPT_PRIVACY_LABEL, Profile.ACCEPT_COOKIES_LABEL, Profile.PRIVACY_STATEMENT_TITLE,
|
|
59
|
+
Profile.COOKIES_STATEMENT_TITLE and Profile.SAVE_BUTTON.
|
|
60
|
+
Use the suite's established translation seam; never mock AccountPage/ProfileComponent under test.
|
|
61
|
+
|
|
62
|
+
## Preamble
|
|
63
|
+
|
|
64
|
+
> This committed WO is the COMPLETE spec. You are the initial implementer, not the reviewer.
|
|
65
|
+
> Preserve other edits. Edit only the two owned source/test paths using explicit UTF-8 I/O.
|
|
66
|
+
> Do not edit dependencies, lockfiles, package/version/changelog/register/README/governance,
|
|
67
|
+
> settings, environment, other repos or memory. Never read .env or dump settings/environment.
|
|
68
|
+
> No installs, package-manager commands, containers, git operations, self-review or subagents.
|
|
69
|
+
> Write the required tests and run your own new file ONCE with the existing Node/Vitest runtime,
|
|
70
|
+
> maxWorkers=2; no affected/full suite, build or app startup. If unavailable, report the obstacle.
|
|
71
|
+
> The orchestrator's tests and separate reviews are the acceptance gate. Leave uncommitted.
|
|
72
|
+
> Start PLAN, emit PROGRESS before relevant actions without silence over two minutes, then
|
|
73
|
+
> exactly one final RESULT: DONE or RESULT: BLOCKED.
|
|
74
|
+
|
|
75
|
+
# C. Orchestrator only — NOT ADDRESSED TO THE IMPLEMENTER
|
|
76
|
+
|
|
77
|
+
> IMPLEMENTER STOP HERE. You ARE the initial implementation instance. No steps below belong to you.
|
|
78
|
+
|
|
79
|
+
Gate #1: operator explicitly approved the exact API/default, retained consents, release and pin
|
|
80
|
+
bump in the innoservice orchestration chat on 2026-10-13. Native plain worker requested
|
|
81
|
+
codex/gpt-6-luna, fresh context, full committed WO inline. Record accepted ID and actual metadata;
|
|
82
|
+
effective provider/model stays unverified when the API does not expose it. No protection-parity claim.
|
|
83
|
+
|
|
84
|
+
Parent owns version/changelog/README/register and pnpm state. Independent four lenses plus UI
|
|
85
|
+
and security concurrently, full scoped context, CLI read-only without bypass. Parent fixes findings.
|
|
86
|
+
Scoped gate: new real-component tests, existing AccountPage navigation and affected auth tests.
|
|
87
|
+
Publish workflow on main already runs build/full tests; shared-core ripple justifies that existing
|
|
88
|
+
release suite. Do not dispatch publish manually; normal reviewed main push triggers it. Check
|
|
89
|
+
publish job success before app pin. Minor 3.13.0 is additive; read exact release diff before app bump.
|
|
90
|
+
Final core row includes deduplicated review totals and test results; second register commit before
|
|
91
|
+
one main push. innoservice AccountPage/WelcomePage statements false and capture evidence remain
|
|
92
|
+
INNO-FIX-13; keep existing consents, request diagnostics and capture checks intact.
|
|
93
|
+
|
|
94
|
+
Accepted native implementer: `/root/kit_account_statements`, explicit requested gpt-6-luna,
|
|
95
|
+
high reasoning, fresh context, complete WO inline. Effective native provider/model metadata
|
|
96
|
+
was not exposed and remains unverified. Source/test edits only; the one permitted author test
|
|
97
|
+
attempt failed before collection with temporary-file EPERM (zero tests). Parent runs the
|
|
98
|
+
authoritative scoped gate through the normal runtime approval path. No author review or git.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# UCM-SHELL-8: Valid SectionNav list items
|
|
2
|
+
|
|
3
|
+
# A. Envelope — operator-approved scope
|
|
4
|
+
|
|
5
|
+
## Goal and expected outcome
|
|
6
|
+
|
|
7
|
+
SectionNav navigation entries are genuine list items containing the existing buttons.
|
|
8
|
+
The account capture in innoservice reports three serious axe list violations at 1280 px;
|
|
9
|
+
these remain in ui-core-micha 3.13.0. The correction removes their structural cause.
|
|
10
|
+
|
|
11
|
+
## Scope and non-goals
|
|
12
|
+
|
|
13
|
+
- Correct grouped and optional overview entries in SectionNav's shared list renderer.
|
|
14
|
+
- Keep selection, aria-current, labels, remembered entry, keyboard operation, callbacks,
|
|
15
|
+
controlled/uncontrolled drawer behavior and appearance unchanged.
|
|
16
|
+
- Release the correction as patch 3.13.1 and update innoservice's exact package pin.
|
|
17
|
+
- No navigation redesign, new text, token delta, authentication, permissions, data contract,
|
|
18
|
+
other component, CI configuration or dependency changes beyond that release/pin.
|
|
19
|
+
- No new visual design or prototype: retain the existing presentation.
|
|
20
|
+
|
|
21
|
+
## Tier, risk and required tests to write
|
|
22
|
+
|
|
23
|
+
Tier 3: shared-core source with a rendered DOM correction used by consumers.
|
|
24
|
+
Risk: wrapper defaults could alter spacing or interrupt existing button interaction.
|
|
25
|
+
Write focused assertions that every grouped and overview list has li children containing
|
|
26
|
+
the existing button controls, on desktop and inside the mobile drawer. Preserve existing
|
|
27
|
+
selection and drawer tests; prove overview selection still passes null and closes the drawer.
|
|
28
|
+
|
|
29
|
+
# B. Implementation map — ADDRESSED TO THE IMPLEMENTER
|
|
30
|
+
|
|
31
|
+
## Target and ownership
|
|
32
|
+
|
|
33
|
+
Target: `C:/Users/Micha Bigler/Documents/webapps/ui-core-micha`.
|
|
34
|
+
Native bootstrap retains workspace-root cwd; use absolute paths and explicit workdirs.
|
|
35
|
+
You are not alone in the codebase; preserve other agents' changes. Own only:
|
|
36
|
+
- `src/layout/SectionNav.jsx`
|
|
37
|
+
- `tests/SectionNav.test.jsx`
|
|
38
|
+
|
|
39
|
+
## Context package
|
|
40
|
+
|
|
41
|
+
`src/layout/SectionNav.jsx:120`, SectionNavList, is shared by the desktop nav and mobile
|
|
42
|
+
drawer. The overview List currently directly contains ListItemButton; grouped Lists at
|
|
43
|
+
159 map item keys to direct ListItemButton children. MUI List defaults to ul and the
|
|
44
|
+
button defaults to div. Correct list containment while preserving the interactive button.
|
|
45
|
+
The overview handler passes null; grouped handlers pass item.key. Selected buttons carry
|
|
46
|
+
aria-current=page; remembered secondary text is shown only on its unselected entry.
|
|
47
|
+
Keep exported sx constants, drawer state handlers, prop defaults and group label IDs.
|
|
48
|
+
Use existing MUI primitives; prevent wrapper padding from changing the presentation.
|
|
49
|
+
|
|
50
|
+
`tests/SectionNav.test.jsx` renders the real named SectionNav with ThemeProvider and a
|
|
51
|
+
translation mock. Existing labels: Profile, Security, Users, Invite, Support, Notifications;
|
|
52
|
+
optional Overview; trigger accessible name matches /Section Profile/i; drawer title
|
|
53
|
+
Switch section. Existing tests cover selection, remembered text, controlled/uncontrolled
|
|
54
|
+
drawer, theme tokens and grid. Extend this file without mocking SectionNav or changing
|
|
55
|
+
existing tests to accept weaker behavior. Work from these two files, no broad exploration.
|
|
56
|
+
|
|
57
|
+
## Preamble and progress contract
|
|
58
|
+
|
|
59
|
+
> This committed WO is the COMPLETE spec. You are the initial implementer, not the reviewer.
|
|
60
|
+
> Edit only the owned source/test paths using explicit UTF-8 I/O. Preserve other changes.
|
|
61
|
+
> Do not edit dependencies, lockfiles, package/version/changelog/register/README/governance,
|
|
62
|
+
> settings, environment, other repos or memory. Never read .env or dump environment/settings.
|
|
63
|
+
> Your tools are for editing source/test files and running the tests you wrote, nothing else.
|
|
64
|
+
> No installs, package-manager commands, containers, git operations, self-review or subagents.
|
|
65
|
+
> Run your newly written SectionNav cases ONCE with the existing Node/Vitest runtime,
|
|
66
|
+
> maxWorkers=2, selected by test names; no affected/full suite, build or app startup.
|
|
67
|
+
> If blocked, report it rather than changing repo state. Leave all changes uncommitted.
|
|
68
|
+
> The orchestrator's tests and independent reviews are the acceptance gate, not your run.
|
|
69
|
+
> Start PLAN, emit PROGRESS before relevant actions with no silence over two minutes,
|
|
70
|
+
> then exactly one final RESULT: DONE or RESULT: BLOCKED.
|
|
71
|
+
|
|
72
|
+
# C. Orchestrator only — NOT ADDRESSED TO THE IMPLEMENTER
|
|
73
|
+
|
|
74
|
+
> IMPLEMENTER STOP HERE. You ARE the initial implementation instance. No steps below belong to you.
|
|
75
|
+
|
|
76
|
+
Gate #1: operator explicitly approved this exact structural correction, preserved behavior
|
|
77
|
+
and appearance, tests/review, release and innoservice pin bump on 2026-10-13.
|
|
78
|
+
No source approval was inferred from the earlier registration or clarification question.
|
|
79
|
+
|
|
80
|
+
Dispatch: fresh native plain worker, requested codex/gpt-6-luna, full WO inline; inherited
|
|
81
|
+
cwd is workspace root, target is the absolute core repo above. Record accepted identity
|
|
82
|
+
and only exposed effective metadata. Assigned ownership is not technical containment.
|
|
83
|
+
Parent owns repo state, package version, changelog, README/register and release/pin.
|
|
84
|
+
|
|
85
|
+
Independent envelope/regression/duplication/tests lenses plus UI run concurrently on the
|
|
86
|
+
same frozen diff via fresh read-only codex/gpt-6-luna processes. Security reviewer is not
|
|
87
|
+
required: no auth/security behavior changes. Parent also checks exposure/permission parity.
|
|
88
|
+
Parent runs SectionNav and both existing AccountPage test files, then build. Existing main
|
|
89
|
+
publish workflow supplies the full shared-core regression suite and publishing evidence;
|
|
90
|
+
normal reviewed main push, no manual publish dispatch. Confirm successful publication of
|
|
91
|
+
3.13.1 before innoservice exact pin and lockfile update. Keep all app capture/axe checks.
|
|
92
|
+
Fresh app capture at 375/1280 must verify navigation and absence of the three list violations.
|
|
93
|
+
Finalize register with named reviews, deduplicated dispositions and actual test/release proof.
|
|
94
|
+
|
|
95
|
+
Accepted native implementer: `/root/kit_sectionnav`, explicit requested gpt-6-luna, high
|
|
96
|
+
reasoning, fresh context. Effective native provider/model metadata was not exposed and
|
|
97
|
+
remains unverified. Only the two assigned files changed; no author git/review/repo-state
|
|
98
|
+
operations. Author's single permitted test run failed before collection with temporary-file
|
|
99
|
+
EPERM (zero tests). Parent runs the authoritative gate through the normal runtime path.
|
|
100
|
+
|
|
101
|
+
Core acceptance: five fresh concurrent CLI reviews (envelope, regression, duplication,
|
|
102
|
+
tests, UI), all exit 0, banners confirm codex/gpt-6-luna, provider openai, read-only.
|
|
103
|
+
Deduplicated disposition: 1 raised, 0 accepted, worst accepted none. Tests lens R1 proposed
|
|
104
|
+
proving the MUI implementation identity against a fabricated class-bearing stub. Rejected:
|
|
105
|
+
the structural assertion fails against the actual pre-change direct div children, existing
|
|
106
|
+
selection/theme/drawer tests retain their behavior checks, and the source still directly
|
|
107
|
+
imports and renders the real MUI ListItemButton. No behavior or identity abstraction is added.
|
|
108
|
+
Other lenses report no findings. Parent targeted pass confirms callbacks/permission filtering,
|
|
109
|
+
labels and selected/remembered state unchanged; wrappers use disablePadding.
|
|
110
|
+
Parent gate: 21 tests in SectionNav.test.jsx, AccountPage.test.jsx and
|
|
111
|
+
AccountPage.profileStatements.test.jsx passed with maxWorkers=2; TypeScript build exit 0.
|
|
112
|
+
Publish workflow's full suite is justified by shared-core consumer ripple. App pin and fresh
|
|
113
|
+
consumer capture remain pending after the reviewed core source/release lands.
|