Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
@@ -1,8 +1,8 @@
'use client'

import { useCallback, useMemo } from 'react'
import { generateId } from '@sim/utils/id'
import type { ComboboxOption } from '@/components/emcn'
import { Plus } from 'lucide-react'
import { Button } from '@/components/emcn'
import { useTableColumns } from '@/lib/table/hooks'
import type { FilterRule } from '@/lib/table/query-builder/constants'
import { useFilterBuilder } from '@/lib/table/query-builder/use-query-builder'
Expand All @@ -22,15 +22,6 @@ interface FilterBuilderProps {
tableIdSubBlockId?: string
}

const createDefaultRule = (columns: ComboboxOption[]): FilterRule => ({
id: generateId(),
logicalOperator: 'and',
column: columns[0]?.value || '',
operator: 'eq',
value: '',
collapsed: false,
})

/** Visual builder for table filter rules in workflow blocks. */
export function FilterBuilder({
blockId,
Expand All @@ -52,8 +43,7 @@ export function FilterBuilder({
}, [propColumns, dynamicColumns])

const value = isPreview ? previewValue : storeValue
const rules: FilterRule[] =
Array.isArray(value) && value.length > 0 ? value : [createDefaultRule(columns)]
const rules: FilterRule[] = Array.isArray(value) ? value : []
const isReadOnly = isPreview || disabled

const { comparisonOptions, logicalOptions, addRule, removeRule, updateRule } = useFilterBuilder({
Expand Down Expand Up @@ -86,15 +76,25 @@ export function FilterBuilder({
const handleRemoveRule = useCallback(
(id: string) => {
if (isReadOnly) return
if (rules.length === 1) {
setStoreValue([createDefaultRule(columns)])
} else {
removeRule(id)
}
removeRule(id)
},
[isReadOnly, rules, columns, setStoreValue, removeRule]
[isReadOnly, removeRule]
)

if (rules.length === 0) {
if (isReadOnly) return null
return (
<Button
variant='ghost'
onClick={addRule}
className='h-7 w-full justify-start gap-1.5 border border-[var(--border-1)] border-dashed text-[var(--text-muted)] text-small'
>
<Plus className='size-[14px]' />
Add filter condition
</Button>
)
}

return (
<div className='space-y-2'>
{rules.map((rule, index) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import { useCallback, useMemo } from 'react'
import { generateId } from '@sim/utils/id'
import type { ComboboxOption } from '@/components/emcn'
import { Plus } from 'lucide-react'
import { Button, type ComboboxOption } from '@/components/emcn'
import { useTableColumns } from '@/lib/table/hooks'
import { SORT_DIRECTIONS, type SortRule } from '@/lib/table/query-builder/constants'
import { useCanonicalSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-canonical-sub-block-value'
Expand Down Expand Up @@ -51,8 +52,7 @@ export function SortBuilder({
)

const value = isPreview ? previewValue : storeValue
const rules: SortRule[] =
Array.isArray(value) && value.length > 0 ? value : [createDefaultRule(columns)]
const rules: SortRule[] = Array.isArray(value) ? value : []
const isReadOnly = isPreview || disabled

const addRule = useCallback(() => {
Expand All @@ -63,13 +63,9 @@ export function SortBuilder({
const removeRule = useCallback(
(id: string) => {
if (isReadOnly) return
if (rules.length === 1) {
setStoreValue([createDefaultRule(columns)])
} else {
setStoreValue(rules.filter((r) => r.id !== id))
}
setStoreValue(rules.filter((r) => r.id !== id))
},
[isReadOnly, rules, columns, setStoreValue]
[isReadOnly, rules, setStoreValue]
)

const updateRule = useCallback(
Expand All @@ -88,6 +84,20 @@ export function SortBuilder({
[isReadOnly, rules, setStoreValue]
)

if (rules.length === 0) {
if (isReadOnly) return null
return (
<Button
variant='ghost'
onClick={addRule}
className='h-7 w-full justify-start gap-1.5 border border-[var(--border-1)] border-dashed text-[var(--text-muted)] text-small'
>
<Plus className='size-[14px]' />
Add sort
</Button>
)
}

return (
<div className='space-y-2'>
{rules.map((rule, index) => (
Expand Down
18 changes: 18 additions & 0 deletions apps/sim/blocks/blocks/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ interface TableBlockParams {
sortBuilder?: unknown
bulkFilterMode?: string
bulkFilterBuilder?: unknown
conflictColumn?: string
}

/** Normalized params after parsing, ready for tool request body */
Expand All @@ -70,6 +71,7 @@ interface ParsedParams {
sort?: unknown
limit?: number
offset?: number
conflictTarget?: string
}

/** Transforms raw block params into tool request params for each operation */
Expand All @@ -82,6 +84,7 @@ const paramTransformers: Record<string, (params: TableBlockParams) => ParsedPara
upsert_row: (params) => ({
tableId: params.tableId,
data: parseJSON(params.data, 'Row Data'),
conflictTarget: params.conflictColumn || undefined,
}),

batch_insert_rows: (params) => ({
Expand Down Expand Up @@ -275,6 +278,16 @@ Return ONLY the data JSON:`,
},
},

// Upsert - which unique column to match on (defaults to the first unique column)
{
id: 'conflictColumn',
title: 'Conflict Column',
type: 'short-input',
placeholder: 'Unique column to match on (required if the table has multiple unique columns)',
dependsOn: ['tableId'],
condition: { field: 'operation', value: 'upsert_row' },
},

// Batch Insert - multiple rows
{
id: 'rows',
Expand Down Expand Up @@ -631,6 +644,11 @@ Return ONLY the sort JSON:`,
sortBuilder: { type: 'json', description: 'Visual sort builder conditions' },
sort: { type: 'json', description: 'Sort order (JSON)' },
offset: { type: 'number', description: 'Query result offset' },
conflictColumn: {
type: 'string',
description:
'Unique column to match on for upsert (required if the table has multiple unique columns)',
},
},

outputs: {
Expand Down
4 changes: 4 additions & 0 deletions apps/sim/lib/table/query-builder/converters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ export function filterRulesToFilter(rules: FilterRule[]): Filter | null {
let currentGroup: Filter = {}

for (const rule of rules) {
// Skip incomplete rows (no column selected) so a blank builder row never
// serializes to a `{ '': ... }` predicate.
if (!rule.column) continue
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated

const isOr = rule.logicalOperator === 'or'
const ruleValue = toRuleValue(rule.operator, rule.value)

Expand Down
2 changes: 1 addition & 1 deletion apps/sim/lib/table/rows/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -496,7 +496,7 @@ export async function upsertRow(
targetColumnKey = getColumnId(uniqueColumns[0])
} else {
throw new Error(
`Table has multiple unique columns (${uniqueColumns.map((c) => c.name).join(', ')}). Specify conflictTarget to indicate which column to match on.`
`Table has multiple unique columns (${uniqueColumns.map((c) => c.name).join(', ')}). Specify a conflict column to indicate which one to match on.`
)
}

Expand Down
2 changes: 2 additions & 0 deletions apps/sim/tools/table/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ export interface TableListParams {
export interface TableRowInsertParams {
tableId: string
data: RowData
/** Unique column to match on for upsert; ignored by plain insert */
conflictTarget?: string
_context?: WorkflowToolExecutionContext
}

Expand Down
8 changes: 8 additions & 0 deletions apps/sim/tools/table/upsert_row.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ export const tableUpsertRowTool: ToolConfig<TableRowInsertParams, TableUpsertRes
description: 'Row data to insert or update',
visibility: 'user-or-llm',
},
conflictTarget: {
type: 'string',
required: false,
description:
'Unique column to match on. Required only when the table has more than one unique column.',
visibility: 'user-only',
},
},

request: {
Expand All @@ -45,6 +52,7 @@ export const tableUpsertRowTool: ToolConfig<TableRowInsertParams, TableUpsertRes
return {
data: params.data,
workspaceId,
...(params.conflictTarget ? { conflictTarget: params.conflictTarget } : {}),
}
},
},
Expand Down
Loading