Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
e5ead97
ui improvements
TheodoreSpeaks May 4, 2026
b38c3b6
Update status pils, make checkbox column sticky
TheodoreSpeaks May 4, 2026
3d5ba73
add Run workflow to context menu
TheodoreSpeaks May 4, 2026
cb563c5
Refactor dispatching logic
TheodoreSpeaks May 5, 2026
49d8904
fix checkbox width to be smaller if csv is small
TheodoreSpeaks May 5, 2026
3b166c8
Add drag behavior for workflows, stop workflow on multi select
TheodoreSpeaks May 5, 2026
6638b17
fix z index of checkbox to left, add view workflow button
TheodoreSpeaks May 5, 2026
0fa4602
Switch to emcn buttons for Add inputs
TheodoreSpeaks May 5, 2026
cd9ab9f
Split up workflow sidebar from column sidebar, refactor cells
TheodoreSpeaks May 5, 2026
899cd46
Lint and add auto run toggle
TheodoreSpeaks May 5, 2026
dffbb80
fix column reordering, add action bar
TheodoreSpeaks May 6, 2026
cd1e032
Create and use emcn square
TheodoreSpeaks May 6, 2026
f68a625
Merge remote-tracking branch 'origin/staging' into fix/table-trigger-…
TheodoreSpeaks May 6, 2026
f364cff
Reconcile post-merge: drop positionMap, use rowId-based selection
TheodoreSpeaks May 6, 2026
6d60133
feat(table): backfill remapped workflow outputs from execution logs
TheodoreSpeaks May 6, 2026
d53d427
fix(table): update column type when remapping workflow output
TheodoreSpeaks May 6, 2026
e318548
fix(table): stringify objects instead of "[object Object]" in cells
TheodoreSpeaks May 6, 2026
c819548
fix(table): drop extra left border on workflow group meta header
TheodoreSpeaks May 6, 2026
2b26d3d
fix(table): align workflow meta header without dropping its left border
TheodoreSpeaks May 6, 2026
bdb2e27
fix(table): draw meta header left border via ::before pseudo
TheodoreSpeaks May 6, 2026
b583ae4
refactor(tables): add missing barrels, drop doubled-path imports
TheodoreSpeaks May 6, 2026
b475654
refactor(tables): introduce TablesDetail wrapper as thin passthrough
TheodoreSpeaks May 6, 2026
6977bb2
refactor(tables): lift slideout panel state into TablesDetail wrapper
TheodoreSpeaks May 6, 2026
a32ffb0
refactor(tables): lift delete-table modal + mutation into wrapper
TheodoreSpeaks May 6, 2026
be8915d
refactor(tables): lift CSV import dialog into wrapper
TheodoreSpeaks May 6, 2026
2eba3db
refactor(tables): lift RowModal (edit + delete) into wrapper
TheodoreSpeaks May 6, 2026
bfb7977
refactor(tables): lift delete-columns modal into wrapper
TheodoreSpeaks May 6, 2026
d039b97
refactor(tables): lift run/stop mutations + TableActionBar to wrapper
TheodoreSpeaks May 6, 2026
735fddc
refactor(tables): lift queryOptions to wrapper
TheodoreSpeaks May 6, 2026
2a2aecf
refactor(tables): lift page header (breadcrumbs/options/filter) to wr…
TheodoreSpeaks May 6, 2026
2fbb8f8
chore(tables): cleanup pass on TablesDetail wrapper extraction
TheodoreSpeaks May 6, 2026
5b5a6e1
fix(table): re-seed columnOrder when columns change server-side
TheodoreSpeaks May 6, 2026
b08c7f3
refactor(tables): rename wrapper to <Table>, grid to <TableGrid>
TheodoreSpeaks May 6, 2026
a41ea68
fix(table): don't show "Waiting" for autoRun=false workflow groups
TheodoreSpeaks May 6, 2026
a94efa9
chore(copilot): regenerate tool catalog from copilot dev (#247)
TheodoreSpeaks May 6, 2026
087f52d
improvement(table): action bar in mothership + per-execution mode
TheodoreSpeaks May 6, 2026
c07041b
fix(table): backfill on add_workflow_group_output, don't re-run
TheodoreSpeaks May 6, 2026
19d1a09
fix(table): drop sql.raw quote-escaping in column-name interpolation
TheodoreSpeaks May 6, 2026
f874b96
feat(copilot-tool): forward autoRun + mappingUpdates on update_workfl…
TheodoreSpeaks May 6, 2026
306e122
fix(table): keep gutter border visible when hovering Run-row button
TheodoreSpeaks May 6, 2026
a48b43f
fix(table): unify auto-fire and manual run paths in scheduler
TheodoreSpeaks May 6, 2026
3c87a07
fix(table): render empty cells as blank, not em-dash
TheodoreSpeaks May 6, 2026
f23f234
fix(table): per-row Run fires autoRun=false groups regardless of deps
TheodoreSpeaks May 6, 2026
d8e96c5
fix ui shape
TheodoreSpeaks May 6, 2026
8dfcc90
improvement(table): collapse run ops into run_column, derive action-b…
TheodoreSpeaks May 7, 2026
941364a
chore(copilot): regen tool catalog after dropping run_cell / run_row …
TheodoreSpeaks May 7, 2026
ba7e565
fix(table): atomic per-key writes for executions, plus run-op race fixes
TheodoreSpeaks May 7, 2026
2ec89ff
chore(table): remove table-row sockets, both sides
TheodoreSpeaks May 7, 2026
6533967
fix(table): clearing a workflow output cell also clears its exec record
TheodoreSpeaks May 7, 2026
036db43
fix(table): waiting state, optimistic UX, schema-mutation polling, ex…
TheodoreSpeaks May 7, 2026
01deeab
refactor(table): consolidate exec-status helpers + fix N-running counter
TheodoreSpeaks May 7, 2026
158f889
fix(table): address pr review (drop dead workflowNameById prop, reset…
TheodoreSpeaks May 7, 2026
c1aa02c
Merge remote-tracking branch 'origin/staging' into fix/table-trigger-…
TheodoreSpeaks May 7, 2026
7c97335
fix(table): scope post-clear schedule to targeted groups, forward mode
TheodoreSpeaks May 7, 2026
b21c4df
fix(table): meta-cell drag-leave flicker guard + plumb unique on create
TheodoreSpeaks May 7, 2026
8706cca
fix(table): strip sibling deps when removing workflow output via upda…
TheodoreSpeaks May 7, 2026
9263521
improvement(table): debug logs at every cascade decision branch
TheodoreSpeaks May 7, 2026
38f5130
improvement(table): parallelize queued-stamp writes within concurrenc…
TheodoreSpeaks May 7, 2026
29cade9
Simplify stripping column names
TheodoreSpeaks May 7, 2026
9a0b3a2
fix lint, ci
TheodoreSpeaks May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions apps/realtime/src/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { setupConnectionHandlers } from '@/handlers/connection'
import { setupOperationsHandlers } from '@/handlers/operations'
import { setupPresenceHandlers } from '@/handlers/presence'
import { setupSubblocksHandlers } from '@/handlers/subblocks'
import { setupTableHandlers } from '@/handlers/tables'
import { setupVariablesHandlers } from '@/handlers/variables'
import { setupWorkflowHandlers } from '@/handlers/workflow'
import type { AuthenticatedSocket } from '@/middleware/auth'
Expand All @@ -14,6 +13,5 @@ export function setupAllHandlers(socket: AuthenticatedSocket, roomManager: IRoom
setupSubblocksHandlers(socket, roomManager)
setupVariablesHandlers(socket, roomManager)
setupPresenceHandlers(socket, roomManager)
setupTableHandlers(socket, roomManager)
setupConnectionHandlers(socket, roomManager)
}
73 changes: 0 additions & 73 deletions apps/realtime/src/handlers/tables.ts

This file was deleted.

48 changes: 0 additions & 48 deletions apps/realtime/src/middleware/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,51 +131,3 @@ export async function verifyWorkflowAccess(
return { hasAccess: false }
}
}

/**
* Verify a user has read access to a table by virtue of workspace permission.
* Mirrors `verifyWorkflowAccess` for the table-room socket join check.
*/
export async function verifyTableAccess(
userId: string,
tableId: string
): Promise<{ hasAccess: boolean; workspaceId?: string }> {
try {
const { userTableDefinitions, permissions } = await import('@sim/db')
const tableData = await db
.select({ workspaceId: userTableDefinitions.workspaceId })
.from(userTableDefinitions)
.where(and(eq(userTableDefinitions.id, tableId), isNull(userTableDefinitions.archivedAt)))
.limit(1)

if (!tableData.length) {
logger.warn(`Table ${tableId} not found`)
return { hasAccess: false }
}
const { workspaceId } = tableData[0]
if (!workspaceId) return { hasAccess: false }

const [permissionRow] = await db
.select({ permissionType: permissions.permissionType })
.from(permissions)
.where(
and(
eq(permissions.userId, userId),
eq(permissions.entityType, 'workspace'),
eq(permissions.entityId, workspaceId)
)
)
.limit(1)

if (!permissionRow?.permissionType) {
logger.warn(
`User ${userId} has no permission for workspace ${workspaceId} (table ${tableId})`
)
return { hasAccess: false }
}
return { hasAccess: true, workspaceId }
} catch (error) {
logger.error(`Error verifying table access for user ${userId}, table ${tableId}:`, error)
return { hasAccess: false }
}
}
28 changes: 1 addition & 27 deletions apps/realtime/src/rooms/memory-manager.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { createLogger } from '@sim/logger'
import type { Server } from 'socket.io'
import {
type IRoomManager,
type TableRowUpdatedPayload,
tableRoomName,
type UserPresence,
type UserSession,
type WorkflowRoom,
} from '@/rooms/types'
import type { IRoomManager, UserPresence, UserSession, WorkflowRoom } from '@/rooms/types'

const logger = createLogger('MemoryRoomManager')

Expand Down Expand Up @@ -262,23 +255,4 @@ export class MemoryRoomManager implements IRoomManager {

logger.info(`Notified ${room.users.size} users about workflow deployment change: ${workflowId}`)
}

emitToTable<T = unknown>(tableId: string, event: string, payload: T): void {
this._io.to(tableRoomName(tableId)).emit(event, payload)
}

async handleTableRowUpdated(tableId: string, payload: TableRowUpdatedPayload): Promise<void> {
this.emitToTable(tableId, 'table-row-updated', { tableId, ...payload })
}

async handleTableRowDeleted(tableId: string, rowId: string): Promise<void> {
this.emitToTable(tableId, 'table-row-deleted', { tableId, rowId })
}

async handleTableDeleted(tableId: string): Promise<void> {
logger.info(`Handling table deletion notification for ${tableId}`)
this.emitToTable(tableId, 'table-deleted', { tableId, timestamp: Date.now() })
// Eject sockets so they don't hold a stale room. Cross-pod safe via socket.io.
await this._io.in(tableRoomName(tableId)).socketsLeave(tableRoomName(tableId))
}
}
27 changes: 1 addition & 26 deletions apps/realtime/src/rooms/redis-manager.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,7 @@
import { createLogger } from '@sim/logger'
import { createClient, type RedisClientType } from 'redis'
import type { Server } from 'socket.io'
import {
type IRoomManager,
type TableRowUpdatedPayload,
tableRoomName,
type UserPresence,
type UserSession,
} from '@/rooms/types'
import type { IRoomManager, UserPresence, UserSession } from '@/rooms/types'

