mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-22 13:28:04 -05:00
chages
This commit is contained in:
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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 }))
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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':
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user