ProficientNowTechRFCs

RFC: Inline Cell Editing for SDTF Tables

RFC: Inline Cell Editing for SDTF Tables

  • Authors: Prathik Shetty(@pshettydev)
  • Date: 2025-12-15
  • Status: Accepted

Abstract

This RFC proposes an Airtable/Teable-like inline editing experience for SDTF-rendered tables in the frontend.

The goal is to allow users to click to activate a cell, double-click to edit (for supported field types), and auto-save on blur/Enter with Esc to cancel — while keeping the pinned (first) data column sheet-only.

This RFC covers:

  • Frontend interaction and UI behavior
  • Schema & API requirements needed to support inline editing across core + dynamic tables

Motivation

SDTF tables currently render dynamic data effectively, but editing is primarily performed via a side sheet. This creates extra steps for high-volume workflows where users need to update many fields rapidly.

Inline editing improves:

  • Speed: fewer context switches compared to opening the sheet.
  • Usability: spreadsheet-like workflow familiar to Airtable users.
  • Consistency: a single “edit-in-place” pattern across native and custom fields.

At the same time, the pinned (first) column remains a reliable entry-point into the existing sheet experience (details, activity, multi-tab layouts).

Approaches

1) Keep sheet-only editing (status quo)

  • Pros: no new interaction complexity; existing validation/UI already works.
  • Cons: slow for bulk edits; feels non-spreadsheet-like.

2) Make the existing DynamicCell component editable directly

  • Pros: minimal component surface area.
  • Cons: DynamicCell is already a large, display-focused renderer; mixing editing concerns risks becoming difficult to maintain.

3) Add a dedicated inline editing layer (choosing this approach)

Introduce a wrapper component that:

  • Renders DynamicCell in read-only mode
  • Switches to field-type-specific editors when the cell is in “editing” state

This keeps DynamicCell mostly render-only and isolates editing logic.

Proposal

UX / Interaction Rules

  • Single click: activates (highlights) a cell.
  • Double click: enters edit mode only for supported field types.
  • Commit: blur or Enter auto-saves.
  • Cancel: Esc restores original value.
  • Clear semantics: clearing a value writes null.

Interaction Sequence Diagram

Pinned Column Behavior

  • The pinned (first) data column is sheet-only.
  • Single click on pinned column opens the sheet.
  • Double click on pinned column does not enter inline edit.

Scope of Editable Field Types

Easy

  • SHORT_TEXT, LONG_TEXT
  • NUMBER, CURRENCY, PERCENT
  • CHECKBOX
  • DATE, DATETIME

Medium

  • SINGLE_SELECT
  • MULTI_SELECT

Hard

  • FORMULA (read-only)
  • USER, ATTACHMENT
  • LINK_TO_ANOTHER_RECORD editor
  • Spreadsheet keyboard navigation (arrow/tab)

Component / State Design

1) Centralize cell interaction in row rendering

Update the table row rendering so that click/double-click behavior is handled in one place, avoiding duplicated click handlers.

Maintain two pieces of UI state:

  • activeCell: { rowId; fieldId } | null
  • editingCell: { rowId; fieldId } | null

Only one cell should be edited at a time (at least for now).

2) Introduce EditableDynamicCell

A new component (e.g. apps/frontend/web/src/components/dynamic/TableCell/EditableDynamicCell.tsx) will:

  • Render DynamicCell when not editing
  • Render a type-specific editor when editing

Suggested props:

  • field, row, value, schemaFields
  • isActive, isEditing
  • draftValue, onDraftChange
  • onCommit(draftValueOrNull), onCancel()

3) Type-specific editors using shadcn primitives

