Skip to content

improvement(tables): extract TablesDetail wrapper, ship trigger followups#4476

Open
TheodoreSpeaks wants to merge 60 commits intostagingfrom
fix/table-trigger-followup
Open

improvement(tables): extract TablesDetail wrapper, ship trigger followups#4476
TheodoreSpeaks wants to merge 60 commits intostagingfrom
fix/table-trigger-followup

Conversation

@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator

Summary

  • Extract <Table> page wrapper (was inline in 3850-line grid). Follows workflow.tsx/base.tsx/logs.tsx precedent — table.tsx exports <Table>, the data grid is <TableGrid>.
  • Drop <ExecutionDetailsSidebar> passthrough; flatten <RunStatusControl> to a single file.
  • Backfill remapped workflow outputs from saved execution logs (no re-run needed). Update column type when the new mapping has a different leaf type.
  • Re-seed columnOrder when the server adds a column (workflow output add was rendering at the end until refresh).
  • autoRun=false groups no longer render "Waiting" — stay empty until manually run.
  • Display defensiveness: stringify objects in cells instead of [object Object].
  • Workflow meta-header alignment: left border drawn via ::before so it doesn't shift content.

Type of Change

  • Bug fix
  • Refactor

Testing

Tested manually. bun run type-check clean across all 18+ commits, all 128 lib/table tests pass, dev server compiles cleanly, mothership embedded <Table embedded> path verified.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

TheodoreSpeaks and others added 30 commits May 4, 2026 10:18
…followup

# Conflicts:
#	apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-sidebar/column-sidebar.tsx
#	apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/cells/cell-content.tsx
#	apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx
Staging refactored Tables UI to decouple from DB position (gutter from
array index, checkedRows keyed by rowId, no PositionGapRows). Bring
HEAD's action-bar / context-menu helpers in line: contextMenuRowIds,
selectedRowIds, actionBarRowIds now key off row.id and walk `rows`
directly. Drop the maxPosition / positionMap derived state. Collapse
COLUMN_SIDEBAR_WIDTH_CSS to a numeric COLUMN_SIDEBAR_WIDTH used by both
the sidebar shell and the table's reserved padding-right.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When a workflow column is re-pointed to a different (blockId, path),
populate its existing rows with the new output's value pulled from saved
execution logs instead of leaving them empty until the next run. Rows
where the new mapping has no logged value clear (matching the previous
behavior for those rows), but rows where the workflow already has the
new output's value surface immediately.

Refactor backfillAddedGroupOutputs into a generalized
backfillGroupOutputsFromLogs helper with an `overwrite` flag — used in
both the added-outputs path (preserves hand-edited values) and the new
remapped path (overwrites since the new mapping is the source of truth).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A remap that changes the output's leaf type (string → number, json →
boolean, etc.) was leaving the column's declared type stale. The clear-
then-backfill flow then failed schema validation on every row, so the
backfill silently aborted and the column stayed empty.

Resolve the new leaf type via flattenWorkflowOutputs +
columnTypeForLeaf for each mappingUpdate, and patch
schema.columns[i].type before the schema write. The clear-tx then
backfill ordering now works end-to-end across type changes. If the
workflow or its target output can't be resolved (workflow deleted,
block removed), fall back to leaving the column type alone — the
backfill will skip rows whose picked value doesn't match, same as
before.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
If a column's declared type lags its row data (e.g. a workflow column
mid-remap, where the schema cache hasn't refetched yet but the row data
already has the new mapping's value), formatValueForInput and the
cell-render text variant fell through to String(value) and rendered
"[object Object]". JSON-stringify objects in both spots so the transient
skew shows the actual data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The meta cell had border-r/b/l while regular headers have only border-r/b.
With border-separate tables, that extra 1px left border shifted the
meta cell's content one pixel right of the columns below it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restore border-l and pull the cell back -1px with -ml-px so the visible
left border overlaps the previous cell's right border instead of adding
1px to the meta cell's box. Content lines up with the columns below.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adding border-l to the meta cell shifted its content right by 1px
because table-fixed + border-separate honors the border inside the
colspan'd cell's width budget. -ml-px doesn't work on <th>. Render the
visible left edge via a ::before at left: -1px instead — paints over
the prior cell's right border without consuming any of the meta cell's
content area. Content lines up with the columns below.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match the convention used by logs/components: every component folder
exposes its public API via index.ts so consumers import from the folder
name, not from its internal filenames.

- New barrels: column-config-sidebar/, workflow-sidebar/,
  table-action-bar/, table/cells/, table/headers/.
- Rename table-filter/index.tsx → index.ts (barrel is not a component).
- Top-level components/index.ts re-exports every sibling folder so
  external consumers have one import path.
- Replace `from '../foo/foo'` doubled paths in table.tsx with the
  shorter barrel-anchored form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 1 step 0 of the wrapper extraction (see plan
okay-lets-make-a-shimmying-trinket.md). page.tsx now renders
TablesDetail, which today is a passthrough to <Table>. Subsequent
commits lift surface state out of <Table> into this wrapper one piece
at a time.

