State Management
The Lens frontend uses no external state library. All state is managed through React’s built-in primitives: useState, useCallback, useMemo, and useRef. This document describes the data flow architecture, state ownership, and persistence strategies.
LensView: The Central State Owner
Section titled “LensView: The Central State Owner”LensView (at src/pages/LensView.tsx) is the single source of truth for all data and UI state within the portfolio view. It delegates state management to 12+ custom hooks in src/pages/lens-view/, each owning a specific concern:
LensView├── useLensDataLoader → nodes, views, syncAgents, lensName, permissions├── useLensViewDraft → columns, density, formatting, pipeline, gantt config├── useLensContextBootstrap → field metadata, priorities, user prefs, admin status├── useLensDialogState → open/close booleans for all dialogs├── useLensIncrementalSync → realtime updates, incremental diffing, animations├── useLensRealtimeController → Forge Realtime subscription lifecycle├── useLensSchedulingController → auto-schedule coordination├── useLensSelection → row selection state├── useLensInspectorState → inspector panel, section expand/collapse├── useLensExport → export logic├── useLensCreateController → inline issue creation├── useSearch → search term and match state├── useQuickFilters → quick filter chips├── useColumnFilters → per-column filter state├── useDragDrop → DnD state and handlers├── useKeyboardShortcuts → keyboard bindings├── useJobStatus → async job polling├── useRetryQueue → failed operation retry├── useNetworkStatus → online/offline detectionData Loading
Section titled “Data Loading”Initial Load Cycle
Section titled “Initial Load Cycle”User navigates to /lens/:id │ ▼LensView mounts → useLensDataLoader fires │ ├─ Check prefetch cache (getPrefetchedView) │ └─ Hit? Use cached data, skip API call │ ├─ api.getLensViewByNumericId(numericId) │ Returns: { lensId, rows, totalCount, lensName, │ views, syncAgents, userPermissionLevel, ownerAccountId } │ ├─ api.initLensContext(lensId) │ Returns: { fields, priorities, hierarchy, preferences, isAdmin } │ ▼State populated: nodes ← rows (HierarchyNode[]) views ← ViewConfig[] (apply default view) syncAgents ← sync agent configs fieldMetadata ← field definitions for editor type resolution columns ← from default view config density ← from default view config formattingRules ← from default view configPrefetch System
Section titled “Prefetch System”utils/prefetch.ts provides hover-to-prefetch for lens cards:
// On mouseenter of a lens card:prefetchLensView(lensId); // Starts after 150ms dwell
// On mouseleave:cancelPrefetchLensView(lensId); // Cancels if dwell threshold not met
// On navigation:const cached = getPrefetchedView(lensId); // Returns data or nullConfiguration:
- TTL: 30 seconds — cached data expires after this period
- Dwell: 150ms — hover must persist before fetch starts
- Concurrency: 2 simultaneous prefetches max
- Cache limit: 5 entries (LRU eviction by
lastAccessedAt) - Version tracking: Bumped on invalidation to prevent stale responses from landing
Row Construction
Section titled “Row Construction”The backend returns nodes in SQL order (depth ASC, position ASC), which is breadth-first-like. The frontend converts this to depth-first tree-walk order for rendering.
buildFlatRows()
Section titled “buildFlatRows()”export function buildFlatRows(nodes: HierarchyNode[]): FlatRow[]This function:
- Calls
treeWalkOrder(nodes)to reorder nodes into depth-first traversal - Resolves orphan promotion — nodes whose parent is missing are promoted to root
- Maps each
HierarchyNodeto aFlatRowwith flattened issueData fields
Critical invariant: Any component rendering content aligned with grid rows (Gantt bars, dependency arrows, row highlights) MUST use the same depth-first tree-walk ordering. The Gantt’s extractGanttItems() uses the same treeWalkOrder() function internally.
FlatRow Structure
Section titled “FlatRow Structure”interface FlatRow { id: string; node_type: string; // 'issue' | 'flex' | 'generator_group' | 'milestone' jira_issue_key: string | null; flex_name: string | null; depth: number; parent_id: string | null; // Effective parent (null for orphan-promoted nodes) position: number; hasChildren: boolean; milestone_date: string | null; // All issueData fields spread flat: summary: string | null; status_name: string | null; assignee_display_name: string | null; priority_name: string | null; due_date: string | null; start_date: string | null; story_points: number | null; // ... (all CachedIssue fields)}View State Management
Section titled “View State Management”useLensViewDraft
Section titled “useLensViewDraft”Manages the “draft” state of the current view configuration — what the user sees vs. what is saved. Tracks dirty state for save/revert.
Managed state:
| State | Type | Persisted In |
|---|---|---|
viewId | string | Forge SQL (views table) |
columns | ColumnConfig[] | Forge SQL |
density | DensityMode | Forge SQL |
wrapRows | boolean | Forge SQL |
formattingRules | FormattingRule[] | Forge SQL |
assigneeDisplayMode | AssigneeDisplayMode | Forge SQL |
columnsFrozen | boolean | Forge SQL |
ganttEnabled | boolean | Forge SQL |
ganttWidth | number | Forge SQL |
ganttConfig | GanttConfig | Forge SQL |
criticalPathEnabled | boolean | Forge SQL |
resourcePanelEnabled | boolean | Forge SQL |
autoScheduleEnabled | boolean | Forge SQL |
sortConfig | SortConfig | Forge SQL |
pipeline | PipelineOperation[] | Forge SQL |
progressMode | ProgressMode | Forge SQL |
Methods:
applyViewState(view)— loads a saved view config into the drafthandleSaveView()— persists the current draft to the backendhandleRevertView()— discards changes and reloads from saved stateisDirty— boolean indicating unsaved changes
View Persistence
Section titled “View Persistence”Views are stored in Forge SQL (views table) with the full configuration serialized as JSON columns. When a user switches views, the frontend calls applyViewState() which replaces all draft state in a single batch.
Local Persistence (localStorage)
Section titled “Local Persistence (localStorage)”Some UI preferences are stored in localStorage with a foundation- prefix:
| Key Pattern | Data |
|---|---|
foundation-{lensId}-inspector-open | Inspector panel open state |
foundation-{lensId}-section-{sectionId} | Accordion section expanded state |
foundation-sort-presets | Sort/group presets |
foundation-density | Last-used density mode |
localStorage is used for ephemeral UI state that doesn’t warrant a server round-trip and shouldn’t be shared between users.
Edit Cycle (Inline Editing)
Section titled “Edit Cycle (Inline Editing)”1. User double-clicks cell │ ▼2. AG Grid opens cell editor (isEditingRef.current = true) │ ▼3. User commits edit (Enter, select, or click-outside) │ ▼4. onCellEdit callback fires │ ├─ Optimistic update: setNodes() with new value │ └─ Grid immediately shows the new value │ ├─ api.updateField(lensId, issueKey, field, value) │ │ │ ├─ Success: Backend updates Jira + cache │ │ └─ Realtime event may trigger smartRefresh │ │ │ └─ Failure: Revert optimistic update │ └─ toast.editFailed(issueKey, field, error) │ ▼5. isEditingRef.current = falseKey detail: isEditingRef is a useRef, not useState. This prevents re-renders while AG Grid is in edit mode, which would destroy the active editor. See Inline Editing for the full explanation.
Incremental Sync (Realtime Updates)
Section titled “Incremental Sync (Realtime Updates)”useLensIncrementalSync handles live updates from other users and background operations:
Forge Realtime event arrives (hierarchy_changed, field_updated, etc.) │ ▼handleRealtimeFieldUpdate(event) │ ├─ If currently editing → defer update │ ├─ smartRefresh() → api.getLensDiff(lensId, lastJobId) │ Returns: { added, moved, removed, hidden, unhidden } │ ├─ Apply diff to nodes state: │ ├─ New rows → newRowIds (green highlight animation) │ ├─ Updated rows → updatedRowIds (blue highlight animation) │ ├─ Changed fields → changedFields map (cell-level animation) │ └─ Removed rows → removingRowIds (strikethrough + slide animation) │ ▼Animation timers clear highlight sets after 2-3 secondsPipeline Engine
Section titled “Pipeline Engine”The pipeline is a composable chain of sort/group/filter operations stored in the view config:
type PipelineOperation = | SortOperation // { type: 'sort', field, direction } | GroupOperation // { type: 'group', field } | FilterOperation // { type: 'filter', field, operator, value } | CollapseOperation // { type: 'collapse', depth }HierarchyGrid executes the pipeline via executePipeline(nodes, pipeline) from utils/pipelineEngine.ts. The pipeline runs client-side on the flat row data after buildFlatRows().
Toast Notification System
Section titled “Toast Notification System”utils/toasts.ts wraps Forge bridge’s showFlag() for standardized notifications:
import { toast } from '../utils/toasts';
toast.success('Saved'); // Auto-dismisstoast.error('Failed', 'Details here'); // Stickytoast.warning('Rate limit approaching'); // Auto-dismisstoast.editFailed('PROJ-123', 'summary', 'err'); // Formatted edit failuretoast.handleResult(apiResult, 'Success msg'); // Auto-detect success/errortoast.permissionDenied(); // Standard permission errortoast.networkError(); // Standard network errorAG Grid rowData/columnDefs Construction
Section titled “AG Grid rowData/columnDefs Construction”rowData
Section titled “rowData”// In HierarchyGrid:const flatRows = useMemo(() => buildFlatRows(nodes), [nodes]);
// Pipeline transforms (sort/group/filter) applied next:const processedRows = useMemo( () => executePipeline(flatRows, pipeline), [flatRows, pipeline]);
// Collapsed rows filtered:const visibleRows = useMemo( () => filterCollapsedRows(processedRows, gridCollapsedIds), [processedRows, gridCollapsedIds]);
// visibleRows becomes AG Grid's rowDatacolumnDefs
Section titled “columnDefs”// In HierarchyGrid:const columnDefs = useMemo( () => buildColumnDefs(columns, { density, wrapRows, formattingRules, assigneeDisplayMode, fieldTypeMap, fieldMetadata, dateFormatMode, // ... 15+ configuration options }), [columns, density, wrapRows, /* ... */]);buildColumnDefs() (in columns.tsx) maps each ColumnConfig to an AG Grid ColDef with:
- Appropriate cell renderer based on field type
- Cell editor based on field type (for inline editing)
- Value getters that extract from the flat row data
- Width, wrap, and formatting configuration
- Column header menu integration
Keyboard Shortcuts
Section titled “Keyboard Shortcuts”useKeyboardShortcuts registers global keyboard handlers:
| Shortcut | Action |
|---|---|
Delete / Backspace | Delete selected nodes |
Ctrl/Cmd+A | Select all rows |
Ctrl/Cmd+K | Open command palette |
Ctrl/Cmd+I | Toggle inspector panel |
Ctrl/Cmd+F | Focus search input |
Shift+C | Select children (when selection exists) |
Ctrl+Shift+1-5 | Switch density mode |
Enter (on row) | Open inline create below |
ArrowLeft/Right | Expand/collapse node |
The hook uses a useRef for handlers to keep the event listener stable across re-renders.