Skip to content

Commit d0cac34

Browse files
fix(tables): resource-cell icons, embedded filters, run-count + queued fixes
- table-grid: render in-workspace resource URLs (workflow/table/KB/file) as tagged-resource cells reusing ContextMentionIcon (colored square for workflows), matching @-mention chips; only the matching list is fetched. - table-grid: fix row-number sticky cell overflow — reserve the full run/stop button area (30px, not 16px) so wide row indices don't clip. - table-grid: show an infinite-scroll loading spinner while the next page loads instead of looking like the end of the table. - table: surface sort + filter (and run/stop via the options-bar extras slot) in the embedded mothership table resource view. - table-grid/utils: stop the dispatch overlay from optimistically painting autoRun=false cells Queued for auto-fire dispatches — the dispatcher skips those groups ('autoRun-off'); manual runs still show Queued (manual-bypass). - dispatcher: exclude orphan pre-stamps (pending + executionId null) from countRunningCells so the "X running" badge doesn't stick above zero. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e62c3ad commit d0cac34

8 files changed

Lines changed: 272 additions & 34 deletions

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-content.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ interface CellContentProps {
1010
value: unknown
1111
exec?: RowExecutionMetadata
1212
column: DisplayColumn
13+
/** Current workspace id — lets string cells holding an in-workspace resource
14+
* URL render as a tagged-resource chip instead of a plain external link. */
15+
workspaceId: string
1316
isEditing: boolean
1417
initialCharacter?: string | null
1518
onSave: (value: unknown, reason: SaveReason) => void
@@ -34,14 +37,22 @@ export function CellContent({
3437
value,
3538
exec,
3639
column,
40+
workspaceId,
3741
isEditing,
3842
initialCharacter,
3943
onSave,
4044
onCancel,
4145
waitingOnLabels,
4246
isEnrichmentOutput,
4347
}: CellContentProps) {
44-
const kind = resolveCellRender({ value, exec, column, waitingOnLabels, isEnrichmentOutput })
48+
const kind = resolveCellRender({
49+
value,
50+
exec,
51+
column,
52+
waitingOnLabels,
53+
isEnrichmentOutput,
54+
currentWorkspaceId: workspaceId,
55+
})
4556

4657
return (
4758
<>

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import type { RowExecutionMetadata } from '@/lib/table'
99
import { StatusBadge } from '@/app/workspace/[workspaceId]/logs/utils'
1010
import { storageToDisplay } from '../../../utils'
1111
import type { DisplayColumn } from '../types'
12+
import { SimResourceCell, type SimResourceType } from './sim-resource-cell'
1213

1314
export type CellRenderKind =
1415
// Workflow-output cells
@@ -26,6 +27,13 @@ export type CellRenderKind =
2627
| { kind: 'json'; text: string }
2728
| { kind: 'date'; text: string }
2829
| { kind: 'url'; text: string; href: string; domain: string }
30+
| {
31+
kind: 'sim-resource'
32+
workspaceId: string
33+
resourceType: SimResourceType
34+
resourceId: string
35+
href: string
36+
}
2937
| { kind: 'text'; text: string }
3038
// Universal fallback
3139
| { kind: 'empty' }
@@ -38,6 +46,9 @@ interface ResolveCellRenderInput {
3846
/** Column is an enrichment-group output — a completed-but-empty cell renders
3947
* "Not found" rather than a blank, since the enrichment ran and matched nothing. */
4048
isEnrichmentOutput?: boolean
49+
/** Current workspace id — a URL pointing to a resource in this workspace
50+
* renders as a tagged-resource chip rather than a plain external link. */
51+
currentWorkspaceId?: string
4152
}
4253

4354
export function resolveCellRender({
@@ -46,6 +57,7 @@ export function resolveCellRender({
4657
column,
4758
waitingOnLabels,
4859
isEnrichmentOutput,
60+
currentWorkspaceId,
4961
}: ResolveCellRenderInput): CellRenderKind {
5062
const isNull = value === null || value === undefined
5163
const isEmpty = isNull || value === ''
@@ -97,6 +109,18 @@ export function resolveCellRender({
97109
if (column.type === 'date') return { kind: 'date', text: String(value) }
98110
if (column.type === 'string') {
99111
const text = stringifyValue(value)
112+
if (currentWorkspaceId) {
113+
const resource = extractSimResourceInfo(text)
114+
if (resource && resource.workspaceId === currentWorkspaceId) {
115+
return {
116+
kind: 'sim-resource',
117+
workspaceId: resource.workspaceId,
118+
resourceType: resource.resourceType,
119+
resourceId: resource.resourceId,
120+
href: resource.href,
121+
}
122+
}
123+
}
100124
const urlInfo = extractUrlInfo(text)
101125
if (urlInfo) return { kind: 'url', text, href: urlInfo.href, domain: urlInfo.domain }
102126
return { kind: 'text', text }
@@ -131,6 +155,43 @@ function extractUrlInfo(text: string): { href: string; domain: string } | null {
131155
return null
132156
}
133157

158+
/** Maps a workspace route section to the sim resource kind it addresses. */
159+
const SIM_RESOURCE_SECTIONS: Record<string, SimResourceType> = {
160+
w: 'workflow',
161+
tables: 'table',
162+
knowledge: 'knowledge',
163+
files: 'file',
164+
}
165+
166+
/**
167+
* Recognizes a `/workspace/{id}/{section}/{resourceId}` URL (absolute or
168+
* relative) pointing to a sim resource and returns its descriptor. The href is
169+
* the pathname so the link stays within the current deployment. Returns null
170+
* for anything that isn't a single-segment resource route.
171+
*/
172+
function extractSimResourceInfo(
173+
text: string
174+
): { workspaceId: string; resourceType: SimResourceType; resourceId: string; href: string } | null {
175+
const trimmed = text.trim()
176+
if (!trimmed) return null
177+
let pathname: string
178+
if (/^https?:\/\//i.test(trimmed)) {
179+
try {
180+
pathname = new URL(trimmed).pathname
181+
} catch {
182+
return null
183+
}
184+
} else if (trimmed.startsWith('/')) {
185+
pathname = trimmed.split(/[?#]/)[0]
186+
} else {
187+
return null
188+
}
189+
const match = pathname.match(/^\/workspace\/([^/]+)\/(w|tables|knowledge|files)\/([^/]+)\/?$/)
190+
if (!match) return null
191+
const [, workspaceId, section, resourceId] = match
192+
return { workspaceId, resourceType: SIM_RESOURCE_SECTIONS[section], resourceId, href: pathname }
193+
}
194+
134195
interface CellRenderProps {
135196
kind: CellRenderKind
136197
isEditing: boolean
@@ -270,6 +331,17 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle
270331
</span>
271332
)
272333

334+
case 'sim-resource':
335+
return (
336+
<SimResourceCell
337+
workspaceId={kind.workspaceId}
338+
resourceType={kind.resourceType}
339+
resourceId={kind.resourceId}
340+
href={kind.href}
341+
isEditing={isEditing}
342+
/>
343+
)
344+
273345
case 'text':
274346
return (
275347
<span
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
'use client'
2+
3+
import { useMemo } from 'react'
4+
import { cn } from '@/lib/core/utils/cn'
5+
import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon'
6+
import type { ChatMessageContext } from '@/app/workspace/[workspaceId]/home/types'
7+
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
8+
import { useTablesList } from '@/hooks/queries/tables'
9+
import { useWorkflows } from '@/hooks/queries/workflows'
10+
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'
11+
12+
/** Sim resource kinds a table cell URL can point to within the current workspace. */
13+
export type SimResourceType = 'workflow' | 'table' | 'knowledge' | 'file'
14+
15+
const FALLBACK_LABEL: Record<SimResourceType, string> = {
16+
workflow: 'Workflow',
17+
table: 'Table',
18+
knowledge: 'Knowledge base',
19+
file: 'File',
20+
}
21+
22+
interface SimResourceCellProps {
23+
/** Always the current workspace — the resolver only emits this kind for same-workspace URLs. */
24+
workspaceId: string
25+
resourceType: SimResourceType
26+
resourceId: string
27+
/** In-app pathname the resource link navigates to. */
28+
href: string
29+
isEditing: boolean
30+
}
31+
32+
/**
33+
* Renders a cell whose value is a URL pointing to a sim resource in the current
34+
* workspace as a tagged-resource chip — the same icon (and per-workflow colored
35+
* square) used for @-style resource mentions, plus the resource's name as a link.
36+
* Only the list matching `resourceType` is fetched; the other queries stay
37+
* disabled so a sim-resource cell subscribes to a single shared list.
38+
*/
39+
export function SimResourceCell({
40+
workspaceId,
41+
resourceType,
42+
resourceId,
43+
href,
44+
isEditing,
45+
}: SimResourceCellProps) {
46+
const { data: workflows = [] } = useWorkflows(
47+
resourceType === 'workflow' ? workspaceId : undefined
48+
)
49+
const { data: tables = [] } = useTablesList(resourceType === 'table' ? workspaceId : undefined)
50+
const { data: knowledgeBases = [] } = useKnowledgeBasesQuery(workspaceId, {
51+
enabled: resourceType === 'knowledge',
52+
})
53+
const { data: files = [] } = useWorkspaceFiles(resourceType === 'file' ? workspaceId : '')
54+
55+
const workflow =
56+
resourceType === 'workflow' ? workflows.find((w) => w.id === resourceId) : undefined
57+
58+
const name = useMemo(() => {
59+
switch (resourceType) {
60+
case 'workflow':
61+
return workflow?.name
62+
case 'table':
63+
return tables.find((t) => t.id === resourceId)?.name
64+
case 'knowledge':
65+
return knowledgeBases.find((kb) => kb.id === resourceId)?.name
66+
case 'file':
67+
return files.find((f) => f.id === resourceId)?.name
68+
}
69+
}, [resourceType, resourceId, workflow, tables, knowledgeBases, files])
70+
71+
const label = name ?? FALLBACK_LABEL[resourceType]
72+
73+
const context: ChatMessageContext =
74+
resourceType === 'workflow'
75+
? { kind: 'workflow', label, workflowId: resourceId }
76+
: resourceType === 'table'
77+
? { kind: 'table', label, tableId: resourceId }
78+
: resourceType === 'knowledge'
79+
? { kind: 'knowledge', label, knowledgeId: resourceId }
80+
: { kind: 'file', label, fileId: resourceId }
81+
82+
return (
83+
<span className={cn('flex min-w-0 items-center gap-1.5', isEditing && 'invisible')}>
84+
<ContextMentionIcon
85+
context={context}
86+
workflowColor={workflow?.color ?? null}
87+
className='size-[14px] shrink-0 text-[var(--text-icon)]'
88+
/>
89+
<a
90+
href={href}
91+
className={cn(
92+
'min-w-0 overflow-clip text-ellipsis text-[var(--text-primary)] underline underline-offset-2 hover:opacity-70',
93+
isEditing && 'pointer-events-none'
94+
)}
95+
onClick={(e) => e.stopPropagation()}
96+
onDoubleClick={(e) => e.stopPropagation()}
97+
>
98+
{label}
99+
</a>
100+
</span>
101+
)
102+
}

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/data-row.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ import { type NormalizedSelection, resolveCellExec } from './utils'
2323
export interface DataRowProps {
2424
row: TableRowType
2525
columns: DisplayColumn[]
26+
/** Current workspace id — forwarded to cells so in-workspace resource URLs
27+
* render as tagged-resource chips. */
28+
workspaceId: string
2629
rowIndex: number
2730
isFirstRow: boolean
2831
editingColumnName: string | null
@@ -94,6 +97,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean {
9497
if (
9598
prev.row !== next.row ||
9699
prev.columns !== next.columns ||
100+
prev.workspaceId !== next.workspaceId ||
97101
prev.rowIndex !== next.rowIndex ||
98102
prev.isFirstRow !== next.isFirstRow ||
99103
prev.editingColumnName !== next.editingColumnName ||
@@ -135,6 +139,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean {
135139
export const DataRow = React.memo(function DataRow({
136140
row,
137141
columns,
142+
workspaceId,
138143
rowIndex,
139144
isFirstRow,
140145
editingColumnName,
@@ -310,6 +315,7 @@ export const DataRow = React.memo(function DataRow({
310315
)}
311316
<div className={CELL_CONTENT}>
312317
<CellContent
318+
workspaceId={workspaceId}
313319
value={
314320
pendingCellValue && column.name in pendingCellValue
315321
? pendingCellValue[column.name]

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { useVirtualizer } from '@tanstack/react-virtual'
77
import { useParams } from 'next/navigation'
88
import { usePostHog } from 'posthog-js/react'
99
import { Skeleton, toast, useToast } from '@/components/emcn'
10-
import { TableX } from '@/components/emcn/icons'
10+
import { Loader, TableX } from '@/components/emcn/icons'
1111
import type { RunLimit, RunMode } from '@/lib/api/contracts/tables'
1212
import { cn } from '@/lib/core/utils/cn'
1313
import { captureEvent } from '@/lib/posthog/client'
@@ -3331,6 +3331,7 @@ export function TableGrid({
33313331
key={row.id}
33323332
row={row}
33333333
columns={displayColumns}
3334+
workspaceId={workspaceId}
33343335
rowIndex={index}
33353336
isFirstRow={index === 0}
33363337
editingColumnName={
@@ -3372,6 +3373,18 @@ export function TableGrid({
33723373
/>
33733374
</tr>
33743375
)}
3376+
{isFetchingNextPage && (
3377+
<tr>
3378+
<td colSpan={displayColumns.length + 1} className='h-[35px] p-0'>
3379+
<div className='flex items-center justify-center'>
3380+
<Loader
3381+
animate
3382+
className='size-[14px] shrink-0 text-[var(--text-tertiary)]'
3383+
/>
3384+
</div>
3385+
</td>
3386+
</tr>
3387+
)}
33753388
</>
33763389
)
33773390
})()

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,10 @@ export function checkboxColLayout(
4949
): { colWidth: number; numDivWidth: number } {
5050
const digits = maxRows > 0 ? Math.floor(Math.log10(maxRows)) + 1 : 1
5151
const numDivWidth = Math.max(20, digits * 8 + 4)
52-
const colWidth = Math.max(32, numDivWidth + 8) + (hasWorkflowCols ? 16 : 0)
52+
// When workflow columns are present a 20px run/stop button sits to the right of
53+
// the number, separated by a 6px gap and a 4px right pad — 30px total. Reserving
54+
// only the button width clipped the number on tables with many (wide) row indices.
55+
const colWidth = Math.max(32, numDivWidth + 8) + (hasWorkflowCols ? 30 : 0)
5356
return { colWidth, numDivWidth }
5457
}
5558

@@ -196,6 +199,12 @@ export function resolveCellExec(
196199
// cell SSE) cover the actual rows instead.
197200
if (d.limit) continue
198201
if (!d.scope.groupIds.includes(group.id)) continue
202+
// Auto-fire dispatches (row writes / schema changes) scope every group but
203+
// the dispatcher honors `autoRun: false` per-cell ('autoRun-off'), so those
204+
// cells never actually run — don't optimistically paint them Queued. Manual
205+
// runs (Run all / Run column) bypass autoRun and DO run them, so keep the
206+
// overlay's Queued there.
207+
if (!d.isManualRun && group.autoRun === false) continue
199208
if (d.scope.rowIds && !d.scope.rowIds.includes(row.id)) continue
200209
if (row.position <= d.cursor) continue
201210
return {

0 commit comments

Comments
 (0)