Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

13 — State Management (Users Module)

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, plus rolesMap (from GET /rbac/members joined 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 a clientFilterActive flag for the honest caption.
  • Cache: sl:{tenant}:users:{query} TTL 5 min; Refresh bypasses.
  • Row action outcomes: after DELETE /users/:id (soft) remove row locally + snackbar (safe, idempotent); after PATCH status update badge locally, rollback on error.

3. UserDetailCubit

  • State: LoadState + user, member? (from GET /rbac/members join), 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 user with PATCH response (server recomputed displayNameusers.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 → state createdWithoutRoles → persistent retry banner (membership unique index makes retry safe — organization-member.schema.ts:48).
  • Errors: 400 VALIDATION_ERROR.details mapped 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 + full preferences object (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
}
EventTransitionNotes
PickFileIdle → ParsingLocalreject non-CSV/size > 2 MB (OQ-10)
ParseProgress(n,total)ParsingLocal (re-emit)isolate posts every ~200 rows
ParseDone→ PreviewReadywarnings: missing optional cols, quotes (naive parser — users.service.ts:251; OQ-7), in-file dup emails (:260-263), Unknown fallback preview (:265-266)
ConfirmImport→ Uploadingmultipart field file (users.controller.ts:104-110)
UploadDone→ ServerProcessingrequest in flight; elapsed timer
ImportDone(payload)→ Succeeded{imported, errors} (users.service.ts:281)
ImportError(e)→ Failedretry allowed — server dedups emails (:260-263), retry never duplicates
CancelUploading → Idlelocal only; ServerProcessing cannot cancel
ImportMoreSucceeded/Failed → Idlekeep 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 + ImportErrorList rows; "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 consistent errors (each row's email check is sequential — users.service.ts:250-279).
  • Idempotency key: Idempotency-Key support unconfirmed (B6); safe because duplicates are rejected not re-created.

7. SelfProfileCubit

  • Resolves :id from JWT sub (auth.service.ts:461); state LoadState
    • user; avatar upload sub-state (uploading → success {avatarFileId} / error) — users.service.ts:230.
  • 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): losing user.* 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.