const logger = createLogger('RedisRoomManager')

Expand Down Expand Up @@ -463,23 +457,4 @@ export class RedisRoomManager implements IRoomManager {
const userCount = await this.getUniqueUserCount(workflowId)
logger.info(`Notified ${userCount} users about workflow deployment change: ${workflowId}`)
}

emitToTable<T = unknown>(tableId: string, event: string, payload: T): void {
this._io.to(tableRoomName(tableId)).emit(event, payload)
}

async handleTableRowUpdated(tableId: string, payload: TableRowUpdatedPayload): Promise<void> {
this.emitToTable(tableId, 'table-row-updated', { tableId, ...payload })
}

async handleTableRowDeleted(tableId: string, rowId: string): Promise<void> {
this.emitToTable(tableId, 'table-row-deleted', { tableId, rowId })
}

async handleTableDeleted(tableId: string): Promise<void> {
logger.info(`Handling table deletion notification for ${tableId}`)
this.emitToTable(tableId, 'table-deleted', { tableId, timestamp: Date.now() })
// Eject sockets across all pods via socket.io's Redis adapter.
await this._io.in(tableRoomName(tableId)).socketsLeave(tableRoomName(tableId))
}
}
41 changes: 0 additions & 41 deletions apps/realtime/src/rooms/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,45 +143,4 @@ export interface IRoomManager {
* Handle workflow deployment change - notify users to refresh deployment state
*/
handleWorkflowDeployed(workflowId: string): Promise<void>

/**
* Emit an event to all clients in a table room (`table:${tableId}`).
* Tables don't track presence/last-modified state — just pub/sub.
*/
emitToTable<T = unknown>(tableId: string, event: string, payload: T): void

/**
* Notify all clients in a table room of a row write (insert/update/cell-state-change).
* Sim API calls this via the `/api/table-row-updated` HTTP bridge after every successful
* row commit; the client merges the delta into its React Query cache.
*/
handleTableRowUpdated(tableId: string, payload: TableRowUpdatedPayload): Promise<void>

/**
* Notify all clients in a table room that a row has been deleted.
*/
handleTableRowDeleted(tableId: string, rowId: string): Promise<void>

/**
* Notify all clients in a table room that the table has been deleted; eject sockets.
*/
handleTableDeleted(tableId: string): Promise<void>
}

