Inline Editing
The Lens app supports inline editing of Jira issue fields directly within AG Grid cells. This document covers the cell editor architecture, all supported editors, and the critical workarounds required for AG Grid editing inside a Forge iframe.
Architecture Overview
Section titled “Architecture Overview”User double-clicks cell │ ▼AG Grid opens cell editor component │ ├─ Editor renders inline (inside the cell) │ └─ For <select>-based editors: native OS dropdown escapes overflow │ ├─ User makes selection or types value │ ├─ Editor calls props.onValueChange(newValue) │ └─ Optional: stores metadata on props.data (e.g., _statusTransition, _assigneeSelection) │ ├─ props.stopEditing() or Enter/Escape/click-outside │ ▼AG Grid fires onCellValueChanged │ ├─ Grid's onCellEdit callback │ ├─ Optimistic local update │ └─ api.updateField(lensId, issueKey, field, value) │ ▼Backend updates Jira → updates cache → publishes realtime eventCell Editor Registration
Section titled “Cell Editor Registration”All editors are registered in HierarchyGrid.tsx via AG Grid’s components prop:
const components = useMemo(() => ({ TextEditor, NumberInput, DatePicker, DateTimePicker, StatusDropdown, PriorityDropdown, AssigneePicker, ReporterPicker, SprintDropdown, LabelsEditor, SelectDropdown, MultiSelectDropdown, CascadingSelectEditor, OptionSelectEditor, ComponentPicker, VersionPicker, MultiVersionPicker, GroupPicker, MultiGroupPicker, CustomUserPicker, MultiUserPicker, UrlTextEditor, IssueLinkEditor, FlaggedToggleEditor,}), []);Column definitions assign editors by field type in columns.tsx:
// columns.tsx — simplified example{ field: 'summary', cellEditor: 'TextEditor', editable: true,}{ field: 'status_name', cellEditor: 'StatusDropdown', editable: true,}The useGridCellEditor Hook
Section titled “The useGridCellEditor Hook”Every editor calls useGridCellEditor({}) from ag-grid-react. This hook:
- Registers the component as a valid AG Grid cell editor
- Provides the AG Grid cell editor lifecycle
import { useGridCellEditor } from 'ag-grid-react';
const MyEditor = (props: any) => { useGridCellEditor({}); // ... render editor UI};Editor Catalog
Section titled “Editor Catalog”TextEditor
Section titled “TextEditor”File: InlineEdit/TextEditor.tsx
Fields: Summary, text custom fields
Pattern: <input> with auto-focus and select-all on mount
Key behavior:
- Stops propagation of Backspace/Delete to prevent AG Grid navigation during editing (QA-BUG-73)
- Calls
props.onValueChange()on every keystroke
NumberInput
Section titled “NumberInput”File: InlineEdit/NumberInput.tsx
Fields: Story points, numeric custom fields
Pattern: <input type="number">
DatePicker
Section titled “DatePicker”File: InlineEdit/DatePicker.tsx
Fields: Due date, start date, date custom fields
Pattern: <input type="date"> — native browser date picker
Uses native HTML date input, which renders the OS-level date picker. This escapes the AG Grid overflow naturally.
DateTimePicker
Section titled “DateTimePicker”File: InlineEdit/DateTimePicker.tsx
Fields: DateTime custom fields
Pattern: Two inputs — date + time
StatusDropdown
Section titled “StatusDropdown”File: InlineEdit/StatusDropdown.tsx
Fields: Status
Pattern: <select> with Jira workflow transitions
On mount:
- Calls
api.getTransitions(issueKey, lensId)to load available transitions - Shows loading state while fetching
- Renders
<select>with transition options
On selection:
- Stores transition metadata on
props.data._statusTransition - Sets value to
'__STATUS_UPDATING__'(sentinel) - Calls
props.stopEditing()
The onCellEdit handler in HierarchyGrid detects the sentinel and reads the transition metadata to execute the status change.
PriorityDropdown
Section titled “PriorityDropdown”File: InlineEdit/PriorityDropdown.tsx
Fields: Priority
Pattern: <select> with priority options from context
AssigneePicker
Section titled “AssigneePicker”File: InlineEdit/AssigneePicker.tsx
Fields: Assignee
Pattern: Search input + custom dropdown (NOT a native <select>)
Features:
- Avatar display for current assignee
- Debounced user search (300ms, minimum 2 characters)
- “Unassigned” and “Automatic” default options
- Clear button
- Project-scoped search (
projectKeyfrom row data)
This editor uses a custom dropdown (DOM elements with position: fixed) instead of a native <select>, because the assignee picker needs avatars and search functionality that HTML <select> cannot provide.
Selection metadata is stored on props.data._assigneeSelection:
{ accountId: string | null, displayName: string | null, avatarUrl: string | null,}ReporterPicker
Section titled “ReporterPicker”File: InlineEdit/ReporterPicker.tsx
Fields: Reporter
Pattern: Same as AssigneePicker with reporter-specific API
SprintDropdown
Section titled “SprintDropdown”File: InlineEdit/SprintDropdown.tsx
Fields: Sprint
Pattern: <select> grouped by sprint state (active/future/closed)
Fetches sprints from the backend scoped to the issue’s board.
LabelsEditor
Section titled “LabelsEditor”File: InlineEdit/LabelsEditor.tsx
Fields: Labels
Pattern: Multi-value tag input with typeahead
SelectDropdown / MultiSelectDropdown
Section titled “SelectDropdown / MultiSelectDropdown”File: InlineEdit/SelectDropdown.tsx, MultiSelectDropdown.tsx
Fields: Single-select and multi-select custom fields
Pattern: <select> / checkbox list from field options
CascadingSelectEditor
Section titled “CascadingSelectEditor”File: InlineEdit/CascadingSelectEditor.tsx
Fields: Cascading select custom fields
Pattern: Two nested <select> elements (parent > child)
OptionSelectEditor
Section titled “OptionSelectEditor”File: InlineEdit/OptionSelectEditor.tsx
Fields: Complex select fields with many options
Pattern: Searchable option list with keyboard navigation
The largest editor (~11KB). Supports search filtering, keyboard navigation (arrow keys, Enter, Escape), and option grouping.
ComponentPicker / VersionPicker / MultiVersionPicker
Section titled “ComponentPicker / VersionPicker / MultiVersionPicker”Files: ComponentPicker.tsx, VersionPicker.tsx, MultiVersionPicker.tsx
Fields: Jira components, fix versions, affects versions
Pattern: Picker UI with project-scoped options
GroupPicker / MultiGroupPicker
Section titled “GroupPicker / MultiGroupPicker”Files: GroupPicker.tsx, MultiGroupPicker.tsx
Fields: Group custom fields
Pattern: Group search picker
CustomUserPicker / MultiUserPicker
Section titled “CustomUserPicker / MultiUserPicker”Files: CustomUserPicker.tsx, MultiUserPicker.tsx
Fields: Custom user picker fields
Pattern: User search with avatars (single or multi-select)
UrlTextEditor
Section titled “UrlTextEditor”File: InlineEdit/UrlTextEditor.tsx
Fields: URL custom fields
Pattern: Text input with URL validation
IssueLinkEditor
Section titled “IssueLinkEditor”File: InlineEdit/IssueLinkEditor.tsx
Fields: Issue links
Pattern: Link type selector + issue key search
The most complex editor (~10KB). Supports:
- Link type selection (blocks, is blocked by, relates to, etc.)
- Issue key search with typeahead
- Creating new links and removing existing ones
FlaggedToggleEditor
Section titled “FlaggedToggleEditor”File: InlineEdit/FlaggedToggleEditor.tsx
Fields: Flagged/impediment field
Pattern: Toggle button
Forge Iframe Challenges & Workarounds
Section titled “Forge Iframe Challenges & Workarounds”Running AG Grid inline editing inside a Forge iframe introduces three critical issues.
Challenge 1: Focus Tracking
Section titled “Challenge 1: Focus Tracking”Problem: AG Grid’s stopEditingWhenCellsLoseFocus={true} relies on focusout events to detect when a user clicks outside an editor. In a cross-origin iframe, focus tracking breaks at the boundary — clicks on the Jira host page outside the iframe don’t fire focusout inside the iframe.
Solution: stopEditingWhenCellsLoseFocus={false} (disabled). Instead, a manual mousedown listener on the grid wrapper div detects clicks outside the editing cell:
// In HierarchyGrid.tsx:const gridDivRef = useRef<HTMLDivElement>(null);
useEffect(() => { const handler = (e: MouseEvent) => { if (!isEditingRef.current) return; const gridDiv = gridDivRef.current; if (!gridDiv) return;
// If click is inside the grid but outside the editing cell, stop editing if (gridDiv.contains(e.target as Node)) { gridApi?.stopEditing(); } }; document.addEventListener('mousedown', handler); return () => document.removeEventListener('mousedown', handler);}, [gridApi]);Note: AG Grid v35 removed the eGridDiv property from AgGridReact. The click-outside handler uses a gridDivRef on the wrapper <div> instead of gridRef.current?.eGridDiv (which returns undefined in v35).
Challenge 2: Popup Editor Clipping
Section titled “Challenge 2: Popup Editor Clipping”Problem: AG Grid’s cellEditorPopup: true renders popup editors inside .ag-root-wrapper, which has overflow: hidden. The popup gets clipped. Additionally, AG Grid’s popup positioning is broken inside Forge iframes (popups appear at wrong Y coordinates).
Solution: Never use cellEditorPopup: true. All editors render inline within the cell. For editors that need dropdown lists (StatusDropdown, PriorityDropdown, SprintDropdown), native <select> elements are used — the OS renders the dropdown outside any CSS overflow context.
For custom dropdowns (AssigneePicker, OptionSelectEditor), the dropdown uses position: fixed with coordinates calculated from getBoundingClientRect():
// Pattern for custom dropdowns in inline editors:const inputRect = inputRef.current.getBoundingClientRect();const dropdownStyle = { position: 'fixed' as const, top: inputRect.bottom, left: inputRect.left, width: inputRect.width, maxHeight: 300,};Challenge 3: useState During Editing
Section titled “Challenge 3: useState During Editing”Problem: Any useState setter called during AG Grid editing triggers a React re-render, which can destroy the active editor component. This is because HierarchyGrid is a controlled component — state changes cause a re-render cycle that may reconstruct the editor.
Solution: isEditing is a useRef, not useState:
// Correct — no re-render during editingconst isEditingRef = useRef(false);
const handleEditStarted = () => { isEditingRef.current = true;};
const handleEditStopped = () => { isEditingRef.current = false;};
// Wrong — would cause re-render and destroy active editorconst [isEditing, setIsEditing] = useState(false);Rule: Zero state updates should occur in AG Grid editing callbacks (onCellEditingStarted, onCellEditingStopped, onCellValueChanged). Use refs for any editing-related tracking.
Edit Metadata Pattern
Section titled “Edit Metadata Pattern”Some editors need to pass structured data beyond a simple string value. They store metadata on the row’s props.data object:
| Editor | Metadata Key | Data |
|---|---|---|
StatusDropdown | _statusTransition | { id, statusName, statusCategory } |
AssigneePicker | _assigneeSelection | { accountId, displayName, avatarUrl } |
ReporterPicker | _reporterSelection | { accountId, displayName, avatarUrl } |
The onCellEdit handler in HierarchyGrid reads this metadata to construct the correct API call:
// Simplified from HierarchyGrid onCellValueChanged:if (field === 'status_name') { const transition = rowData._statusTransition; api.updateField(lensId, issueKey, 'status', { transitionId: transition.id, }); // Optimistic update for the status display rowData.status_name = transition.statusName; rowData.status_category = transition.statusCategory;}Keyboard Handling
Section titled “Keyboard Handling”| Key | In Text/Number Editors | In Dropdown Editors |
|---|---|---|
Enter | Commits edit, stops editing | N/A (OS handles) |
Escape | Cancels edit, reverts value | Closes dropdown |
Tab | Commits and moves to next cell | N/A |
Backspace/Delete | Normal text editing (propagation stopped) | N/A |
The TextEditor explicitly stops propagation of Backspace and Delete keys to prevent AG Grid from interpreting them as navigation commands (QA-BUG-73).
CSS for Editors
Section titled “CSS for Editors”Editor styles are defined in foundation.css under the .cell-editor-* class namespace:
.cell-editor-input { /* Text/number input styling */ }.cell-editor-select { /* <select> dropdown styling */ }.cell-editor-picker { /* Complex picker container */ }.cell-editor-dropdown { /* Custom dropdown panel */ }.cell-editor-loading { /* Loading state */ }AssigneePicker has its own namespace:
.assignee-picker { /* Container */ }.assignee-picker-input { /* Search input bar with avatar */ }.assignee-dropdown { /* Dropdown list */ }.assignee-dropdown-item { /* Individual user row */ }.picker-clear { /* Clear button */ }