The mothership chat path (<Table embedded>) is untouched — <Table>
stays exportable as a lower-level component for embedded contexts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The three right-edge slideout panels (column config, workflow config,
execution details) move out of <Table> into the wrapper. The wrapper
owns a single useReducer that encodes the at-most-one-open invariant
as a discriminated union — opening any one panel automatically closes
the others. <Table> emits open requests via three new callback props.

Also extract <ExecutionDetailsSidebar> from inline-in-table.tsx to its
own folder so the wrapper can compose it cleanly. Update the embedded
mothership callsite (resource-content.tsx) to render <TablesDetail
embedded> instead of <Table embedded>.

Phase 1 step 1 of the wrapper extraction. <Table> shrinks from 3849 →
3787 lines; <TablesDetail> grows from 19 → 145 lines.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The delete-table confirmation modal and `useDeleteTable` mutation move
out of <Table> into TablesDetail. <Table> exposes a new
`onRequestDeleteTable` callback fired by the page-header Delete action.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ImportCsvDialog moves out of <Table>. Grid exposes
`onRequestImportCsv` fired by the page-header menu item; wrapper owns
the open state and renders the dialog.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Both RowModal instances move out of <Table> into the wrapper. Grid
emits `onOpenRowModal(row)` (Space key) and
`onRequestDeleteRows(snapshots)` (context menu).

Post-delete cleanup (push undo, clear selection) needs grid-internal
state, so the grid populates an `afterDeleteRowsSinkRef` callback that
the wrapper's modal `onSuccess` invokes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The destructive delete-columns confirmation modal moves into the
wrapper. Grid emits `onRequestDeleteColumns(names)`; the cascade itself
(per-column mutation, undo push, columnOrder + columnWidths cleanup)
stays in the grid as a sink the wrapper invokes on confirm — too
grid-internal to lift cleanly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
useRunGroup and useCancelTableRuns move out of <Table> into the
wrapper, along with the <TableActionBar> render. Grid receives
onRunGroup, onRunRows, onStopRow, onStopRows, onStopAll, and
cancelRunsPending as props — used by the per-row gutter Play/Stop, the
workflow-group meta-cell run menu, and the right-click context menu's
Run/Stop on selection items.

Action-bar selection state (actionBarRowIds, runningInActionBar,
hasWorkflowColumns) is derived from grid-internal state, so the grid
emits a `SelectionSnapshot` via `onSelectionChange` from a useEffect.
Wrapper uses the snapshot to drive the floating <TableActionBar>.

Phase 2 step 1 of the wrapper extraction.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
queryOptions (filter + sort) moves out of <Table> into the wrapper,
making it a single source of truth that drives one useTable call. The
wrapper passes the bundle down to the grid; sort/filter handlers in
the grid call onQueryOptionsChange.

Eliminates the previous double-useTable pattern (one for the grid's
filtered/sorted view, one in the wrapper's hardcoded null/null query
for sidebar metadata).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…apper

Phase 3 of the wrapper extraction. The full page-header surface moves
out of <Table>:

- ResourceHeader (breadcrumbs, table-rename UI, headerActions, createTrigger)
- ResourceOptionsBar (sort + filter toggle)
- TableFilter (filter panel — wrapper owns filterOpen state)
- RunStatusControl (in the leading actions when runs are active)

useRenameTable + useInlineRename for the breadcrumb name move to the
wrapper. The grid populates pushTableRenameUndoSinkRef so the rename is
still part of the grid's undo stack.

Extract NewColumnDropdown and RunStatusControl from inline-in-table.tsx
to their own folders so the wrapper composes them cleanly without
reaching into the grid's internals.

Hoist generateColumnName from grid-internal useCallback to a shared util
so both the page-header and inline-header NewColumnDropdowns use the
same logic.

After this lift <Table> is the data grid only — no page surface, no
modals, no slideouts, no breadcrumbs. The selection snapshot now
includes totalRunning so the wrapper can render the page-header
RunStatusControl from outside the grid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tables don't use realtime sockets in prod — strip the dead path so we stop
paying the per-row HTTP forward + socket emit on every cell write. Polling on
running execs already covers reconciliation.

Sim side:
- service.ts: drop notifyTableRowUpdated/Deleted, notifyTableDeleted, the
  postRealtimeBridge helper, and all callsites.
- hooks/queries/tables.ts: drop the socket subscription block in useTableRows;
  poll-on-running stays. Remove useEffect / useSocket imports.
- app/.../tables/[tableId]/hooks/use-table.ts: drop the merge-on-event
  useEffect and unused imports.
- app/workspace/providers/socket-provider.tsx: drop joinTable/leaveTable,
  onTableRowUpdated/Deleted/onTableDeleted, currentTableId state, related
  events + types.

Realtime side:
- handlers/tables.ts deleted; index.ts no longer wires it.
- routes/http.ts: drop /api/table-row-updated, /api/table-row-deleted,
  /api/table-deleted endpoints.
- rooms/{memory,redis}-manager.ts: drop emitToTable, handleTableRowUpdated/
  Deleted, handleTableDeleted, related imports.
