This commit is contained in:
Lakee Sivaraya
2026-01-14 11:44:31 -08:00
parent fc6dbcf066
commit 48250f5ed8
8 changed files with 192 additions and 168 deletions

View File

@@ -105,7 +105,8 @@ export function TableDataViewer() {
const res = await fetch(`/api/table/${tableId}?workspaceId=${workspaceId}`)
if (!res.ok) throw new Error('Failed to fetch table')
const json = await res.json()
return json.table as TableData
const data = json.data || json
return data.table as TableData
},
})
@@ -135,7 +136,8 @@ export function TableDataViewer() {
const res = await fetch(`/api/table/${tableId}/rows?${params}`)
if (!res.ok) throw new Error('Failed to fetch rows')
return res.json()
const json = await res.json()
return json.data || json
},
enabled: !!tableData,
})

View File

@@ -119,7 +119,8 @@ export function FilterFormat({
const response = await fetch(`/api/table/${tableIdValue}?workspaceId=${workspaceId}`)
if (!response.ok) return
const data = await response.json()
const result = await response.json()
const data = result.data || result
const cols = data.table?.schema?.columns || []
setDynamicColumns(
cols.map((col: { name: string }) => ({ value: col.name, label: col.name }))

View File

@@ -31,6 +31,7 @@ export { InputFormat } from './starter/input-format'
export { SubBlockInputController } from './sub-block-input-controller'
export { Switch } from './switch/switch'
export { Table } from './table/table'
export { TableSelector } from './table-selector/table-selector'
export { Text } from './text/text'
export { TimeInput } from './time-input/time-input'
export { ToolInput } from './tool-input/tool-input'

View File

@@ -110,7 +110,8 @@ export function SortFormat({
const response = await fetch(`/api/table/${tableIdValue}?workspaceId=${workspaceId}`)
if (!response.ok) return
const data = await response.json()
const result = await response.json()
const data = result.data || result
const cols = data.table?.schema?.columns || []
// Add built-in columns for sorting
const builtInCols = [

View File

@@ -0,0 +1,148 @@
'use client'
import { useCallback, useEffect, useMemo, useState } from 'react'
import { ExternalLink } from 'lucide-react'
import { useParams } from 'next/navigation'
import { Combobox, type ComboboxOption, Tooltip } from '@/components/emcn'
import { Button } from '@/components/ui/button'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import type { SubBlockConfig } from '@/blocks/types'
interface TableSelectorProps {
blockId: string
subBlock: SubBlockConfig
disabled?: boolean
isPreview?: boolean
previewValue?: string | null
}
interface TableOption {
label: string
id: string
}
/**
* Table selector component with dropdown and link to view table
*
* @remarks
* Provides a dropdown to select workspace tables and an external link
* to navigate directly to the table page view when a table is selected.
*/
export function TableSelector({
blockId,
subBlock,
disabled = false,
isPreview = false,
previewValue,
}: TableSelectorProps) {
const params = useParams()
const workspaceId = params.workspaceId as string
const [storeValue, setStoreValue] = useSubBlockValue<string>(blockId, subBlock.id)
const [tables, setTables] = useState<TableOption[]>([])
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const value = isPreview ? previewValue : storeValue
const tableId = typeof value === 'string' ? value : null
/**
* Fetches available tables from the API
*/
const fetchTables = useCallback(async () => {
if (!workspaceId || isPreview || disabled) return
setIsLoading(true)
setError(null)
try {
const response = await fetch(`/api/table?workspaceId=${workspaceId}`)
if (!response.ok) {
throw new Error('Failed to fetch tables')
}
const data = await response.json()
const tableOptions = (data.data?.tables || []).map((table: { id: string; name: string }) => ({
label: table.name,
id: table.id,
}))
setTables(tableOptions)
} catch (err) {
const errorMessage = err instanceof Error ? err.message : 'Failed to fetch tables'
setError(errorMessage)
setTables([])
} finally {
setIsLoading(false)
}
}, [workspaceId, isPreview, disabled])
useEffect(() => {
if (!isPreview && !disabled && tables.length === 0 && !isLoading && !error) {
void fetchTables()
}
}, [fetchTables, isPreview, disabled, tables.length, isLoading, error])
const options = useMemo<ComboboxOption[]>(() => {
return tables.map((table) => ({
label: table.label.toLowerCase(),
value: table.id,
}))
}, [tables])
const handleChange = useCallback(
(selectedValue: string) => {
if (isPreview || disabled) return
setStoreValue(selectedValue)
},
[isPreview, disabled, setStoreValue]
)
const handleNavigateToTable = useCallback(() => {
if (tableId && workspaceId) {
window.open(`/workspace/${workspaceId}/tables/${tableId}`, '_blank')
}
}, [workspaceId, tableId])
const hasSelectedTable = tableId && !tableId.startsWith('<')
return (
<div className='flex items-center gap-[6px]'>
<div className='flex-1'>
<Combobox
options={options}
value={tableId ?? undefined}
onChange={handleChange}
placeholder={subBlock.placeholder || 'Select a table'}
disabled={disabled || isPreview}
editable={false}
onOpenChange={(open) => {
if (open) {
void fetchTables()
}
}}
isLoading={isLoading}
error={error}
searchable={options.length > 5}
searchPlaceholder='Search...'
/>
</div>
{hasSelectedTable && !isPreview && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
size='sm'
className='h-[30px] w-[30px] flex-shrink-0 p-0'
onClick={handleNavigateToTable}
>
<ExternalLink className='h-[14px] w-[14px] text-[var(--text-secondary)]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p>View table</p>
</Tooltip.Content>
</Tooltip.Root>
)}
</div>
)
}

View File

@@ -1,6 +1,5 @@
import { type JSX, type MouseEvent, memo, useCallback, useRef, useState } from 'react'
import { AlertTriangle, ExternalLink, Wand2 } from 'lucide-react'
import { useParams } from 'next/navigation'
import { type JSX, type MouseEvent, memo, useRef, useState } from 'react'
import { AlertTriangle, Wand2 } from 'lucide-react'
import { Label, Tooltip } from '@/components/emcn/components'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/core/utils/cn'
@@ -39,6 +38,7 @@ import {
SortFormat,
Switch,
Table,
TableSelector,
Text,
TimeInput,
ToolInput,
@@ -47,8 +47,6 @@ import {
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components'
import { useDependsOnGate } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-depends-on-gate'
import type { SubBlockConfig } from '@/blocks/types'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
/**
* Interface for wand control handlers exposed by sub-block inputs
@@ -296,99 +294,6 @@ const arePropsEqual = (prevProps: SubBlockProps, nextProps: SubBlockProps): bool
)
}
/**
* Props for the DropdownWithTableLink component
*/
interface DropdownWithTableLinkProps {
blockId: string
config: SubBlockConfig
isPreview: boolean
previewValue: string | string[] | null | undefined
isDisabled: boolean
handleMouseDown: (e: MouseEvent<HTMLDivElement>) => void
}
/**
* Renders a dropdown with an optional navigation link for table selectors.
* When the dropdown is for selecting a table (tableId), shows an icon button
* to navigate directly to the table page view.
*/
function DropdownWithTableLink({
blockId,
config,
isPreview,
previewValue,
isDisabled,
handleMouseDown,
}: DropdownWithTableLinkProps): JSX.Element {
const params = useParams()
const workspaceId = params.workspaceId as string
const activeWorkflowId = useWorkflowRegistry((s) => s.activeWorkflowId)
const tableId = useSubBlockStore(
useCallback(
(state) => {
if (!activeWorkflowId) return null
const value = state.workflowValues[activeWorkflowId]?.[blockId]?.[config.id]
return typeof value === 'string' ? value : null
},
[activeWorkflowId, blockId, config.id]
)
)
const isTableSelector = config.id === 'tableId'
const hasSelectedTable = isTableSelector && tableId && !tableId.startsWith('<')
const handleNavigateToTable = useCallback(
(e: MouseEvent) => {
e.stopPropagation()
if (tableId && workspaceId) {
window.open(`/workspace/${workspaceId}/tables/${tableId}`, '_blank')
}
},
[workspaceId, tableId]
)
return (
<div onMouseDown={handleMouseDown} className='flex items-center gap-[6px]'>
<div className='flex-1'>
<Dropdown
blockId={blockId}
subBlockId={config.id}
options={config.options as { label: string; id: string }[]}
defaultValue={typeof config.value === 'function' ? config.value({}) : config.value}
placeholder={config.placeholder}
isPreview={isPreview}
previewValue={previewValue}
disabled={isDisabled}
multiSelect={config.multiSelect}
fetchOptions={config.fetchOptions}
fetchOptionById={config.fetchOptionById}
dependsOn={config.dependsOn}
searchable={config.searchable}
/>
</div>
{hasSelectedTable && !isPreview && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
size='sm'
className='h-[30px] w-[30px] flex-shrink-0 p-0'
onClick={handleNavigateToTable}
>
<ExternalLink className='h-[14px] w-[14px] text-[var(--text-secondary)]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
<p>View table</p>
</Tooltip.Content>
</Tooltip.Root>
)}
</div>
)
}
/**
* Renders a single workflow sub-block input based on config.type.
*
@@ -547,14 +452,36 @@ function SubBlockComponent({
case 'dropdown':
return (
<DropdownWithTableLink
blockId={blockId}
config={config}
isPreview={isPreview}
previewValue={previewValue}
isDisabled={isDisabled}
handleMouseDown={handleMouseDown}
/>
<div onMouseDown={handleMouseDown}>
<Dropdown
blockId={blockId}
subBlockId={config.id}
options={config.options as { label: string; id: string }[]}
defaultValue={typeof config.value === 'function' ? config.value({}) : config.value}
placeholder={config.placeholder}
isPreview={isPreview}
previewValue={previewValue}
disabled={isDisabled}
multiSelect={config.multiSelect}
fetchOptions={config.fetchOptions}
fetchOptionById={config.fetchOptionById}
dependsOn={config.dependsOn}
searchable={config.searchable}
/>
</div>
)
case 'table-selector':
return (
<div onMouseDown={handleMouseDown}>
<TableSelector
blockId={blockId}
subBlock={config}
disabled={isDisabled}
isPreview={isPreview}
previewValue={previewValue as string | null}
/>
</div>
)
case 'combobox':

View File

@@ -3,60 +3,6 @@ import { conditionsToFilter, sortConditionsToSort } from '@/lib/table/filter-bui
import type { BlockConfig } from '@/blocks/types'
import type { TableQueryResponse } from '@/tools/table/types'
/**
* Fetches available tables for the dropdown selector.
* Defined outside BlockConfig to maintain stable reference and prevent infinite re-renders.
*/
const fetchTableOptions = async () => {
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
const workspaceId = useWorkflowRegistry.getState().hydration.workspaceId
if (!workspaceId) {
return []
}
try {
const response = await fetch(`/api/table?workspaceId=${workspaceId}`)
if (!response.ok) {
return []
}
const data = await response.json()
return (data.data?.tables || []).map((table: any) => ({
label: table.name,
id: table.id,
}))
} catch (error) {
return []
}
}
/**
* Fetches a specific table option by ID.
* Defined outside BlockConfig to maintain stable reference and prevent infinite re-renders.
*/
const fetchTableOptionById = async (_blockId: string, _subBlockId: string, tableId: string) => {
const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store')
const workspaceId = useWorkflowRegistry.getState().hydration.workspaceId
if (!workspaceId) {
return null
}
try {
const response = await fetch(`/api/table?workspaceId=${workspaceId}`)
if (!response.ok) {
return null
}
const data = await response.json()
const table = (data.data?.tables || []).find((t: any) => t.id === tableId)
return table ? { label: table.name, id: table.id } : null
} catch (error) {
return null
}
}
export const TableBlock: BlockConfig<TableQueryResponse> = {
type: 'table',
name: 'Table',
@@ -91,12 +37,9 @@ export const TableBlock: BlockConfig<TableQueryResponse> = {
{
id: 'tableId',
title: 'Table',
type: 'dropdown',
type: 'table-selector',
placeholder: 'Select a table',
required: true,
options: [],
fetchOptions: fetchTableOptions,
fetchOptionById: fetchTableOptionById,
},
// Row ID for get/update/delete

View File

@@ -82,6 +82,7 @@ export type SubBlockType =
| 'workflow-input-mapper' // Dynamic workflow input mapper based on selected workflow
| 'text' // Read-only text display
| 'router-input' // Router route definitions with descriptions
| 'table-selector' // Table selector with link to view table
/**
* Selector types that require display name hydration