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 (Organizations Module)

Cubit architecture per 00-shared/06. One cubit per screen; base LoadState (Initial/Loading/Success/Error(ApiException)), PaginatedListMixin, cache + SWR, optimistic updates, connectivity. All (proposed) client design.


1. Cubit map

CubitScreen(s)Data sourcesNotes
OrgDetailCubitS1 Overview, S8 detailE3 GET /organizations/:idshared by self + platform (id from route/context); TTL 5 min SWR
OrgEditCubitS2 Edit, S3 Branding, S8 create/editE1/E4 + storage upload (planned)form state not cached; submit → full DTO
OrgSettingsCubitS4 Settings tabsE6 GET :id/settings + E7 PATCH; reference tabs E10full-object save; per-tab dirty flags
FeatureFlagsCubitS5E8 map + E11 catalogmerged view; optimistic toggle; rollback
MembersCubitS6E13 rbac endpoints + users moduleinvite/role/remove
TenantsCubitS7E2 listPaginatedListMixin<Organization>; q/sort/filter state
OnboardingCubitglobalorg status + registerdrives banner + registration flow (forward-looking)

2. State machine (generic per 00-shared/06 §3.1)

stateDiagram-v2
    [*] --> Initial
    Initial --> Loading: Load
    Loading --> Success: load(org) ok
    Loading --> Error: ApiException
    Error --> Loading: Retry
    Success --> Success: Refresh / mutate (reconcile)
    Success --> Error: mutation fails → rollback (toggle) or snackbar (form)
    Success --> Loading: pull-to-refresh (bypass cache)

3. OrgDetailCubit

sealed class OrgDetailState {}
class OrgDetailInitial extends OrgDetailState {}
class OrgDetailLoading extends OrgDetailState {}
class OrgDetailSuccess extends OrgDetailState {
  final Organization org;         // domain model
  final bool stale;               // served from cache while revalidating
}
class OrgDetailError extends OrgDetailState { final ApiException e; }

Events: LoadOrg(id), RefreshOrg(), OrgUpdated(org) (post-save reconcile from Edit/Branding/Settings cubits via shared bus or direct repository cache invalidation — prefer cache-bust key + refetch).

Load flow: repo.getOrg(id) → cache hit (sl:{tenant}:org:{id}, TTL 5 min) → emit Success(stale:true) + background refetch → Success(stale:false). Miss → Loading → Success/Error. Self-view (OQ-1): loadSelf() resolves id via (planned) /organizations/me; fallback (documented stopgap) list-lookup by slug — flagged in code comment.

4. OrgEditCubit

State: form (OrgFormModel), submitting, fieldErrors, conflicts {slug?, domain?}, saveResult. Events: InitForm(org), FieldChanged(field, value), SlugPreview(name), Submit(), Discard(). Reducers: submit → submitting → on 200 Success (emit OrgUpdated for dependents) → snackbar; on 409 → conflicts[field] inline, no form reset; on 400 → fieldErrors mapped from envelope details[] (http-exception.filter.ts:103-107).

sequenceDiagram
    participant W as OrgEditCubit
    participant R as OrganizationsRepository
    participant API as PATCH /organizations/:id
    W->>W: Submit()
    W->>R: update(id, dto)
    alt 200
        API-->>R: updated doc
        R-->>W: Success(org)
        W-->>UI: snackbar + OrgUpdated
    else 409
        R-->>W: ApiException(code=DUPLICATE_RESOURCE)
        W-->>UI: conflicts[field] + scroll-to-field
    else 400
        W-->>UI: fieldErrors from details[]
    end

5. OrgSettingsCubit

State: initial (per tab), dirty {attendance, academic, theme} (bitmap), saving, error. Events: LoadSettings(id), TabChanged(tab), FieldChanged(tab, field, value), SaveAll(), RevertTab(tab). Key reducer: SaveAll() always serializes the merged full object from lastServerSettings + dirty fields (full-replace safety — organizations.service.ts:134). Success → clear dirty + snackbar. Failure → keep dirty + error snackbar (retry safe).

stateDiagram-v2
    [*] --> Idle
    Idle --> Loading: LoadSettings
    Loading --> Ready: settings fetched
    Ready --> Ready: FieldChanged (dirty[tab]=true)
    Ready --> Saving: SaveAll
    Saving --> Ready: 200 → dirty cleared, snackbar
    Saving --> Ready: 400 → field errors on tab (dirty kept)
    Saving --> Ready: network → error snackbar + Retry

6. FeatureFlagsCubit

State: flags: Map<String, FlagView> where FlagView {key, label?, description?, module?, enabled, pending}; loading, error. Events: Load(), Toggle(key), Retry(key), DeleteFlag(key). Optimistic flow: Toggle → set enabled=!enabled, pending=true → repo setFlagMap(fullNewMap) (E9) → on 200 reconcile + pending=false + lightImpact; on error → rollback + pending=false + error snackbar with Retry (retry re-submits last-intended map). Map = merge(catalog keys, server map keys).

stateDiagram-v2
    [*] --> Idle
    Idle --> Loading: Load
    Loading --> Ready: map + catalog merged
    Ready --> PendingToggle: Toggle(key)
    PendingToggle --> Ready: 200 reconcile
    PendingToggle --> Ready: error → rollback + snackbar(Retry)

7. TenantsCubit (PaginatedListMixin<Organization>)

State: page (1), limit (20), sort (-createdAt), q, statusFilter (proposed), items, hasNext, isLoadingMore, loadState. Events: LoadFirst(), LoadMore(), Search(q), ChangeSort(sort), ChangeFilter(status), Refresh(), TenantDeleted(id) (remove row + refetch meta). Contract mirrors API exactly (pagination-query.dto.ts:5-54): on Search/ChangeSort/Filter → reset page=1, clear items, fetch; hasNext drives infinite scroll; Refresh bypasses cache.

sequenceDiagram
    participant C as TenantsCubit
    participant R as OrganizationsRepository
    participant API as GET /organizations?page&limit&sort&q
    C->>C: Search("spring")
    C->>R: list(page:1, q:"spring", sort:"-createdAt")
    API-->>R: data[] + meta {totalItems,totalPages,hasNext,...}
    R-->>C: Success(items, meta)
    C->>C: hasNext=true → LoadMore on scroll bottom
    C->>R: list(page:2, ...)

8. Caching & staleness (module TTLs)

DataCache keyTTLNotes
Org detailsl:{tenant}:org:{id}5 min SWRRefreshIndicator bypasses
Tenants pagesl:{tenant}:orgs:{page}:{limit}:{sort}:{q}5 minfull query key (includes filter (proposed))
Settings (embedded)no client cacheform data; refetch on entry
Flag mapsl:{tenant}:flags5 mininvalidated on toggle success
Flag catalogsl:{tenant}:flag-catalog24 h (reference)labels/descriptions
Memberssl:{tenant}:members5 mininvalidated on invite/role change

9. Realtime & cross-cubit

  • No org-config WS topics today; (forward-looking): subscribe org.branding.updated, org.feature-flags.updated → invalidate caches + refetch (B2: WS protocol unverified).
  • Cross-cubit invalidation: after E4/E7/E9 success → bump org:{id} cache version; Overview re-fetches on focus (AppLifecycle/route pop).
  • FeatureFlagsCubit is the module-level toggle source for other modules' gating (00-shared/06 §4).

10. Testing hooks

  • Pure-Dart cubits, mocked repositories; widget tests per state machine (Loading/Success/Error/Empty) + optimistic rollback (00-shared/06 §6).
  • Golden: full-object-save form with 3 tabs dirty states.