Use shadcn/radix primitives already present under:

  • apps/frontend/web/src/components/primitives/*

Editor mapping:

  • Text: Input
  • Long text: Textarea
  • Number/currency/percent: Input type="number" (with safe parsing)
  • Checkbox: Checkbox
  • Date/DateTime: Popover + Calendar
  • Single select: Select (and optionally Command for search later)
  • Multi select: a popover list UI; optionally reuse/extend existing MultiSelect primitive

Saving Behavior (frontend wiring)

Inline editing should reuse the same “save path” as sheet editing.

  • The table-level handler (currently handleValueChange in apps/frontend/web/src/components/dynamic/DynamicTable.tsx) should be exposed to row/cell components as onCellValueChange.
  • On commit, call:
onCellValueChange({ fieldId, rowId, value })

Where value is either a real value or null.

Optionally (recommended for UX): apply an optimistic UI update for the affected cell before the async request returns.

Schema & API Requirements

Inline editing must remain schema-driven (no hardcoded per-field logic in the UI). The UI uses TableSchema.fields[] entries.

SDTF Schema Requirements

1) Field identity

  • field_id: immutable SDTF field id (e.g. pnfld-...) — used in update payloads.
  • db_field_name: database column / logical field name (e.g. size, recruiter_assignee, contacts).

Important: update payloads use fieldId (SDTF field_id), not db_field_name.

2) Relation metadata

For relational fields:

  • is_relational: true
  • relation.type: ONE or MANY
  • relation.table: related table name
  • relation.display.format.template and relation.display_fields determine rendering

This metadata is used by the UI to choose editors and decide between scalar vs relational update behavior.

Per-field inline editing behavior should be configured via field_edit_options.

Base:

{
  "field_edit_options": {
    "editable": true
  }
}

Remote fetch options (server-side search):

{
  "field_edit_options": {
    "editable": true,
    "fetch_options": {
      "field_name": "research_analyst",
      "fetch_endpoint": "/api/v1/users",
      "template": "{email}",
      "value_field": "id",
      "label_field": "email",
      "page_param": "page",
      "limit_param": "limit",
      "search_param": "search",
      "default_page": 1,
      "default_limit": 20,
      "params": {
        "department": "RESEARCHERS"
      }
    }
  }
}

Contract: UI calls:

<fetch_endpoint>?page=1&limit=20&search=<term>

and will always include params (fixed query params).

MANY relations: incremental editing can be explicitly configured with multi_select_mode: "incremental":

{
  "field_edit_options": {
    "editable": true,
    "multi_select_mode": "incremental",
    "fetch_options": {
      "field_name": "opportunities",
      "fetch_endpoint": "/api/v1/opportunities",
      "template": "{title}",
      "value_field": "id",
      "label_field": "title",
      "page_param": "page",
      "limit_param": "limit",
      "search_param": "search",
      "default_page": 1,
      "default_limit": 20
    }
  }
}

Note: The inline editor should default to incremental ops for async MANY relations.

4) Static options via formatting.options

If the schema provides formatting.options for a field (common for status-like enums), the UI should:

  • Use those options directly (no remote fetch)
  • Prefer option.id as the stored value when present

Alternative (legacy) config: additional_config.filter_configs

Some deployments have fetch configs under additional_config.filter_configs. The UI may fall back to these configs for inline editing, but the preferred path is to migrate to per-field field_edit_options.fetch_options.

Update API Contracts

Inline editing uses a single update endpoint per SDTF table (core or dynamic):

PATCH <schema.endpoint_url>/:id

Update payloads

Scalar / single-value update:

{
  "tableId": "pntbl-...",
  "fieldId": "pnfld-...",
  "value": "new value"
}

Custom fields (hybrid core table):

{
  "tableId": "pntbl-...",
  "customFields": {
    "pnfld-custom-1": "x",
    "pnfld-custom-2": 123
  }
}

MANY relation incremental ops:

{
  "tableId": "pntbl-...",
  "fieldId": "pnfld-...",
  "op": "connect",
  "value": ["<related-id>"]
}

and:

{
  "tableId": "pntbl-...",
  "fieldId": "pnfld-...",
  "op": "disconnect",
  "value": ["<related-id>"]
}

Audit fields

Clients must not send audit fields; api-gateway should strip them defensively:

  • created_by, created_at, updated_by, updated_at
  • last_updated_by, last_updated_at
  • deleted_by, deleted_at, is_deleted

Option endpoints (fetch_options.fetch_endpoint)

For every field that uses remote options, api-gateway must expose an endpoint that supports:

  • page (1-based), limit, search
  • any additional fixed params (e.g. department=RESEARCHERS)

The UI normalizer is tolerant, but the easiest response shape is:

{
  "status": "success",
  "data": {
    "users": [{ "id": "...", "email": "..." }],
    "totalRecords": 123,
    "currentPage": 1,
    "pageSize": 20,
    "totalPages": 7
  }
}

Visual States

  • Active cell: subtle outline/background.
  • Editing cell: stronger outline + editor focus.
  • Editors must stop propagation to prevent:
    • sheet open (pinned column)
    • context menu conflicts

Future Enhancements

  • Spreadsheet-like keyboard navigation (arrows/tab/enter workflows)
  • Searchable selects via Command
  • Richer editors for USER, ATTACHMENT, and linked records
  • Better offline/latency handling (queued edits, batching)
  • Backend-driven validation/error rendering consistency across inline and sheet edits