Skip to content

Commit 8d68c8a

Browse files
fix(tables): resource-cell icons, embedded filters, run-state + UI fixes (#4789)
* 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> * fix(tables): single run/stop control, right-aligned row numbers, View-execution guard - table: de-duplicate the run/stop control in the embedded mothership view — drop TableGrid's own embedded run-status bar; it now lives only in the options bar (left-aligned next to Filter + Sort). Removes the orphaned RunStatusControl import + onStopAll/cancelRunsPending props. - data-row: right-align the row number within its box (hugs the right edge, no hover position jump) with a scaled right inset — 2px for ≤3-digit indices, 4px for 4+ so narrow columns don't look over-padded. - table-grid: require a real executionId in the action bar's canViewExecution flag so an error that never produced an execution (enqueue failure → status 'error', executionId null) doesn't offer "View execution". Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(tables): address review — drizzle operators for orphan filter, enabled flag for files query - dispatcher: replace the `not(and(...)) as SQL` cast in countRunningCells with `or(ne(status,'pending'), isNotNull(executionId))` — De Morgan equivalent, fully type-checked, no cast and no hand-written raw SQL. - workspace-files: add an `enabled` option to useWorkspaceFiles; sim-resource cell now passes the real workspaceId with `enabled` instead of '' so the query cache isn't polluted with an empty-key entry. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(tables): "X running" badge counts actual in-flight cells, not dispatch scope The badge derived from `runningCellCount` (the dispatch-scope estimate = rows-ahead × groupCount), which over-counts groups that already finished on rows still inside a dispatch's scope — a cascade where 3 of 4 workflow columns completed read "4 running" instead of "1". Derive `totalRunning` from the live `runningByRowId` map instead (the same per-row source the gutter and action-bar selection already sum), so it reflects cells actually in flight and updates per-cell via SSE rather than only on dispatch-window events. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent fd77bb4 commit 8d68c8a

9 files changed

Lines changed: 294 additions & 58 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: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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(workspaceId, 'active', {
54+
enabled: resourceType === 'file',
55+
})
56+
57+
const workflow =
58+
resourceType === 'workflow' ? workflows.find((w) => w.id === resourceId) : undefined
59+
60+
const name = useMemo(() => {
61+
switch (resourceType) {
62+
case 'workflow':
63+
return workflow?.name
64+
case 'table':
65+
return tables.find((t) => t.id === resourceId)?.name
66+
case 'knowledge':
67+
return knowledgeBases.find((kb) => kb.id === resourceId)?.name
68+
case 'file':
69+
return files.find((f) => f.id === resourceId)?.name
70+
}
71+
}, [resourceType, resourceId, workflow, tables, knowledgeBases, files])
72+
73+
const label = name ?? FALLBACK_LABEL[resourceType]
74+
75+
const context: ChatMessageContext =
76+
resourceType === 'workflow'
77+
? { kind: 'workflow', label, workflowId: resourceId }
78+
: resourceType === 'table'
79+
? { kind: 'table', label, tableId: resourceId }
80+
: resourceType === 'knowledge'
81+
? { kind: 'knowledge', label, knowledgeId: resourceId }
82+
: { kind: 'file', label, fileId: resourceId }
83+
84+
return (
85+
<span className={cn('flex min-w-0 items-center gap-1.5', isEditing && 'invisible')}>
86+
<ContextMentionIcon
87+
context={context}
88+
workflowColor={workflow?.color ?? null}
89+
className='size-[14px] shrink-0 text-[var(--text-icon)]'
90+
/>
91+
<a
92+
href={href}
93+
className={cn(
94+
'min-w-0 overflow-clip text-ellipsis text-[var(--text-primary)] underline underline-offset-2 hover:opacity-70',
95+
isEditing && 'pointer-events-none'
96+
)}
97+
onClick={(e) => e.stopPropagation()}
98+
onDoubleClick={(e) => e.stopPropagation()}
99+
>
100+
{label}
101+
</a>
102+
</span>
103+
)
104+
}

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

Lines changed: 13 additions & 2 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
@@ -98,6 +101,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean {
98101
if (
99102
prev.row !== next.row ||
100103
prev.columns !== next.columns ||
104+
prev.workspaceId !== next.workspaceId ||
101105
prev.rowIndex !== next.rowIndex ||
102106
prev.isFirstRow !== next.isFirstRow ||
103107
prev.editingColumnName !== next.editingColumnName ||
@@ -141,6 +145,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean {
141145
export const DataRow = React.memo(function DataRow({
142146
row,
143147
columns,
148+
workspaceId,
144149
rowIndex,
145150
isFirstRow,
146151
editingColumnName,
@@ -204,7 +209,12 @@ export const DataRow = React.memo(function DataRow({
204209
tabIndex={0}
205210
aria-checked={isRowSelected}
206211
aria-label={`Select row ${rowIndex + 1}`}
207-
className='group/checkbox flex h-[20px] shrink-0 items-center justify-center'
212+
className={cn(
213+
'group/checkbox flex h-[20px] shrink-0 items-center justify-end',
214+
// Lighter right inset for narrow indices (≤3 digits → numDivWidth ≤ 28);
215+
// full 4px once the column widens (4+ digits, numDivWidth ≥ 36).
216+
numDivWidth >= 36 ? 'pr-1' : 'pr-0.5'
217+
)}
208218
style={{ width: numDivWidth }}
209219
onMouseDown={(e) => {
210220
if (e.button !== 0) return
@@ -216,7 +226,7 @@ export const DataRow = React.memo(function DataRow({
216226
>
217227
<span
218228
className={cn(
219-
'text-center text-[var(--text-tertiary)] text-xs tabular-nums',
229+
'text-right text-[var(--text-tertiary)] text-xs tabular-nums',
220230
isRowSelected ? 'hidden' : 'block group-hover/checkbox:hidden'
221231
)}
222232
>
@@ -328,6 +338,7 @@ export const DataRow = React.memo(function DataRow({
328338
)}
329339
<div className={CELL_CONTENT}>
330340
<CellContent
341+
workspaceId={workspaceId}
331342
value={
332343
pendingCellValue && column.name in pendingCellValue
333344
? pendingCellValue[column.name]

0 commit comments

Comments
 (0)