Skip to content

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.

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 event

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,
}

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
};

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

File: InlineEdit/NumberInput.tsx Fields: Story points, numeric custom fields Pattern: <input type="number">

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.

File: InlineEdit/DateTimePicker.tsx Fields: DateTime custom fields Pattern: Two inputs — date + time

File: InlineEdit/StatusDropdown.tsx Fields: Status Pattern: <select> with Jira workflow transitions

On mount:

  1. Calls api.getTransitions(issueKey, lensId) to load available transitions
  2. Shows loading state while fetching
  3. Renders <select> with transition options

On selection:

  1. Stores transition metadata on props.data._statusTransition
  2. Sets value to '__STATUS_UPDATING__' (sentinel)
  3. Calls props.stopEditing()

The onCellEdit handler in HierarchyGrid detects the sentinel and reads the transition metadata to execute the status change.

File: InlineEdit/PriorityDropdown.tsx Fields: Priority Pattern: <select> with priority options from context

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 (projectKey from 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,
}

File: InlineEdit/ReporterPicker.tsx Fields: Reporter Pattern: Same as AssigneePicker with reporter-specific API

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.

File: InlineEdit/LabelsEditor.tsx Fields: Labels Pattern: Multi-value tag input with typeahead

File: InlineEdit/SelectDropdown.tsx, MultiSelectDropdown.tsx Fields: Single-select and multi-select custom fields Pattern: <select> / checkbox list from field options

File: InlineEdit/CascadingSelectEditor.tsx Fields: Cascading select custom fields Pattern: Two nested <select> elements (parent > child)

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

Files: GroupPicker.tsx, MultiGroupPicker.tsx Fields: Group custom fields Pattern: Group search picker

Files: CustomUserPicker.tsx, MultiUserPicker.tsx Fields: Custom user picker fields Pattern: User search with avatars (single or multi-select)

File: InlineEdit/UrlTextEditor.tsx Fields: URL custom fields Pattern: Text input with URL validation

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

File: InlineEdit/FlaggedToggleEditor.tsx Fields: Flagged/impediment field Pattern: Toggle button

Running AG Grid inline editing inside a Forge iframe introduces three critical issues.

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).

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,
};

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 editing
const isEditingRef = useRef(false);
const handleEditStarted = () => {
isEditingRef.current = true;
};
const handleEditStopped = () => {
isEditingRef.current = false;
};
// Wrong — would cause re-render and destroy active editor
const [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.

Some editors need to pass structured data beyond a simple string value. They store metadata on the row’s props.data object:

EditorMetadata KeyData
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;
}
KeyIn Text/Number EditorsIn Dropdown Editors
EnterCommits edit, stops editingN/A (OS handles)
EscapeCancels edit, reverts valueCloses dropdown
TabCommits and moves to next cellN/A
Backspace/DeleteNormal 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).

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 */ }