- rooms/types.ts: drop method declarations, TableRowUpdatedPayload type,
  tableRoomName helper.
- middleware/permissions.ts: drop unused verifyTableAccess.

Bonus from parallel work:
- cell-content typewriter trigger refinement.
When the user wipes a workflow output column value, the auto-fire reactor
needs to be re-armed for that group. Previously, a stale cancelled / error
exec record blocked the eligibility predicate (gate at line 79 hard-rejects
those statuses on auto-fire) and the cell stayed stuck in its old terminal
state — visible as "Cancelled" cells that wouldn't re-run no matter what.

Both updateRow and batchUpdateRows now derive an `executionsPatch[gid] = null`
for any output column the patch sets to empty. The data clear and the exec
clear ride the same SQL transaction, so the row never lands in a stale-
status-with-empty-data state.

Symmetric to how `completed` already worked via `areOutputsFilled` in the
predicate — clearing the cell wins over the prior exec status, regardless of
what that status was.

(Also revert typewriter-trigger experiment from a parallel session that was
in-progress on this branch.)
…ec cleanup

A bundle of small UX + correctness fixes around workflow-cell run state.

cell-render.tsx
- In-flight (queued/running/pending) now wins over the existing value, so
  re-runs surface immediately instead of looking like nothing happened until
  the worker writes the new value.
- "Waiting on X" wins over a stale `cancelled` / `error` exec when deps are
  unmet — clearing a dep now reads as actionable instead of stuck.

useRunColumn (hooks/queries/tables.ts)
- onSettled now cancels in-flight polls before invalidating. Stops a poll
  that landed mid-mutation from clobbering the optimistic state with stale
  data, which produced the queued → cancelled → queued flicker.

addWorkflowGroup / updateWorkflowGroup (autoRun toggle on)
- Awaits scheduleRunsForTable instead of fire-and-forget. The route returned
  before the queued exec stamps committed, so the post-mutation refetch saw
  no in-flight cells and polling never started — cells looked stuck even
  though the server eventually stamped them.

deleteColumn / deleteColumns
- Strip orphaned executions[gid] keys when deleting a column orphans its
  parent group. Without this, stale running/queued exec records lingered on
  every row forever and inflated the page-header "N running" counter even
  on tables with no actually-running cells.

UI
- Action-bar leading label: "Selected N workflow cell(s)".
- Context menu: Run / Refresh items mirror the action bar's Play / Refresh
  split, gated on the same selection-status flags so both surfaces show the
  actions that match the current state.
Cleanup pass on the recent table changes — pulls duplicated predicates and
SQL snippets into shared helpers and fixes one drift bug along the way.

- isExecInFlight: now single export from lib/table/deps.ts. Removed the
  duplicate in components/table-grid/utils.ts. Used by isGroupEligible
  (server eligibility) and runningByRowId (client counter).
- isOptimisticInFlight: kept local to hooks/queries/tables.ts — renamed from
  isInFlight to disambiguate from the stricter isExecInFlight. The two
  predicates differ on `pending` without a jobId: optimistic patches and
  poll-trigger want the broader version, eligibility wants the strict one.
- areOutputsFilled: single export from lib/table/deps.ts, dropped duplicate
  from workflow-columns.ts.
- classifyExecStatusMix: shared row × group walker in table-grid/utils.ts.
  Replaces two copies of the same loop in table-grid.tsx (selectionStats +
  contextMenuStats). Both surfaces now have the same short-circuit
  semantics, including the seen-all-selected-rows early break that
  contextMenuStats was missing.
- stripGroupExecutions: SQL helper in service.ts. Replaces three copies of
  the `UPDATE user_table_rows SET executions = executions - $gid::text`
  pattern across deleteColumn / deleteColumns / deleteWorkflowGroup.

Drift bug:
- runningByRowId / totalRunning counted only `running` and `queued`. Every
  other in-flight check in the codebase treats post-stamp `pending` as
  in-flight too, so the page-header "N running" badge briefly dropped to 0
  between scheduler stamp and worker pickup. Now uses isExecInFlight.
… didDragRef on dragend, align sidebar width)
…followup

# Conflicts:
#	apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx
#	scripts/check-api-validation-contracts.ts
Multi-group manual runs (Run row, gutter Play, action-bar Play across mixed
completed + cancelled cells) re-fired completed-and-filled siblings.
runWorkflowGroupsInternal cleared only the groups it filtered, then called
scheduleRunsForRows with isManualRun: true and no group / mode filter — so
the post-clear pass walked every group on the table with default mode 'all',
and any autoRun=true completed sibling whose deps were satisfied got queued
again. Scope the post-clear call to targetGroups and forward mode.
…teWorkflowGroup

deleteWorkflowGroup already stripped removed-column deps from sibling
groups, but updateWorkflowGroup (the path the UI takes when deleting one
output of a multi-output group) didn't — schema validation then rejected
the update with 'Group X depends on missing column Y'.
@TheodoreSpeaks
Copy link
Copy Markdown
Collaborator Author

@BugBot review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 9a0b3a2. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant