diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx index fc3442c124f..853349ed363 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options-bar/resource-options-bar.tsx @@ -73,6 +73,10 @@ interface ResourceOptionsBarProps { filterActive?: boolean filterTags?: FilterTag[] extras?: ReactNode + /** Right-aligned slot. Unlike `extras` (which sits with the left controls), + * `trailing` is pushed to the far right via `justify-between` — used for the + * table's run/stop control opposite the left-aligned filter/sort. */ + trailing?: ReactNode } export const ResourceOptionsBar = memo(function ResourceOptionsBar({ @@ -83,9 +87,16 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({ filterActive, filterTags, extras, + trailing, }: ResourceOptionsBarProps) { const hasContent = - search || sort || filter || onFilterToggle || extras || (filterTags && filterTags.length > 0) + search || + sort || + filter || + onFilterToggle || + extras || + trailing || + (filterTags && filterTags.length > 0) if (!hasContent) return null return ( @@ -143,6 +154,7 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({ ) : null} {sort && } + {trailing &&
{trailing}
} ) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx index fe6a6bfd3da..065385a9f05 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx @@ -77,7 +77,13 @@ export function resolveCellRender({ // Value wins over pending-upstream: a finished column stays finished even // while other blocks in the group are still running. An empty string is not // a value — it falls through so a completed enrichment can show "Not found". - if (!isEmpty) return { kind: 'value', text: stringifyValue(value) } + // A value that's wholly a resource/URL string renders as a chip/link (any + // column type — workflow output is free-form); otherwise the plain `value` + // kind keeps the typewriter reveal for streaming text. + if (!isEmpty) { + const text = stringifyValue(value) + return resolveLinkKind(text, currentWorkspaceId) ?? { kind: 'value', text } + } if (inFlight && !(groupHasBlockErrors && !blockRunning)) { // A `pending` cell whose jobId starts with `paused-` is mid-pause @@ -109,21 +115,7 @@ export function resolveCellRender({ if (column.type === 'date') return { kind: 'date', text: String(value) } if (column.type === 'string') { const text = stringifyValue(value) - if (currentWorkspaceId) { - const resource = extractSimResourceInfo(text) - if (resource && resource.workspaceId === currentWorkspaceId) { - return { - kind: 'sim-resource', - workspaceId: resource.workspaceId, - resourceType: resource.resourceType, - resourceId: resource.resourceId, - href: resource.href, - } - } - } - const urlInfo = extractUrlInfo(text) - if (urlInfo) return { kind: 'url', text, href: urlInfo.href, domain: urlInfo.domain } - return { kind: 'text', text } + return resolveLinkKind(text, currentWorkspaceId) ?? { kind: 'text', text } } return { kind: 'text', text: stringifyValue(value) } } @@ -134,6 +126,45 @@ function stringifyValue(value: unknown): string { return JSON.stringify(value) } +/** Returns a `sim-resource` cell kind when `text` is a URL pointing to a + * resource in the current workspace, else null. Shared by plain string cells + * and workflow-output value cells so both surface in-workspace resource links + * as tagged chips. */ +function resolveSimResourceKind( + text: string, + currentWorkspaceId: string | undefined +): Extract | null { + if (!currentWorkspaceId) return null + const resource = extractSimResourceInfo(text) + if (!resource || resource.workspaceId !== currentWorkspaceId) return null + return { + kind: 'sim-resource', + workspaceId: resource.workspaceId, + resourceType: resource.resourceType, + resourceId: resource.resourceId, + href: resource.href, + } +} + +/** + * Promotes a cell value that is wholly a resource/URL string to a chip + * (in-workspace resource) or a favicon link, else null. Shared by plain string + * cells and workflow-output value cells. Workflow outputs apply this regardless + * of `column.type` (their type defaults to `json`, so gating on `string` would + * miss URL outputs); a stringified object never matches the whole-string URL + * check, so it stays JSON/text. + */ +function resolveLinkKind( + text: string, + currentWorkspaceId: string | undefined +): Extract | null { + const simKind = resolveSimResourceKind(text, currentWorkspaceId) + if (simKind) return simKind + const urlInfo = extractUrlInfo(text) + if (urlInfo) return { kind: 'url', text, href: urlInfo.href, domain: urlInfo.domain } + return null +} + const BARE_DOMAIN_RE = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/ function extractUrlInfo(text: string): { href: string; domain: string } | null { diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx index 466835d041b..9eb5a8de8e8 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table.tsx @@ -478,14 +478,14 @@ export function Table({ } /> )} - {/* Sort + filter render in both modes. In embedded (mothership) mode there's - no ResourceHeader, so the run/stop control rides in the options bar's - `extras` slot — keeping the bar populated whether or not a run is live. */} + {/* Sort + filter render in both modes (left-aligned). In embedded (mothership) + mode there's no ResourceHeader, so the run/stop control rides in the options + bar's right-aligned `trailing` slot — opposite the left-aligned filter/sort. */} setFilterOpen((prev) => !prev)} filterActive={filterOpen || !!queryOptions.filter} - extras={ + trailing={ embedded && selection.totalRunning > 0 ? (