@micha.bigler/ui-core-micha 3.13.1 → 3.13.2
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 +6 -0
- package/WORK_ORDERS.md +2 -1
- package/dist/components/ProfileComponent.js +45 -10
- package/package.json +1 -1
- package/src/components/ProfileComponent.jsx +44 -10
- package/tests/ProfileComponent.refresh.test.jsx +194 -0
- package/work-orders/UCM-AUTH-13.md +45 -0
- package/work-orders/UCM-SHELL-8.md +12 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.13.2 - UCM-AUTH-13
|
|
4
|
+
|
|
5
|
+
`ProfileComponent` preserves edited names and consent values when the same person's
|
|
6
|
+
context data refreshes. Untouched fields still receive server updates. Account changes
|
|
7
|
+
and logout reset the form, keeping edits and consent isolated between people.
|
|
8
|
+
|
|
3
9
|
## 3.13.1 - UCM-SHELL-8
|
|
4
10
|
|
|
5
11
|
`SectionNav` now renders grouped and overview navigation buttons inside list items on
|
package/WORK_ORDERS.md
CHANGED
|
@@ -28,8 +28,9 @@ 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-13 | [Preserve profile edits during refresh](work-orders/UCM-AUTH-13.md) | Keep unsaved names and consent for the same person; reset at account boundaries. Patch release and Spesix consumption approved. | 2026-10-14 | in-review | ab85c60 | Tier 3. Gate #1 approved by the operator in the Spesix orchestration chat on 2026-10-14. Fresh native configured codex/gpt-6-luna/high author `/root/ucm_auth13_author`; workspace-root cwd bootstrap, absolute target paths; effective provider/cwd/sandbox not exposed. Foreign scratchpad excluded. review: codex/gpt-6-luna · lenses: envelope,regression,duplication,tests,ui,security · 2 raised · 0 accepted · worst accepted: none. Parent 11 focused tests and TypeScript build passed. Patch 3.13.2 release and Spesix consumption pending; detailed dispositions in WO C. |
|
|
31
32
|
| 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 |
|
|
33
|
+
| 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 | done | `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. Closing proof:3.13.1 published CI38435441867 (744tests89files/build/npm success); app exact pin/sourcec527918, CI38444366349 account375/1280 both axe0,console/network0,rubric22yes and parent actual-image inspection; all three list violations gone. No source change since reviewed d399f51. |
|
|
33
34
|
| 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 |
|
|
34
35
|
| 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 |
|
|
35
36
|
| 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,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
// src/components/ProfileComponent.jsx
|
|
3
|
-
import React, { useEffect, useState, useContext } from 'react';
|
|
3
|
+
import React, { useEffect, useState, useContext, useRef } from 'react';
|
|
4
4
|
import { Box, Stack, TextField, FormControlLabel, Checkbox, Button, CircularProgress, Alert, Typography, Accordion, AccordionSummary, AccordionDetails, } from '@mui/material';
|
|
5
5
|
import { useTranslation } from 'react-i18next';
|
|
6
6
|
// WICHTIG: Importiere den Context, um die bereits geladenen Daten zu nutzen
|
|
@@ -22,20 +22,43 @@ export function ProfileComponent({ onSubmit, submitText, showName = true, showPr
|
|
|
22
22
|
const [lastName, setLastName] = useState('');
|
|
23
23
|
const [acceptedPrivacy, setAcceptedPrivacy] = useState(false);
|
|
24
24
|
const [acceptedCookies, setAcceptedCookies] = useState(false);
|
|
25
|
+
const previousUserIdentity = useRef(Symbol('uninitialized'));
|
|
26
|
+
const editedFields = useRef(new Set());
|
|
25
27
|
const [privacyStatement, setPrivacyStatement] = useState(privacyStatementText || '');
|
|
26
28
|
const [cookieStatement, setCookieStatement] = useState(cookieStatementText || '');
|
|
27
29
|
const [loadingPrivacyStatement, setLoadingPrivacyStatement] = useState(false);
|
|
28
30
|
const [loadingCookieStatement, setLoadingCookieStatement] = useState(false);
|
|
29
|
-
//
|
|
31
|
+
// Refresh server values for untouched fields, while keeping edits for this identity.
|
|
30
32
|
useEffect(() => {
|
|
31
|
-
var _a, _b, _c, _d;
|
|
33
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
34
|
+
const userIdentity = user
|
|
35
|
+
? ((_c = (_b = (_a = user.id) !== null && _a !== void 0 ? _a : user.email) !== null && _b !== void 0 ? _b : user.username) !== null && _c !== void 0 ? _c : null)
|
|
36
|
+
: null;
|
|
37
|
+
if (previousUserIdentity.current !== userIdentity) {
|
|
38
|
+
previousUserIdentity.current = userIdentity;
|
|
39
|
+
editedFields.current = new Set();
|
|
40
|
+
}
|
|
32
41
|
if (user) {
|
|
33
|
-
setUsername((
|
|
34
|
-
setEmail((
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
42
|
+
setUsername((_d = user.username) !== null && _d !== void 0 ? _d : '');
|
|
43
|
+
setEmail((_e = user.email) !== null && _e !== void 0 ? _e : '');
|
|
44
|
+
if (!editedFields.current.has('firstName'))
|
|
45
|
+
setFirstName((_f = user.first_name) !== null && _f !== void 0 ? _f : '');
|
|
46
|
+
if (!editedFields.current.has('lastName'))
|
|
47
|
+
setLastName((_g = user.last_name) !== null && _g !== void 0 ? _g : '');
|
|
48
|
+
if (!editedFields.current.has('acceptedPrivacy')) {
|
|
49
|
+
setAcceptedPrivacy(Boolean(user.accepted_privacy_statement));
|
|
50
|
+
}
|
|
51
|
+
if (!editedFields.current.has('acceptedCookies')) {
|
|
52
|
+
setAcceptedCookies(Boolean(user.accepted_convenience_cookies));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
setUsername('');
|
|
57
|
+
setEmail('');
|
|
58
|
+
setFirstName('');
|
|
59
|
+
setLastName('');
|
|
60
|
+
setAcceptedPrivacy(false);
|
|
61
|
+
setAcceptedCookies(false);
|
|
39
62
|
}
|
|
40
63
|
}, [user]);
|
|
41
64
|
useEffect(() => {
|
|
@@ -129,5 +152,17 @@ export function ProfileComponent({ onSubmit, submitText, showName = true, showPr
|
|
|
129
152
|
}
|
|
130
153
|
const submitLabel = submitText || t('Profile.SAVE_BUTTON');
|
|
131
154
|
const submitLabelLoading = t('Profile.SAVE_BUTTON_LOADING');
|
|
132
|
-
return (_jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { maxWidth: 600, display: 'flex', flexDirection: 'column', gap: 2 }, children: [errorKey && (_jsx(Alert, { severity: "error", children: errorKey })), successKey && (_jsx(Alert, { severity: "success", children: t(successKey) })), _jsxs(Stack, { spacing: 2, children: [_jsx(TextField, { label: t('Profile.USERNAME_LABEL'), value: username, fullWidth: true, disabled: true }), _jsx(TextField, { label: t('Auth.EMAIL_LABEL'), type: "email", value: email, fullWidth: true, disabled: true })] }), showName && (_jsxs(Stack, { spacing: 2, direction: { xs: 'column', sm: 'row' }, children: [_jsx(TextField, { label: t('Profile.FIRST_NAME_LABEL'), value: firstName, onChange: (e) =>
|
|
155
|
+
return (_jsxs(Box, { component: "form", onSubmit: handleSubmit, sx: { maxWidth: 600, display: 'flex', flexDirection: 'column', gap: 2 }, children: [errorKey && (_jsx(Alert, { severity: "error", children: errorKey })), successKey && (_jsx(Alert, { severity: "success", children: t(successKey) })), _jsxs(Stack, { spacing: 2, children: [_jsx(TextField, { label: t('Profile.USERNAME_LABEL'), value: username, fullWidth: true, disabled: true }), _jsx(TextField, { label: t('Auth.EMAIL_LABEL'), type: "email", value: email, fullWidth: true, disabled: true })] }), showName && (_jsxs(Stack, { spacing: 2, direction: { xs: 'column', sm: 'row' }, children: [_jsx(TextField, { label: t('Profile.FIRST_NAME_LABEL'), value: firstName, onChange: (e) => {
|
|
156
|
+
editedFields.current.add('firstName');
|
|
157
|
+
setFirstName(e.target.value);
|
|
158
|
+
}, fullWidth: true }), _jsx(TextField, { label: t('Profile.LAST_NAME_LABEL'), value: lastName, onChange: (e) => {
|
|
159
|
+
editedFields.current.add('lastName');
|
|
160
|
+
setLastName(e.target.value);
|
|
161
|
+
}, fullWidth: true })] })), (showPrivacy || showCookies) && (_jsxs(Box, { sx: { mt: 1 }, children: [_jsx(Typography, { variant: "subtitle1", gutterBottom: true, children: t('Profile.PRIVACY_SECTION_TITLE') }), _jsxs(Stack, { spacing: 1, children: [showPrivacy && (_jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: acceptedPrivacy, onChange: (e) => {
|
|
162
|
+
editedFields.current.add('acceptedPrivacy');
|
|
163
|
+
setAcceptedPrivacy(e.target.checked);
|
|
164
|
+
} }), label: t('Profile.ACCEPT_PRIVACY_LABEL') })), showCookies && (_jsx(FormControlLabel, { control: _jsx(Checkbox, { checked: acceptedCookies, onChange: (e) => {
|
|
165
|
+
editedFields.current.add('acceptedCookies');
|
|
166
|
+
setAcceptedCookies(e.target.checked);
|
|
167
|
+
} }), label: t('Profile.ACCEPT_COOKIES_LABEL') }))] }), showStatements && (_jsxs(Stack, { spacing: 1.25, sx: { mt: 1.5 }, children: [showPrivacy && (_jsxs(Accordion, { disableGutters: true, children: [_jsx(AccordionSummary, { children: _jsx(Typography, { variant: "body2", fontWeight: 600, children: t('Profile.PRIVACY_STATEMENT_TITLE', 'Privacy statement') }) }), _jsx(AccordionDetails, { children: loadingPrivacyStatement ? (_jsx(Typography, { variant: "body2", color: "text.secondary", children: t('Profile.STATEMENT_LOADING', 'Loading statement...') })) : (_jsx(Typography, { variant: "body2", sx: { whiteSpace: 'pre-wrap' }, children: privacyStatement || t('Profile.PRIVACY_STATEMENT_EMPTY', 'Privacy statement is currently unavailable.') })) })] })), showCookies && (_jsxs(Accordion, { disableGutters: true, children: [_jsx(AccordionSummary, { children: _jsx(Typography, { variant: "body2", fontWeight: 600, children: t('Profile.COOKIES_STATEMENT_TITLE', 'Cookie statement') }) }), _jsx(AccordionDetails, { children: loadingCookieStatement ? (_jsx(Typography, { variant: "body2", color: "text.secondary", children: t('Profile.STATEMENT_LOADING', 'Loading statement...') })) : (_jsx(Typography, { variant: "body2", sx: { whiteSpace: 'pre-wrap' }, children: cookieStatement || t('Profile.COOKIES_STATEMENT_EMPTY', 'Cookie statement is currently unavailable.') })) })] }))] }))] })), _jsx(Box, { sx: { mt: 2 }, children: _jsx(Button, { type: "submit", variant: "contained", disabled: saving, children: saving ? submitLabelLoading : submitLabel }) })] }));
|
|
133
168
|
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/components/ProfileComponent.jsx
|
|
2
|
-
import React, { useEffect, useState, useContext } from 'react';
|
|
2
|
+
import React, { useEffect, useState, useContext, useRef } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
Box,
|
|
5
5
|
Stack,
|
|
@@ -47,20 +47,42 @@ export function ProfileComponent({
|
|
|
47
47
|
const [lastName, setLastName] = useState('');
|
|
48
48
|
const [acceptedPrivacy, setAcceptedPrivacy] = useState(false);
|
|
49
49
|
const [acceptedCookies, setAcceptedCookies] = useState(false);
|
|
50
|
+
const previousUserIdentity = useRef(Symbol('uninitialized'));
|
|
51
|
+
const editedFields = useRef(new Set());
|
|
50
52
|
const [privacyStatement, setPrivacyStatement] = useState(privacyStatementText || '');
|
|
51
53
|
const [cookieStatement, setCookieStatement] = useState(cookieStatementText || '');
|
|
52
54
|
const [loadingPrivacyStatement, setLoadingPrivacyStatement] = useState(false);
|
|
53
55
|
const [loadingCookieStatement, setLoadingCookieStatement] = useState(false);
|
|
54
56
|
|
|
55
|
-
//
|
|
57
|
+
// Refresh server values for untouched fields, while keeping edits for this identity.
|
|
56
58
|
useEffect(() => {
|
|
59
|
+
const userIdentity = user
|
|
60
|
+
? (user.id ?? user.email ?? user.username ?? null)
|
|
61
|
+
: null;
|
|
62
|
+
|
|
63
|
+
if (previousUserIdentity.current !== userIdentity) {
|
|
64
|
+
previousUserIdentity.current = userIdentity;
|
|
65
|
+
editedFields.current = new Set();
|
|
66
|
+
}
|
|
67
|
+
|
|
57
68
|
if (user) {
|
|
58
69
|
setUsername(user.username ?? '');
|
|
59
70
|
setEmail(user.email ?? '');
|
|
60
|
-
setFirstName(user.first_name ?? '');
|
|
61
|
-
setLastName(user.last_name ?? '');
|
|
62
|
-
|
|
63
|
-
|
|
71
|
+
if (!editedFields.current.has('firstName')) setFirstName(user.first_name ?? '');
|
|
72
|
+
if (!editedFields.current.has('lastName')) setLastName(user.last_name ?? '');
|
|
73
|
+
if (!editedFields.current.has('acceptedPrivacy')) {
|
|
74
|
+
setAcceptedPrivacy(Boolean(user.accepted_privacy_statement));
|
|
75
|
+
}
|
|
76
|
+
if (!editedFields.current.has('acceptedCookies')) {
|
|
77
|
+
setAcceptedCookies(Boolean(user.accepted_convenience_cookies));
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
setUsername('');
|
|
81
|
+
setEmail('');
|
|
82
|
+
setFirstName('');
|
|
83
|
+
setLastName('');
|
|
84
|
+
setAcceptedPrivacy(false);
|
|
85
|
+
setAcceptedCookies(false);
|
|
64
86
|
}
|
|
65
87
|
}, [user]);
|
|
66
88
|
|
|
@@ -208,13 +230,19 @@ export function ProfileComponent({
|
|
|
208
230
|
<TextField
|
|
209
231
|
label={t('Profile.FIRST_NAME_LABEL')}
|
|
210
232
|
value={firstName}
|
|
211
|
-
onChange={(e) =>
|
|
233
|
+
onChange={(e) => {
|
|
234
|
+
editedFields.current.add('firstName');
|
|
235
|
+
setFirstName(e.target.value);
|
|
236
|
+
}}
|
|
212
237
|
fullWidth
|
|
213
238
|
/>
|
|
214
239
|
<TextField
|
|
215
240
|
label={t('Profile.LAST_NAME_LABEL')}
|
|
216
241
|
value={lastName}
|
|
217
|
-
onChange={(e) =>
|
|
242
|
+
onChange={(e) => {
|
|
243
|
+
editedFields.current.add('lastName');
|
|
244
|
+
setLastName(e.target.value);
|
|
245
|
+
}}
|
|
218
246
|
fullWidth
|
|
219
247
|
/>
|
|
220
248
|
</Stack>
|
|
@@ -232,7 +260,10 @@ export function ProfileComponent({
|
|
|
232
260
|
control={
|
|
233
261
|
<Checkbox
|
|
234
262
|
checked={acceptedPrivacy}
|
|
235
|
-
onChange={(e) =>
|
|
263
|
+
onChange={(e) => {
|
|
264
|
+
editedFields.current.add('acceptedPrivacy');
|
|
265
|
+
setAcceptedPrivacy(e.target.checked);
|
|
266
|
+
}}
|
|
236
267
|
/>
|
|
237
268
|
}
|
|
238
269
|
label={t('Profile.ACCEPT_PRIVACY_LABEL')}
|
|
@@ -244,7 +275,10 @@ export function ProfileComponent({
|
|
|
244
275
|
control={
|
|
245
276
|
<Checkbox
|
|
246
277
|
checked={acceptedCookies}
|
|
247
|
-
onChange={(e) =>
|
|
278
|
+
onChange={(e) => {
|
|
279
|
+
editedFields.current.add('acceptedCookies');
|
|
280
|
+
setAcceptedCookies(e.target.checked);
|
|
281
|
+
}}
|
|
248
282
|
/>
|
|
249
283
|
}
|
|
250
284
|
label={t('Profile.ACCEPT_COOKIES_LABEL')}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { afterEach, 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
|
+
|
|
7
|
+
vi.mock('react-i18next', () => ({
|
|
8
|
+
useTranslation: () => ({
|
|
9
|
+
t: (key, fallback) => ({
|
|
10
|
+
'Profile.USERNAME_LABEL': 'Username',
|
|
11
|
+
'Auth.EMAIL_LABEL': 'Email',
|
|
12
|
+
'Profile.FIRST_NAME_LABEL': 'First name',
|
|
13
|
+
'Profile.LAST_NAME_LABEL': 'Last name',
|
|
14
|
+
'Profile.ACCEPT_PRIVACY_LABEL': 'Accept privacy statement',
|
|
15
|
+
'Profile.ACCEPT_COOKIES_LABEL': 'Accept convenience cookies',
|
|
16
|
+
'Profile.SAVE_BUTTON': 'Save',
|
|
17
|
+
}[key] ?? fallback ?? key),
|
|
18
|
+
i18n: { exists: () => true },
|
|
19
|
+
}),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
vi.mock('../src/auth/authApi', () => ({
|
|
23
|
+
fetchCookieStatement: vi.fn(),
|
|
24
|
+
fetchPrivacyStatement: vi.fn(),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
import { AuthContext } from '../src/auth/AuthContext';
|
|
28
|
+
import { ProfileComponent } from '../src/components/ProfileComponent';
|
|
29
|
+
|
|
30
|
+
const theme = createTheme();
|
|
31
|
+
|
|
32
|
+
function makeUser(overrides = {}) {
|
|
33
|
+
return {
|
|
34
|
+
id: 17,
|
|
35
|
+
username: 'pat',
|
|
36
|
+
email: 'pat@example.com',
|
|
37
|
+
first_name: 'Pat',
|
|
38
|
+
last_name: 'Example',
|
|
39
|
+
accepted_privacy_statement: false,
|
|
40
|
+
accepted_convenience_cookies: true,
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function profileTree(user, onSubmit) {
|
|
46
|
+
return (
|
|
47
|
+
<ThemeProvider theme={theme}>
|
|
48
|
+
<AuthContext.Provider value={{ user, loading: false }}>
|
|
49
|
+
<ProfileComponent onSubmit={onSubmit} showStatements={false} />
|
|
50
|
+
</AuthContext.Provider>
|
|
51
|
+
</ThemeProvider>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function renderProfile(user, onSubmit = vi.fn()) {
|
|
56
|
+
const view = render(profileTree(user, onSubmit));
|
|
57
|
+
return {
|
|
58
|
+
...view,
|
|
59
|
+
onSubmit,
|
|
60
|
+
setUser: (nextUser) => view.rerender(profileTree(nextUser, onSubmit)),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
afterEach(() => {
|
|
65
|
+
cleanup();
|
|
66
|
+
vi.clearAllMocks();
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe('ProfileComponent user refresh', () => {
|
|
70
|
+
it('refreshes untouched editable fields while retaining the independently edited field', () => {
|
|
71
|
+
const { setUser } = renderProfile(makeUser());
|
|
72
|
+
fireEvent.change(screen.getByRole('textbox', { name: 'First name' }), {
|
|
73
|
+
target: { value: 'Local first' },
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
setUser(makeUser({
|
|
77
|
+
first_name: 'Server first',
|
|
78
|
+
last_name: 'Server last',
|
|
79
|
+
accepted_privacy_statement: true,
|
|
80
|
+
accepted_convenience_cookies: false,
|
|
81
|
+
}));
|
|
82
|
+
|
|
83
|
+
expect(screen.getByRole('textbox', { name: 'First name' }).value).toBe('Local first');
|
|
84
|
+
expect(screen.getByRole('textbox', { name: 'Last name' }).value).toBe('Server last');
|
|
85
|
+
expect(screen.getByRole('checkbox', { name: 'Accept privacy statement' }).checked).toBe(true);
|
|
86
|
+
expect(screen.getByRole('checkbox', { name: 'Accept convenience cookies' }).checked).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('preserves edits for the same identity and refreshes untouched fields', async () => {
|
|
90
|
+
const { onSubmit, setUser } = renderProfile(makeUser());
|
|
91
|
+
fireEvent.change(screen.getByRole('textbox', { name: 'First name' }), {
|
|
92
|
+
target: { value: 'Patricia' },
|
|
93
|
+
});
|
|
94
|
+
fireEvent.change(screen.getByRole('textbox', { name: 'Last name' }), {
|
|
95
|
+
target: { value: 'Newname' },
|
|
96
|
+
});
|
|
97
|
+
fireEvent.click(screen.getByRole('checkbox', { name: 'Accept privacy statement' }));
|
|
98
|
+
fireEvent.click(screen.getByRole('checkbox', { name: 'Accept convenience cookies' }));
|
|
99
|
+
|
|
100
|
+
setUser(makeUser({
|
|
101
|
+
username: 'pat-updated',
|
|
102
|
+
email: 'new@example.com',
|
|
103
|
+
first_name: 'Server first',
|
|
104
|
+
last_name: 'Server last',
|
|
105
|
+
accepted_privacy_statement: false,
|
|
106
|
+
accepted_convenience_cookies: true,
|
|
107
|
+
}));
|
|
108
|
+
|
|
109
|
+
await waitFor(() => {
|
|
110
|
+
expect(screen.getByRole('textbox', { name: 'Username' }).value).toBe('pat-updated');
|
|
111
|
+
expect(screen.getByRole('textbox', { name: 'Email' }).value).toBe('new@example.com');
|
|
112
|
+
});
|
|
113
|
+
expect(screen.getByRole('textbox', { name: 'First name' }).value).toBe('Patricia');
|
|
114
|
+
expect(screen.getByRole('textbox', { name: 'Last name' }).value).toBe('Newname');
|
|
115
|
+
expect(screen.getByRole('checkbox', { name: 'Accept privacy statement' }).checked).toBe(true);
|
|
116
|
+
expect(screen.getByRole('checkbox', { name: 'Accept convenience cookies' }).checked).toBe(false);
|
|
117
|
+
|
|
118
|
+
fireEvent.click(screen.getByRole('button', { name: 'Save' }));
|
|
119
|
+
await waitFor(() => {
|
|
120
|
+
expect(onSubmit).toHaveBeenCalledWith({
|
|
121
|
+
first_name: 'Patricia',
|
|
122
|
+
last_name: 'Newname',
|
|
123
|
+
accepted_privacy_statement: true,
|
|
124
|
+
accepted_convenience_cookies: false,
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('resets every field when the identity changes', async () => {
|
|
130
|
+
const { setUser } = renderProfile(makeUser());
|
|
131
|
+
fireEvent.change(screen.getByRole('textbox', { name: 'First name' }), {
|
|
132
|
+
target: { value: 'Unsaved first' },
|
|
133
|
+
});
|
|
134
|
+
fireEvent.change(screen.getByRole('textbox', { name: 'Last name' }), {
|
|
135
|
+
target: { value: 'Unsaved last' },
|
|
136
|
+
});
|
|
137
|
+
fireEvent.click(screen.getByRole('checkbox', { name: 'Accept privacy statement' }));
|
|
138
|
+
fireEvent.click(screen.getByRole('checkbox', { name: 'Accept convenience cookies' }));
|
|
139
|
+
|
|
140
|
+
setUser(makeUser({
|
|
141
|
+
id: 18,
|
|
142
|
+
username: 'lee',
|
|
143
|
+
email: 'lee@example.com',
|
|
144
|
+
first_name: 'Lee',
|
|
145
|
+
last_name: 'Newperson',
|
|
146
|
+
accepted_privacy_statement: false,
|
|
147
|
+
accepted_convenience_cookies: true,
|
|
148
|
+
}));
|
|
149
|
+
|
|
150
|
+
await waitFor(() => {
|
|
151
|
+
expect(screen.getByRole('textbox', { name: 'First name' }).value).toBe('Lee');
|
|
152
|
+
});
|
|
153
|
+
expect(screen.getByRole('textbox', { name: 'Username' }).value).toBe('lee');
|
|
154
|
+
expect(screen.getByRole('textbox', { name: 'Email' }).value).toBe('lee@example.com');
|
|
155
|
+
expect(screen.getByRole('textbox', { name: 'Last name' }).value).toBe('Newperson');
|
|
156
|
+
expect(screen.getByRole('checkbox', { name: 'Accept privacy statement' }).checked).toBe(false);
|
|
157
|
+
expect(screen.getByRole('checkbox', { name: 'Accept convenience cookies' }).checked).toBe(true);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('clears edits on logout before initializing the next login', async () => {
|
|
161
|
+
const { setUser } = renderProfile(makeUser());
|
|
162
|
+
fireEvent.change(screen.getByRole('textbox', { name: 'First name' }), {
|
|
163
|
+
target: { value: 'Unsaved first' },
|
|
164
|
+
});
|
|
165
|
+
fireEvent.click(screen.getByRole('checkbox', { name: 'Accept privacy statement' }));
|
|
166
|
+
fireEvent.click(screen.getByRole('checkbox', { name: 'Accept convenience cookies' }));
|
|
167
|
+
|
|
168
|
+
setUser(null);
|
|
169
|
+
expect(await screen.findByText('User not logged in.')).toBeTruthy();
|
|
170
|
+
setUser(makeUser({
|
|
171
|
+
first_name: 'Pat after login',
|
|
172
|
+
accepted_privacy_statement: false,
|
|
173
|
+
accepted_convenience_cookies: true,
|
|
174
|
+
}));
|
|
175
|
+
|
|
176
|
+
await waitFor(() => {
|
|
177
|
+
expect(screen.getByRole('textbox', { name: 'First name' }).value).toBe('Pat after login');
|
|
178
|
+
});
|
|
179
|
+
expect(screen.getByRole('checkbox', { name: 'Accept privacy statement' }).checked).toBe(false);
|
|
180
|
+
expect(screen.getByRole('checkbox', { name: 'Accept convenience cookies' }).checked).toBe(true);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('hydrates fields when the current user arrives asynchronously', async () => {
|
|
184
|
+
const { setUser } = renderProfile(null);
|
|
185
|
+
setUser(makeUser({ first_name: 'Hydrated' }));
|
|
186
|
+
|
|
187
|
+
await waitFor(() => {
|
|
188
|
+
expect(screen.getByRole('textbox', { name: 'First name' }).value).toBe('Hydrated');
|
|
189
|
+
expect(screen.getByRole('textbox', { name: 'Last name' }).value).toBe('Example');
|
|
190
|
+
});
|
|
191
|
+
expect(screen.getByRole('checkbox', { name: 'Accept privacy statement' }).checked).toBe(false);
|
|
192
|
+
expect(screen.getByRole('checkbox', { name: 'Accept convenience cookies' }).checked).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# UCM-AUTH-13 — Preserve profile edits during a same-person refresh
|
|
2
|
+
|
|
3
|
+
## A. Envelope — operator-approved correction
|
|
4
|
+
|
|
5
|
+
The Spesix first-steps browser trace proves that a background current-user response can arrive while the privacy checkbox is being edited. `ProfileComponent` currently resets every form field whenever the context user object changes, losing the person's input.
|
|
6
|
+
|
|
7
|
+
Expected outcome: preserve unsaved name and consent edits across a background refresh of the same person. Continue to refresh untouched fields from server data. A change of person, including logout followed by login, must initialize the form from the new person's data and never transfer consent or edits between people. Initial asynchronous user hydration must still work.
|
|
8
|
+
|
|
9
|
+
Scope: the existing shared ProfileComponent form state, targeted regression tests, a patch release, and subsequent Spesix dependency consumption. No change to authentication, permissions, consent policy, API payload shape, statement requests, layout or copy. No prototype; existing components and labels remain the visual specification. Token delta: EMPTY. Copy list: EMPTY.
|
|
10
|
+
|
|
11
|
+
Tier 3: shared-core and profile/consent surface. Operator explicitly approved correction, checking, publication and Spesix pin update in the Spesix orchestration chat on 2026-10-14.
|
|
12
|
+
|
|
13
|
+
Risks: stale pristine fields, failure to hydrate an initially absent user, or carrying unsaved consent into a different account. Tests to write: same-person object replacement preserves typed first/last names and both edited consent values in the submitted payload; untouched fields update; different identity and logout/login reset all fields; initial hydration works. Use real form controls and context rerenders.
|
|
14
|
+
|
|
15
|
+
## B. Implementation map — addressed to the implementer
|
|
16
|
+
|
|
17
|
+
Target: `C:/Users/Micha Bigler/Documents/webapps/ui-core-micha`.
|
|
18
|
+
|
|
19
|
+
Own only `src/components/ProfileComponent.jsx` and new `tests/ProfileComponent.refresh.test.jsx`. Read `tests/AccountPage.profileStatements.test.jsx` for the existing Vitest/jsdom, MUI and AuthContext conventions; `src/auth/AuthContext.jsx` for user identity shape; and `src/pages/AccountPage.jsx` for the caller contract. The library is not a joined application: one author owns component and focused unit tests.
|
|
20
|
+
|
|
21
|
+
The synchronization effect at ProfileComponent lines 56–65 currently depends on `[user]` and unconditionally assigns username, email, first/last name and both consent flags. The editable controls are near lines 205–250. `handleSubmit` near line 117 constructs the existing optional-fields payload; preserve it. Username/email remain read-only. Statement loading effects and visibility props are unchanged. AccountPage passes its profile update callback into this component. Work from these seams; do not broadly explore or change AuthContext or callers. Verify the identity contract before choosing synchronization behavior; do not use object reference as person identity.
|
|
22
|
+
|
|
23
|
+
### Required preamble and progress contract
|
|
24
|
+
|
|
25
|
+
The text above is the COMPLETE spec. Read root AGENTS.md and `.codex/skills/frontend-engineering/SKILL.md` only for conventions. Stay in scope. Do not edit WORK_ORDERS.md, MEMORY.md, package files, lockfiles, changelog, CI or unrelated files. Tools are only for reading the named context and editing assigned source/tests, plus one run of the new tests using the existing direct Vitest runner. No installs, package-manager commands, git, self-review or reviewer dispatch. Leave edits uncommitted. Your run is not the gate; the Orchestrator runs the affected set and independent reviews.
|
|
26
|
+
|
|
27
|
+
Use explicit UTF-8 file I/O. You share this checkout: preserve all other edits, including the foreign scratchpad directory. Native cwd bootstrap is the workspace root; assigned paths and test cwd must be absolute, without changing the shared process cwd. Start with `PLAN: ...`, report `PROGRESS: [n/total] ...` at meaningful actions with gaps below two minutes, and finish with exactly one `RESULT: DONE|BLOCKED <reason>`.
|
|
28
|
+
|
|
29
|
+
## C. Orchestrator only — NOT addressed to the implementer
|
|
30
|
+
|
|
31
|
+
> STOP here if you are the implementer. Everything below belongs to the Orchestrator after implementation. Do not execute it or dispatch anyone.
|
|
32
|
+
|
|
33
|
+
Dispatch a fresh native worker, explicit codex/gpt-6-luna, fresh context. Record requested and exposed effective metadata honestly; native provider/cwd/sandbox are unverified if not exposed.
|
|
34
|
+
|
|
35
|
+
Gate #1 is the explicit operator approval recorded above. Run independent envelope, regression, duplication and tests lenses plus UI and security concurrently on the corrected diff, read-only, configured Luna. Parent checks profile payload/identity boundaries in addition. Run focused ProfileComponent and existing AccountPage tests and the build. Root owns patch metadata and publication through the existing main-push workflow; no workflow dispatch is authorized by this file. Verify published version before changing Spesix pin, and inspect the intervening 3.12.0-to-current changes. No screenshots are needed for this state correction.
|
|
36
|
+
|
|
37
|
+
Foreign scratchpad is excluded. Commit only owned explicit paths. Update register with actual author, reviews, checks, release and consumption evidence; never claim completion before those checks.
|
|
38
|
+
|
|
39
|
+
### Execution and gate evidence
|
|
40
|
+
|
|
41
|
+
Fresh native `/root/ucm_auth13_author`, requested codex/gpt-6-luna/high, completed only the two assigned files; no git or package operations. Native provider/cwd/sandbox are not exposed. Author's one test attempt failed before collection on Windows Vitest cache EPERM. Parent authoritative elevated run passed: ProfileComponent.refresh (5) and existing AccountPage suites (6), 11 tests in 3 files. Parent added the independently edited-versus-pristine editable-fields case. Existing-dependency TypeScript build passed. Diff whitespace check passed. Root checked unchanged payload construction, statement effects, visibility flags and server-side authority; local edit tracking resets at identity/logout boundaries.
|
|
42
|
+
|
|
43
|
+
Six fresh CLI codex/gpt-6-luna read-only lanes ran concurrently against the same frozen diff, including package/changelog and complete component/caller context. Each exited 0 with requested model, openai provider and library cwd confirmed in banner. Envelope, duplication, UI and security reported No findings. Regression raised mutable fallback identity for consumers without IDs: not accepted as a defect in the supported AuthContext contract, which maps the immutable API id; without an immutable identity the component cannot establish that two changed fallback identities are the same person, and preserving consent across them would weaken account isolation. Tests raised that identity/logout/hydration guards also pass against the previous code: not accepted; these are explicitly required preservation cases and catch a naive freeze-after-first-hydration fix, while two same-person tests discriminate the actual defect and submitted consent values. Review: codex/gpt-6-luna; lenses envelope, regression, duplication, tests, ui, security; 2 raised, 0 accepted, worst accepted none. Some optional narrow reads failed process setup; reviewers completed from the full inline diff/context. UI attempted a second blocked narrow read; no command actually executed. No screenshots or image reviews.
|
|
44
|
+
|
|
45
|
+
npm version verified as 3.13.1 before reserving patch 3.13.2. Existing publish.yml builds and runs the library suite on main push before Trusted Publishing; no manual dispatch. Root inspected the consumer's intervening 3.12.0-to-3.13.1 code: AccountPage adds optional showProfileStatements default true, preserving existing controls/payload; SectionNav adds ListItem wrappers with existing selection/callbacks/labels. No changed exports or dependency requirements. Published patch and Spesix installed-package proof remain pending.
|
|
@@ -111,3 +111,15 @@ Parent gate: 21 tests in SectionNav.test.jsx, AccountPage.test.jsx and
|
|
|
111
111
|
AccountPage.profileStatements.test.jsx passed with maxWorkers=2; TypeScript build exit 0.
|
|
112
112
|
Publish workflow's full suite is justified by shared-core consumer ripple. App pin and fresh
|
|
113
113
|
consumer capture remain pending after the reviewed core source/release lands.
|
|
114
|
+
|
|
115
|
+
Closing consumer proof: published3.13.1 verified by workflow38435441867,
|
|
116
|
+
744tests89files plus build and npm Trusted Publishing successful. Innoservice
|
|
117
|
+
exact pin3.13.1 is in sourcec527918, CI38444366349. Both source-matching account
|
|
118
|
+
captures375/1280 have no axe failures, no console/network failures, and all11
|
|
119
|
+
rubric items yes (22answers). Parent inspected both actual account images: same
|
|
120
|
+
profile/security/admin/help navigation and consent controls, readable at both
|
|
121
|
+
widths. Three previous kit list violations are gone. This completes the scoped
|
|
122
|
+
consumer gate; unrelated Organisation-policy axe failures in that app candidate
|
|
123
|
+
remain owned by INNO-FIX-11 and do not reopen the kit list correction.
|
|
124
|
+
Review remains the named five independent readonly Luna source passes above,
|
|
125
|
+
1deduplicated raised0accepted,worst accepted none; no source changes afterward.
|