/**
* Payload broadcast on `table-row-updated`. Mirrors the shape of `TableRow.data` so
* the client can merge directly into its React Query rows cache. `position` and
* `updatedAt` are included for cache reconciliation; `data` is the full row data
* (not a per-cell delta) — see plan Notes.
*/
export interface TableRowUpdatedPayload {
rowId: string
data: Record<string, unknown>
/** Per-workflow-group execution state. Keyed by `WorkflowGroup.id`. */
executions?: Record<string, unknown>
position: number
updatedAt: string | number
}

/** Socket.IO room name for a table. Namespaced from workflow rooms. */
export const tableRoomName = (tableId: string): string => `table:${tableId}`
46 changes: 0 additions & 46 deletions apps/realtime/src/routes/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,52 +150,6 @@ export function createHttpHandler(roomManager: IRoomManager, logger: Logger) {
return
}

// Handle table row write notifications from the Sim API
if (req.method === 'POST' && req.url === '/api/table-row-updated') {
try {
const body = await readRequestBody(req)
const { tableId, rowId, data, executions, position, updatedAt } = JSON.parse(body)
await roomManager.handleTableRowUpdated(tableId, {
rowId,
data,
executions,
position,
updatedAt,
})
sendSuccess(res)
} catch (error) {
logger.error('Error handling table row update notification:', error)
sendError(res, 'Failed to process table row update')
}
return
}

