Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ interface CellContentProps {
value: unknown
exec?: RowExecutionMetadata
column: DisplayColumn
/** Current workspace id — lets string cells holding an in-workspace resource
* URL render as a tagged-resource chip instead of a plain external link. */
workspaceId: string
isEditing: boolean
initialCharacter?: string | null
onSave: (value: unknown, reason: SaveReason) => void
Expand All @@ -34,14 +37,22 @@ export function CellContent({
value,
exec,
column,
workspaceId,
isEditing,
initialCharacter,
onSave,
onCancel,
waitingOnLabels,
isEnrichmentOutput,
}: CellContentProps) {
const kind = resolveCellRender({ value, exec, column, waitingOnLabels, isEnrichmentOutput })
const kind = resolveCellRender({
value,
exec,
column,
waitingOnLabels,
isEnrichmentOutput,
currentWorkspaceId: workspaceId,
})

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { RowExecutionMetadata } from '@/lib/table'
import { StatusBadge } from '@/app/workspace/[workspaceId]/logs/utils'
import { storageToDisplay } from '../../../utils'
import type { DisplayColumn } from '../types'
import { SimResourceCell, type SimResourceType } from './sim-resource-cell'

export type CellRenderKind =
// Workflow-output cells
Expand All @@ -26,6 +27,13 @@ export type CellRenderKind =
| { kind: 'json'; text: string }
| { kind: 'date'; text: string }
| { kind: 'url'; text: string; href: string; domain: string }
| {
kind: 'sim-resource'
workspaceId: string
resourceType: SimResourceType
resourceId: string
href: string
}
| { kind: 'text'; text: string }
// Universal fallback
| { kind: 'empty' }
Expand All @@ -38,6 +46,9 @@ interface ResolveCellRenderInput {
/** Column is an enrichment-group output — a completed-but-empty cell renders
* "Not found" rather than a blank, since the enrichment ran and matched nothing. */
isEnrichmentOutput?: boolean
/** Current workspace id — a URL pointing to a resource in this workspace
* renders as a tagged-resource chip rather than a plain external link. */
currentWorkspaceId?: string
}

export function resolveCellRender({
Expand All @@ -46,6 +57,7 @@ export function resolveCellRender({
column,
waitingOnLabels,
isEnrichmentOutput,
currentWorkspaceId,
}: ResolveCellRenderInput): CellRenderKind {
const isNull = value === null || value === undefined
const isEmpty = isNull || value === ''
Expand Down Expand Up @@ -97,6 +109,18 @@ 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 }
Expand Down Expand Up @@ -131,6 +155,43 @@ function extractUrlInfo(text: string): { href: string; domain: string } | null {
return null
}

/** Maps a workspace route section to the sim resource kind it addresses. */
const SIM_RESOURCE_SECTIONS: Record<string, SimResourceType> = {
w: 'workflow',
tables: 'table',
knowledge: 'knowledge',
files: 'file',
}

/**
* Recognizes a `/workspace/{id}/{section}/{resourceId}` URL (absolute or
* relative) pointing to a sim resource and returns its descriptor. The href is
* the pathname so the link stays within the current deployment. Returns null
* for anything that isn't a single-segment resource route.
*/
function extractSimResourceInfo(
text: string
): { workspaceId: string; resourceType: SimResourceType; resourceId: string; href: string } | null {
const trimmed = text.trim()
if (!trimmed) return null
let pathname: string
if (/^https?:\/\//i.test(trimmed)) {
try {
pathname = new URL(trimmed).pathname
} catch {
return null
}
} else if (trimmed.startsWith('/')) {
pathname = trimmed.split(/[?#]/)[0]
} else {
return null
}
const match = pathname.match(/^\/workspace\/([^/]+)\/(w|tables|knowledge|files)\/([^/]+)\/?$/)
if (!match) return null
const [, workspaceId, section, resourceId] = match
return { workspaceId, resourceType: SIM_RESOURCE_SECTIONS[section], resourceId, href: pathname }
}

interface CellRenderProps {
kind: CellRenderKind
isEditing: boolean
Expand Down Expand Up @@ -270,6 +331,17 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle
</span>
)

case 'sim-resource':
return (
<SimResourceCell
workspaceId={kind.workspaceId}
resourceType={kind.resourceType}
resourceId={kind.resourceId}
href={kind.href}
isEditing={isEditing}
/>
)

case 'text':
return (
<span
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
'use client'

import { useMemo } from 'react'
import { cn } from '@/lib/core/utils/cn'
import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon'
import type { ChatMessageContext } from '@/app/workspace/[workspaceId]/home/types'
import { useKnowledgeBasesQuery } from '@/hooks/queries/kb/knowledge'
import { useTablesList } from '@/hooks/queries/tables'
import { useWorkflows } from '@/hooks/queries/workflows'
import { useWorkspaceFiles } from '@/hooks/queries/workspace-files'

/** Sim resource kinds a table cell URL can point to within the current workspace. */
export type SimResourceType = 'workflow' | 'table' | 'knowledge' | 'file'

const FALLBACK_LABEL: Record<SimResourceType, string> = {
workflow: 'Workflow',
table: 'Table',
knowledge: 'Knowledge base',
file: 'File',
}

interface SimResourceCellProps {
/** Always the current workspace — the resolver only emits this kind for same-workspace URLs. */
workspaceId: string
resourceType: SimResourceType
resourceId: string
/** In-app pathname the resource link navigates to. */
href: string
isEditing: boolean
}

/**
* Renders a cell whose value is a URL pointing to a sim resource in the current
* workspace as a tagged-resource chip — the same icon (and per-workflow colored
* square) used for @-style resource mentions, plus the resource's name as a link.
* Only the list matching `resourceType` is fetched; the other queries stay
* disabled so a sim-resource cell subscribes to a single shared list.
*/
export function SimResourceCell({
workspaceId,
resourceType,
resourceId,
href,
isEditing,
}: SimResourceCellProps) {
const { data: workflows = [] } = useWorkflows(
resourceType === 'workflow' ? workspaceId : undefined
)
const { data: tables = [] } = useTablesList(resourceType === 'table' ? workspaceId : undefined)
const { data: knowledgeBases = [] } = useKnowledgeBasesQuery(workspaceId, {
enabled: resourceType === 'knowledge',
})
const { data: files = [] } = useWorkspaceFiles(workspaceId, 'active', {
enabled: resourceType === 'file',
})

const workflow =
resourceType === 'workflow' ? workflows.find((w) => w.id === resourceId) : undefined

const name = useMemo(() => {
switch (resourceType) {
case 'workflow':
return workflow?.name
case 'table':
return tables.find((t) => t.id === resourceId)?.name
case 'knowledge':
return knowledgeBases.find((kb) => kb.id === resourceId)?.name
case 'file':
return files.find((f) => f.id === resourceId)?.name
}
}, [resourceType, resourceId, workflow, tables, knowledgeBases, files])

const label = name ?? FALLBACK_LABEL[resourceType]

const context: ChatMessageContext =
resourceType === 'workflow'
? { kind: 'workflow', label, workflowId: resourceId }
: resourceType === 'table'
? { kind: 'table', label, tableId: resourceId }
: resourceType === 'knowledge'
? { kind: 'knowledge', label, knowledgeId: resourceId }
: { kind: 'file', label, fileId: resourceId }

return (
<span className={cn('flex min-w-0 items-center gap-1.5', isEditing && 'invisible')}>
<ContextMentionIcon
context={context}
workflowColor={workflow?.color ?? null}
className='size-[14px] shrink-0 text-[var(--text-icon)]'
/>
<a
href={href}
className={cn(
'min-w-0 overflow-clip text-ellipsis text-[var(--text-primary)] underline underline-offset-2 hover:opacity-70',
isEditing && 'pointer-events-none'
)}
onClick={(e) => e.stopPropagation()}
onDoubleClick={(e) => e.stopPropagation()}
>
{label}
</a>
</span>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import { type NormalizedSelection, resolveCellExec } from './utils'
export interface DataRowProps {
row: TableRowType
columns: DisplayColumn[]
/** Current workspace id — forwarded to cells so in-workspace resource URLs
* render as tagged-resource chips. */
workspaceId: string
rowIndex: number
isFirstRow: boolean
editingColumnName: string | null
Expand Down Expand Up @@ -94,6 +97,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean {
if (
prev.row !== next.row ||
prev.columns !== next.columns ||
prev.workspaceId !== next.workspaceId ||
prev.rowIndex !== next.rowIndex ||
prev.isFirstRow !== next.isFirstRow ||
prev.editingColumnName !== next.editingColumnName ||
Expand Down Expand Up @@ -135,6 +139,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean {
export const DataRow = React.memo(function DataRow({
row,
columns,
workspaceId,
rowIndex,
isFirstRow,
editingColumnName,
Expand Down Expand Up @@ -196,7 +201,12 @@ export const DataRow = React.memo(function DataRow({
tabIndex={0}
aria-checked={isRowSelected}
aria-label={`Select row ${rowIndex + 1}`}
className='group/checkbox flex h-[20px] shrink-0 items-center justify-center'
className={cn(
'group/checkbox flex h-[20px] shrink-0 items-center justify-end',
// Lighter right inset for narrow indices (≤3 digits → numDivWidth ≤ 28);
// full 4px once the column widens (4+ digits, numDivWidth ≥ 36).
numDivWidth >= 36 ? 'pr-1' : 'pr-0.5'
)}
style={{ width: numDivWidth }}
onMouseDown={(e) => {
if (e.button !== 0) return
Expand All @@ -208,7 +218,7 @@ export const DataRow = React.memo(function DataRow({
>
<span
className={cn(
'text-center text-[var(--text-tertiary)] text-xs tabular-nums',
'text-right text-[var(--text-tertiary)] text-xs tabular-nums',
isRowSelected ? 'hidden' : 'block group-hover/checkbox:hidden'
)}
>
Expand Down Expand Up @@ -310,6 +320,7 @@ export const DataRow = React.memo(function DataRow({
)}
<div className={CELL_CONTENT}>
<CellContent
workspaceId={workspaceId}
value={
pendingCellValue && column.name in pendingCellValue
? pendingCellValue[column.name]
Expand Down
Loading
Loading