13 — State Management (Users Module)
- 1. Cubit map
- 2. UsersListCubit
- 3. UserDetailCubit
- 4. UserFormCubit (create + edit)
- 5. PreferencesCubit
- 6. BulkImportCubit — the import state machine
- 7. SelfProfileCubit
- 8. MembershipCubit
- 9. Cross-cutting
Per-screen state on top of 00-shared/06_State_Management.md (Bloc/Cubit proposal). Cubits are
(planned); repository/API facts are exact. Special attention: the long-running import state machine (06 §6).
1. Cubit map
UsersListCubit → SS1
UserDetailCubit → SS3
UserFormCubit → SS2/SS5
PreferencesCubit → SS6
SelfProfileCubit → SS7
BulkImportCubit → SS4 (owns the import state machine, §6)
MembershipCubit → SS3 Membership tab (roles)
SessionCubit → shared (self `:id`, permissions)
2. UsersListCubit
- State:
LoadState(00-shared/06 §3.1) +items,page,limit=20,sort=-createdAt(default,users.service.ts:103),q,totalItems,hasNext, plusrolesMap(fromGET /rbac/membersjoined by userId —rbac.controller.ts:57-61). - Events:
Load,Refresh(reset page 1, bypass cache),LoadMore,ChangeQuery(q)(debounced 300 ms, resets page),ChangeStatusFilter,ChangeRoleFilter,Retry. - Server contract: envelope
data[]+meta {page,limit,totalItems, totalPages,hasNext,hasPrevious}(pagination-query.dto.ts:32-55). - Status/role chips filter loaded pages only (no server params —
users.service.ts:90-115; OQ-9) — state carries aclientFilterActiveflag for the honest caption. - Cache:
sl:{tenant}:users:{query}TTL 5 min;Refreshbypasses. - Row action outcomes: after
DELETE /users/:id(soft) remove row locally + snackbar (safe, idempotent); afterPATCH statusupdate badge locally, rollback on error.
3. UserDetailCubit
- State:
LoadState+user,member?(fromGET /rbac/membersjoin),preferences(lazy via tab). - 404 handling:
RESOURCE_NOT_FOUND→ empty-state "User not found" (cross-tenant/deleted/erased —base.repository.ts:20-30). - After Edit save: replace
userwith PATCH response (server recomputeddisplayName—users.service.ts:136-139). - Erasure: sets flag
erased→ navigates back, list refresh.
4. UserFormCubit (create + edit)
- Shared by SS2/SS5; mode
create | edit. - Field-level state mirrors
CreateUserDto/UpdateUserDto(08_Form_Specifications.md §1-§2); dirty tracking for unsaved-changes dialog. - Submit flow (create):
POST /users→ on success, if roles selected →POST /rbac/members; second call failure → statecreatedWithoutRoles→ persistent retry banner (membership unique index makes retry safe —organization-member.schema.ts:48). - Errors: 400
VALIDATION_ERROR.detailsmapped per field (00-shared/07 §3); 409 email/phone → inline conflict state + "search existing" affordance; 429 → backoff copy; 5xx → generic + requestId. - 5xx on create may be a concurrent duplicate (unique index race —
user.schema.ts:83; OQ-14) — copy "someone may already exist with this email" + refresh list.
5. PreferencesCubit
- State:
LoadState+ fullpreferencesobject (GET→?? {},users.service.ts:168-171). - Optimistic toggles per
00-shared/06 §3.5(safe switches), but submit sends the complete merged object — full-replace contract (users.service.ts:161-163); rollback on failure. - Debounced auto-save
(proposed)— every toggle change PATCHes the full object; avoid race by serializing saves (queue one in-flight save).
6. BulkImportCubit — the import state machine
Synchronous backend (users.service.ts:233-282) ⇒ the machine is
client-staged, with honest labels (never fake server progress — OQ-6):
sealed class ImportState {
Idle
ParsingLocal // isolate parse: rows parsed so far / total (real)
PreviewReady // headerMap, rows[], warnings[]
Uploading // bytes sent / total (real, dio onSendProgress)
ServerProcessing // indeterminate: "Server is importing N rows"
Succeeded // imported, errors[]
Failed // ApiException (network/429/5xx), retryable
}
| Event | Transition | Notes |
|---|---|---|
PickFile | Idle → ParsingLocal | reject non-CSV/size > 2 MB (OQ-10) |
ParseProgress(n,total) | ParsingLocal (re-emit) | isolate posts every ~200 rows |
ParseDone | → PreviewReady | warnings: missing optional cols, quotes (naive parser — users.service.ts:251; OQ-7), in-file dup emails (:260-263), Unknown fallback preview (:265-266) |
ConfirmImport | → Uploading | multipart field file (users.controller.ts:104-110) |
UploadDone | → ServerProcessing | request in flight; elapsed timer |
ImportDone(payload) | → Succeeded | {imported, errors} (users.service.ts:281) |
ImportError(e) | → Failed | retry allowed — server dedups emails (:260-263), retry never duplicates |
Cancel | Uploading → Idle | local only; ServerProcessing cannot cancel |
ImportMore | Succeeded/Failed → Idle | keep parsed template |
- State survival: cubit lives above the route (registered at shell
scope) so leaving the wizard preserves preview/result; navigation back
reuses state (
04_IA §7). - Result view derives:
errors.isEmpty → success; else counts +ImportErrorListrows; "Download errors" client-side CSV export. - Concurrency: one import at a time per tenant UI; block the FAB/route
guard while
Uploading/ServerProcessing(anti-double-submit,00-shared/08 §6). Two tabs importing the same file: server dedups; both see consistenterrors(each row's email check is sequential —users.service.ts:250-279). - Idempotency key:
Idempotency-Keysupport unconfirmed (B6); safe because duplicates are rejected not re-created.
7. SelfProfileCubit
- Resolves
:idfrom JWTsub(auth.service.ts:461); stateLoadState- user; avatar upload sub-state (uploading → success
{avatarFileId}/ error) —users.service.ts:230.
- user; avatar upload sub-state (uploading → success
- Permission-derived visibility: edit/delete/erase controls rendered via
PermissionScoped(07 §11) with the client permission set (00-shared/05 §9).
8. MembershipCubit
- Loads
GET /rbac/members+GET /rbac/roles(rbac.controller.ts:27-31,57-61); role edit →PATCH /rbac/members/:id(:69-73); remove →DELETE /rbac/members/:id(:75-79). - Not org_admin (403) → empty state with "no role visibility" copy (OQ-15).
- Copy rule: "Role changes apply on next sign-in" (JWT embeds roles at issue
—
auth.service.ts:460-476).
9. Cross-cutting
- Auth state (
00-shared/06 §3.6): any 401 → single-flight refresh; on failure, session-expiry overlay preserving list query. - Connectivity (
00-shared/06 §3.7): offline → cached list + banner; writes blocked (no module offline queue); import wizard blocked at step 1 with guidance. - Permission changes → route rebuild (
00-shared/05 §9): losinguser.*mid-session removes users routes. - Analytics
(proposed)(10_QA_Baseline.md §8):users.list.search,users.create.submit,users.import.start|complete|failure,users.erasure.confirm,users.status.change.