if (req.method === 'POST' && req.url === '/api/table-row-deleted') {
try {
const body = await readRequestBody(req)
const { tableId, rowId } = JSON.parse(body)
await roomManager.handleTableRowDeleted(tableId, rowId)
sendSuccess(res)
} catch (error) {
logger.error('Error handling table row deletion notification:', error)
sendError(res, 'Failed to process table row deletion')
}
return
}

if (req.method === 'POST' && req.url === '/api/table-deleted') {
try {
const body = await readRequestBody(req)
const { tableId } = JSON.parse(body)
await roomManager.handleTableDeleted(tableId)
sendSuccess(res)
} catch (error) {
logger.error('Error handling table deletion notification:', error)
sendError(res, 'Failed to process table deletion')
}
return
}

res.writeHead(404, { 'Content-Type': 'application/json' })
res.end(JSON.stringify({ error: 'Not found' }))
}
Expand Down
48 changes: 48 additions & 0 deletions apps/sim/app/api/table/[tableId]/columns/run/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { createLogger } from '@sim/logger'
import { type NextRequest, NextResponse } from 'next/server'
import { runColumnContract } from '@/lib/api/contracts/tables'
import { parseRequest } from '@/lib/api/server'
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
import { generateRequestId } from '@/lib/core/utils/request'
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
import { runWorkflowColumn } from '@/lib/table/workflow-columns'
import { accessError, checkAccess } from '@/app/api/table/utils'

const logger = createLogger('TableRunColumnAPI')

interface RouteParams {
params: Promise<{ tableId: string }>
}

/** POST /api/table/[tableId]/columns/run */
export const POST = withRouteHandler(async (request: NextRequest, { params }: RouteParams) => {
const requestId = generateRequestId()
try {
const auth = await checkSessionOrInternalAuth(request, { requireWorkflowId: false })
if (!auth.success || !auth.userId) {
return NextResponse.json({ error: 'Authentication required' }, { status: 401 })
}
const parsed = await parseRequest(runColumnContract, request, { params })
if (!parsed.success) return parsed.response
const { tableId } = parsed.data.params
const { workspaceId, groupIds, runMode, rowIds } = parsed.data.body
const access = await checkAccess(tableId, auth.userId, 'write')
if (!access.ok) return accessError(access, requestId, tableId)

const { triggered } = await runWorkflowColumn({
tableId,
workspaceId,
groupIds,
mode: runMode,
rowIds,
requestId,
})
return NextResponse.json({ success: true, data: { triggered } })
} catch (error) {
if (error instanceof Error && error.message === 'Invalid workspace ID') {
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
}
logger.error(`run-column failed:`, error)
return NextResponse.json({ error: 'Failed to run columns' }, { status: 500 })
}
})
Loading
Loading