This commit is contained in:
Lakee Sivaraya
2026-01-16 11:40:32 -08:00
parent 271375df9b
commit 26d96624af
17 changed files with 253 additions and 549 deletions

View File

@@ -4,80 +4,56 @@ import { useCallback, useMemo, useState } from 'react'
import { ArrowDownAZ, ArrowUpAZ, Loader2, Plus, X } from 'lucide-react'
import { nanoid } from 'nanoid'
import { Button, Combobox, Input } from '@/components/emcn'
import type { FilterCondition, SortCondition } from '@/lib/table/filters/constants'
import type { FilterRule, SortRule } from '@/lib/table/filters/constants'
import { useFilterBuilder } from '@/lib/table/filters/use-builder'
import { conditionsToFilter, sortConditionToSort } from '@/lib/table/filters/utils'
import { filterRulesToFilter, sortRuleToSort } from '@/lib/table/filters/utils'
import type { ColumnDefinition, Filter, Sort } from '@/lib/table/types'
/**
* Result of applying query builder filters and sorts.
* Contains the converted API-ready filter and sort objects.
*/
/** Query result containing API-ready filter and sort objects. */
export interface BuilderQueryResult {
/** MongoDB-style filter object for API queries */
filter: Filter | null
/** Sort specification for API queries */
sort: Sort | null
}
/**
* Column definition for filter building (subset of ColumnDefinition).
*/
type Column = Pick<ColumnDefinition, 'name' | 'type'>
/**
* Props for the TableQueryBuilder component.
*/
interface TableQueryBuilderProps {
/** Available columns for filtering */
columns: Column[]
/** Callback when query options should be applied */
onApply: (options: BuilderQueryResult) => void
/** Callback to add a new row */
onAddRow: () => void
/** Whether a query is currently loading */
isLoading?: boolean
}
/**
* Component for building filter and sort queries for table data.
*
* Provides a visual interface for:
* - Adding multiple filter conditions with AND/OR logic
* - Configuring sort column and direction
* - Applying or clearing the query
* ```
*/
/** Visual query builder for filtering and sorting table data. */
export function TableQueryBuilder({
columns,
onApply,
onAddRow,
isLoading = false,
}: TableQueryBuilderProps) {
const [conditions, setConditions] = useState<FilterCondition[]>([])
const [sortCondition, setSortCondition] = useState<SortCondition | null>(null)
const [rules, setRules] = useState<FilterRule[]>([])
const [sortRule, setSortRule] = useState<SortRule | null>(null)
const columnOptions = useMemo(
() => columns.map((col) => ({ value: col.name, label: col.name })),
[columns]
)
// Use the shared filter builder hook
const {
comparisonOptions,
logicalOptions,
sortDirectionOptions,
addCondition: handleAddCondition,
removeCondition: handleRemoveCondition,
updateCondition: handleUpdateCondition,
addRule: handleAddRule,
removeRule: handleRemoveRule,
updateRule: handleUpdateRule,
} = useFilterBuilder({
columns: columnOptions,
conditions,
setConditions,
rules,
setRules,
})
const handleAddSort = useCallback(() => {
setSortCondition({
setSortRule({
id: nanoid(),
column: columns[0]?.name || '',
direction: 'asc',
@@ -85,67 +61,64 @@ export function TableQueryBuilder({
}, [columns])
const handleRemoveSort = useCallback(() => {
setSortCondition(null)
setSortRule(null)
}, [])
const handleApply = useCallback(() => {
const filter = conditionsToFilter(conditions)
const sort = sortConditionToSort(sortCondition)
const filter = filterRulesToFilter(rules)
const sort = sortRuleToSort(sortRule)
onApply({ filter, sort })
}, [conditions, sortCondition, onApply])
}, [rules, sortRule, onApply])
const handleClear = useCallback(() => {
setConditions([])
setSortCondition(null)
setRules([])
setSortRule(null)
onApply({
filter: null,
sort: null,
})
}, [onApply])
const hasChanges = conditions.length > 0 || sortCondition !== null
const hasChanges = rules.length > 0 || sortRule !== null
return (
<div className='flex flex-col gap-[8px]'>
{/* Filter Conditions */}
{conditions.map((condition, index) => (
<FilterConditionRow
key={condition.id}
condition={condition}
{rules.map((rule, index) => (
<FilterRuleRow
key={rule.id}
rule={rule}
index={index}
columnOptions={columnOptions}
comparisonOptions={comparisonOptions}
logicalOptions={logicalOptions}
onUpdate={handleUpdateCondition}
onRemove={handleRemoveCondition}
onUpdate={handleUpdateRule}
onRemove={handleRemoveRule}
onApply={handleApply}
/>
))}
{/* Sort Row */}
{sortCondition && (
<SortConditionRow
sortCondition={sortCondition}
{sortRule && (
<SortRuleRow
sortRule={sortRule}
columnOptions={columnOptions}
sortDirectionOptions={sortDirectionOptions}
onChange={setSortCondition}
onChange={setSortRule}
onRemove={handleRemoveSort}
/>
)}
{/* Action Buttons */}
<div className='flex items-center gap-[8px]'>
<Button variant='default' size='sm' onClick={onAddRow}>
<Plus className='mr-[4px] h-[12px] w-[12px]' />
Add row
</Button>
<Button variant='default' size='sm' onClick={handleAddCondition}>
<Button variant='default' size='sm' onClick={handleAddRule}>
<Plus className='mr-[4px] h-[12px] w-[12px]' />
Add filter
</Button>
{!sortCondition && (
{!sortRule && (
<Button variant='default' size='sm' onClick={handleAddSort}>
<ArrowUpAZ className='mr-[4px] h-[12px] w-[12px]' />
Add sort
@@ -172,33 +145,19 @@ export function TableQueryBuilder({
)
}
/**
* Props for the FilterConditionRow component.
*/
interface FilterConditionRowProps {
/** The filter condition */
condition: FilterCondition
/** Index in the conditions array */
interface FilterRuleRowProps {
rule: FilterRule
index: number
/** Available column options */
columnOptions: Array<{ value: string; label: string }>
/** Available comparison operator options */
comparisonOptions: Array<{ value: string; label: string }>
/** Available logical operator options */
logicalOptions: Array<{ value: string; label: string }>
/** Callback to update a condition field */
onUpdate: (id: string, field: keyof FilterCondition, value: string) => void
/** Callback to remove the condition */
onUpdate: (id: string, field: keyof FilterRule, value: string) => void
onRemove: (id: string) => void
/** Callback to apply filters */
onApply: () => void
}
/**
* A single filter condition row.
*/
function FilterConditionRow({
condition,
function FilterRuleRow({
rule,
index,
columnOptions,
comparisonOptions,
@@ -206,13 +165,13 @@ function FilterConditionRow({
onUpdate,
onRemove,
onApply,
}: FilterConditionRowProps) {
}: FilterRuleRowProps) {
return (
<div className='flex items-center gap-[8px]'>
<Button
variant='ghost'
size='sm'
onClick={() => onRemove(condition.id)}
onClick={() => onRemove(rule.id)}
className='h-[28px] w-[28px] shrink-0 p-0 text-[var(--text-tertiary)] hover:text-[var(--text-primary)]'
>
<X className='h-[12px] w-[12px]' />
@@ -230,8 +189,8 @@ function FilterConditionRow({
<Combobox
size='sm'
options={logicalOptions}
value={condition.logicalOperator}
onChange={(value) => onUpdate(condition.id, 'logicalOperator', value as 'and' | 'or')}
value={rule.logicalOperator}
onChange={(value) => onUpdate(rule.id, 'logicalOperator', value as 'and' | 'or')}
/>
)}
</div>
@@ -240,8 +199,8 @@ function FilterConditionRow({
<Combobox
size='sm'
options={columnOptions}
value={condition.column}
onChange={(value) => onUpdate(condition.id, 'column', value)}
value={rule.column}
onChange={(value) => onUpdate(rule.id, 'column', value)}
placeholder='Column'
/>
</div>
@@ -250,15 +209,15 @@ function FilterConditionRow({
<Combobox
size='sm'
options={comparisonOptions}
value={condition.operator}
onChange={(value) => onUpdate(condition.id, 'operator', value)}
value={rule.operator}
onChange={(value) => onUpdate(rule.id, 'operator', value)}
/>
</div>
<Input
className='h-[28px] min-w-[200px] flex-1 text-[12px]'
value={condition.value}
onChange={(e) => onUpdate(condition.id, 'value', e.target.value)}
value={rule.value}
onChange={(e) => onUpdate(rule.id, 'value', e.target.value)}
placeholder='Value'
onKeyDown={(e) => {
if (e.key === 'Enter') {
@@ -270,32 +229,21 @@ function FilterConditionRow({
)
}
/**
* Props for the SortConditionRow component.
*/
interface SortConditionRowProps {
/** The sort condition */
sortCondition: SortCondition
/** Available column options */
interface SortRuleRowProps {
sortRule: SortRule
columnOptions: Array<{ value: string; label: string }>
/** Available sort direction options */
sortDirectionOptions: Array<{ value: string; label: string }>
/** Callback to update the sort condition */
onChange: (condition: SortCondition | null) => void
/** Callback to remove the sort */
onChange: (rule: SortRule | null) => void
onRemove: () => void
}
/**
* Sort condition row component.
*/
function SortConditionRow({
sortCondition,
function SortRuleRow({
sortRule,
columnOptions,
sortDirectionOptions,
onChange,
onRemove,
}: SortConditionRowProps) {
}: SortRuleRowProps) {
return (
<div className='flex items-center gap-[8px]'>
<Button
@@ -315,8 +263,8 @@ function SortConditionRow({
<Combobox
size='sm'
options={columnOptions}
value={sortCondition.column}
onChange={(value) => onChange({ ...sortCondition, column: value })}
value={sortRule.column}
onChange={(value) => onChange({ ...sortRule, column: value })}
placeholder='Column'
/>
</div>
@@ -325,13 +273,13 @@ function SortConditionRow({
<Combobox
size='sm'
options={sortDirectionOptions}
value={sortCondition.direction}
onChange={(value) => onChange({ ...sortCondition, direction: value as 'asc' | 'desc' })}
value={sortRule.direction}
onChange={(value) => onChange({ ...sortRule, direction: value as 'asc' | 'desc' })}
/>
</div>
<div className='flex items-center text-[12px] text-[var(--text-tertiary)]'>
{sortCondition.direction === 'asc' ? (
{sortRule.direction === 'asc' ? (
<ArrowUpAZ className='h-[14px] w-[14px]' />
) : (
<ArrowDownAZ className='h-[14px] w-[14px]' />

View File

@@ -30,9 +30,6 @@ export interface TableRowModalProps {
onSuccess: () => void
}
/**
* Creates initial form data for columns.
*/
function createInitialRowData(columns: ColumnDefinition[]): Record<string, unknown> {
const initial: Record<string, unknown> = {}
columns.forEach((col) => {
@@ -166,9 +163,6 @@ export function TableRowModal({
}
}
/**
* Handles delete operation.
*/
const handleDelete = async () => {
setError(null)
setIsSubmitting(true)
@@ -324,9 +318,6 @@ export function TableRowModal({
)
}
/**
* Error message display component.
*/
function ErrorMessage({ error }: { error: string | null }) {
if (!error) return null

View File

@@ -1,7 +1,3 @@
/**
* Hook for fetching table data and rows.
*/
import { useQuery } from '@tanstack/react-query'
import type { TableDefinition, TableRow } from '@/lib/table'
import type { BuilderQueryResult } from '../../components/table-query-builder'
@@ -24,12 +20,7 @@ interface UseTableDataReturn {
refetchRows: () => void
}
/**
* Fetches table metadata and rows with filtering/sorting support.
*
* @param params - The parameters for fetching table data
* @returns Table data, rows, and loading states
*/
/** Fetches table metadata and rows with filtering/sorting/pagination. */
export function useTableData({
workspaceId,
tableId,
@@ -65,7 +56,6 @@ export function useTableData({
}
if (queryOptions.sort) {
// sort is already in the correct format: { column: direction }
searchParams.set('sort', JSON.stringify(queryOptions.sort))
}

View File

@@ -28,15 +28,7 @@ import {
import { useContextMenu, useRowSelection, useTableData } from './hooks'
import type { CellViewerData } from './types'
/**
* Main component for viewing and managing table data.
*
* Provides functionality for:
* - Viewing rows with pagination
* - Filtering and sorting
* - Adding, editing, and deleting rows
* - Viewing cell details for long/complex values
*/
/** Table data viewer with filtering, sorting, pagination, and CRUD operations. */
export function TableDataViewer() {
const params = useParams()
const router = useRouter()

View File

@@ -1,15 +1,15 @@
import { X } from 'lucide-react'
import { Button, Combobox, type ComboboxOption, Input } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import type { FilterCondition } from '@/lib/table/filters/constants'
import type { FilterRule } from '@/lib/table/filters/constants'
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
import { SubBlockInputController } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sub-block-input-controller'
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
interface FilterConditionRowProps {
interface FilterRuleRowProps {
blockId: string
subBlockId: string
condition: FilterCondition
rule: FilterRule
index: number
columns: ComboboxOption[]
comparisonOptions: ComboboxOption[]
@@ -18,13 +18,13 @@ interface FilterConditionRowProps {
isPreview: boolean
disabled: boolean
onRemove: (id: string) => void
onUpdate: (id: string, field: keyof FilterCondition, value: string) => void
onUpdate: (id: string, field: keyof FilterRule, value: string) => void
}
export function FilterConditionRow({
export function FilterRuleRow({
blockId,
subBlockId,
condition,
rule,
index,
columns,
comparisonOptions,
@@ -34,7 +34,7 @@ export function FilterConditionRow({
disabled,
onRemove,
onUpdate,
}: FilterConditionRowProps) {
}: FilterRuleRowProps) {
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
return (
@@ -42,7 +42,7 @@ export function FilterConditionRow({
<Button
variant='ghost'
size='sm'
onClick={() => onRemove(condition.id)}
onClick={() => onRemove(rule.id)}
disabled={isReadOnly}
className='h-[24px] w-[24px] shrink-0 p-0 text-[var(--text-tertiary)] hover:text-[var(--text-primary)]'
>
@@ -61,8 +61,8 @@ export function FilterConditionRow({
<Combobox
size='sm'
options={logicalOptions}
value={condition.logicalOperator}
onChange={(v) => onUpdate(condition.id, 'logicalOperator', v as 'and' | 'or')}
value={rule.logicalOperator}
onChange={(v) => onUpdate(rule.id, 'logicalOperator', v as 'and' | 'or')}
disabled={isReadOnly}
/>
)}
@@ -72,8 +72,8 @@ export function FilterConditionRow({
<Combobox
size='sm'
options={columns}
value={condition.column}
onChange={(v) => onUpdate(condition.id, 'column', v)}
value={rule.column}
onChange={(v) => onUpdate(rule.id, 'column', v)}
placeholder='Column'
disabled={isReadOnly}
/>
@@ -83,8 +83,8 @@ export function FilterConditionRow({
<Combobox
size='sm'
options={comparisonOptions}
value={condition.operator}
onChange={(v) => onUpdate(condition.id, 'operator', v)}
value={rule.operator}
onChange={(v) => onUpdate(rule.id, 'operator', v)}
disabled={isReadOnly}
/>
</div>
@@ -92,10 +92,10 @@ export function FilterConditionRow({
<div className='relative min-w-[80px] flex-1'>
<SubBlockInputController
blockId={blockId}
subBlockId={`${subBlockId}_filter_${condition.id}`}
config={{ id: `filter_value_${condition.id}`, type: 'short-input' }}
value={condition.value}
onChange={(newValue) => onUpdate(condition.id, 'value', newValue)}
subBlockId={`${subBlockId}_filter_${rule.id}`}
config={{ id: `filter_value_${rule.id}`, type: 'short-input' }}
value={rule.value}
onChange={(newValue) => onUpdate(rule.id, 'value', newValue)}
isPreview={isPreview}
disabled={disabled}
>

View File

@@ -3,26 +3,24 @@
import { useMemo } from 'react'
import { Plus } from 'lucide-react'
import { Button } from '@/components/emcn'
import type { FilterCondition } from '@/lib/table/filters/constants'
import type { FilterRule } from '@/lib/table/filters/constants'
import { useFilterBuilder } from '@/lib/table/filters/use-builder'
import { useTableColumns } from '@/lib/table/hooks'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { EmptyState } from './components/empty-state'
import { FilterConditionRow } from './components/filter-condition-row'
import { FilterRuleRow } from './components/filter-rule-row'
interface FilterFormatProps {
blockId: string
subBlockId: string
isPreview?: boolean
previewValue?: FilterCondition[] | null
previewValue?: FilterRule[] | null
disabled?: boolean
columns?: Array<{ value: string; label: string }>
tableIdSubBlockId?: string
}
/**
* Visual builder for filter conditions.
*/
/** Visual builder for table filter rules in workflow blocks. */
export function FilterFormat({
blockId,
subBlockId,
@@ -32,7 +30,7 @@ export function FilterFormat({
columns: propColumns,
tableIdSubBlockId = 'tableId',
}: FilterFormatProps) {
const [storeValue, setStoreValue] = useSubBlockValue<FilterCondition[]>(blockId, subBlockId)
const [storeValue, setStoreValue] = useSubBlockValue<FilterRule[]>(blockId, subBlockId)
const [tableIdValue] = useSubBlockValue<string>(blockId, tableIdSubBlockId)
const dynamicColumns = useTableColumns({ tableId: tableIdValue })
@@ -42,29 +40,28 @@ export function FilterFormat({
}, [propColumns, dynamicColumns])
const value = isPreview ? previewValue : storeValue
const conditions: FilterCondition[] = Array.isArray(value) && value.length > 0 ? value : []
const rules: FilterRule[] = Array.isArray(value) && value.length > 0 ? value : []
const isReadOnly = isPreview || disabled
const { comparisonOptions, logicalOptions, addCondition, removeCondition, updateCondition } =
useFilterBuilder({
columns,
conditions,
setConditions: setStoreValue,
isReadOnly,
})
const { comparisonOptions, logicalOptions, addRule, removeRule, updateRule } = useFilterBuilder({
columns,
rules,
setRules: setStoreValue,
isReadOnly,
})
return (
<div className='flex flex-col gap-[8px]'>
{conditions.length === 0 ? (
<EmptyState onAdd={addCondition} disabled={isReadOnly} label='Add filter condition' />
{rules.length === 0 ? (
<EmptyState onAdd={addRule} disabled={isReadOnly} label='Add filter rule' />
) : (
<>
{conditions.map((condition, index) => (
<FilterConditionRow
key={condition.id}
{rules.map((rule, index) => (
<FilterRuleRow
key={rule.id}
blockId={blockId}
subBlockId={subBlockId}
condition={condition}
rule={rule}
index={index}
columns={columns}
comparisonOptions={comparisonOptions}
@@ -72,19 +69,19 @@ export function FilterFormat({
isReadOnly={isReadOnly}
isPreview={isPreview}
disabled={disabled}
onRemove={removeCondition}
onUpdate={updateCondition}
onRemove={removeRule}
onUpdate={updateRule}
/>
))}
<Button
variant='ghost'
size='sm'
onClick={addCondition}
onClick={addRule}
disabled={isReadOnly}
className='self-start'
>
<Plus className='mr-[4px] h-[12px] w-[12px]' />
Add condition
Add rule
</Button>
</>
)}

View File

@@ -1,32 +1,32 @@
import { X } from 'lucide-react'
import { Button, Combobox, type ComboboxOption } from '@/components/emcn'
import type { SortCondition } from '@/lib/table/filters/constants'
import type { SortRule } from '@/lib/table/filters/constants'
interface SortConditionRowProps {
condition: SortCondition
interface SortRuleRowProps {
rule: SortRule
index: number
columns: ComboboxOption[]
directionOptions: ComboboxOption[]
isReadOnly: boolean
onRemove: (id: string) => void
onUpdate: (id: string, field: keyof SortCondition, value: string) => void
onUpdate: (id: string, field: keyof SortRule, value: string) => void
}
export function SortConditionRow({
condition,
export function SortRuleRow({
rule,
index,
columns,
directionOptions,
isReadOnly,
onRemove,
onUpdate,
}: SortConditionRowProps) {
}: SortRuleRowProps) {
return (
<div className='flex items-center gap-[6px]'>
<Button
variant='ghost'
size='sm'
onClick={() => onRemove(condition.id)}
onClick={() => onRemove(rule.id)}
disabled={isReadOnly}
className='h-[24px] w-[24px] shrink-0 p-0 text-[var(--text-tertiary)] hover:text-[var(--text-primary)]'
>
@@ -46,8 +46,8 @@ export function SortConditionRow({
<Combobox
size='sm'
options={columns}
value={condition.column}
onChange={(v) => onUpdate(condition.id, 'column', v)}
value={rule.column}
onChange={(v) => onUpdate(rule.id, 'column', v)}
placeholder='Column'
disabled={isReadOnly}
/>
@@ -57,8 +57,8 @@ export function SortConditionRow({
<Combobox
size='sm'
options={directionOptions}
value={condition.direction}
onChange={(v) => onUpdate(condition.id, 'direction', v as 'asc' | 'desc')}
value={rule.direction}
onChange={(v) => onUpdate(rule.id, 'direction', v as 'asc' | 'desc')}
disabled={isReadOnly}
/>
</div>

View File

@@ -4,31 +4,29 @@ import { useCallback, useMemo } from 'react'
import { Plus } from 'lucide-react'
import { nanoid } from 'nanoid'
import { Button, type ComboboxOption } from '@/components/emcn'
import { SORT_DIRECTIONS, type SortCondition } from '@/lib/table/filters/constants'
import { SORT_DIRECTIONS, type SortRule } from '@/lib/table/filters/constants'
import { useTableColumns } from '@/lib/table/hooks'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
import { EmptyState } from './components/empty-state'
import { SortConditionRow } from './components/sort-condition-row'
import { SortRuleRow } from './components/sort-rule-row'
interface SortFormatProps {
blockId: string
subBlockId: string
isPreview?: boolean
previewValue?: SortCondition[] | null
previewValue?: SortRule[] | null
disabled?: boolean
columns?: Array<{ value: string; label: string }>
tableIdSubBlockId?: string
}
const createDefaultCondition = (columns: ComboboxOption[]): SortCondition => ({
const createDefaultRule = (columns: ComboboxOption[]): SortRule => ({
id: nanoid(),
column: columns[0]?.value || '',
direction: 'asc',
})
/**
* Visual builder for sort conditions.
*/
/** Visual builder for table sort rules in workflow blocks. */
export function SortFormat({
blockId,
subBlockId,
@@ -38,7 +36,7 @@ export function SortFormat({
columns: propColumns,
tableIdSubBlockId = 'tableId',
}: SortFormatProps) {
const [storeValue, setStoreValue] = useSubBlockValue<SortCondition[]>(blockId, subBlockId)
const [storeValue, setStoreValue] = useSubBlockValue<SortRule[]>(blockId, subBlockId)
const [tableIdValue] = useSubBlockValue<string>(blockId, tableIdSubBlockId)
const dynamicColumns = useTableColumns({ tableId: tableIdValue, includeBuiltIn: true })
@@ -53,52 +51,52 @@ export function SortFormat({
)
const value = isPreview ? previewValue : storeValue
const conditions: SortCondition[] = Array.isArray(value) && value.length > 0 ? value : []
const rules: SortRule[] = Array.isArray(value) && value.length > 0 ? value : []
const isReadOnly = isPreview || disabled
const addCondition = useCallback(() => {
const addRule = useCallback(() => {
if (isReadOnly) return
setStoreValue([...conditions, createDefaultCondition(columns)])
}, [isReadOnly, conditions, columns, setStoreValue])
setStoreValue([...rules, createDefaultRule(columns)])
}, [isReadOnly, rules, columns, setStoreValue])
const removeCondition = useCallback(
const removeRule = useCallback(
(id: string) => {
if (isReadOnly) return
setStoreValue(conditions.filter((c) => c.id !== id))
setStoreValue(rules.filter((r) => r.id !== id))
},
[isReadOnly, conditions, setStoreValue]
[isReadOnly, rules, setStoreValue]
)
const updateCondition = useCallback(
(id: string, field: keyof SortCondition, newValue: string) => {
const updateRule = useCallback(
(id: string, field: keyof SortRule, newValue: string) => {
if (isReadOnly) return
setStoreValue(conditions.map((c) => (c.id === id ? { ...c, [field]: newValue } : c)))
setStoreValue(rules.map((r) => (r.id === id ? { ...r, [field]: newValue } : r)))
},
[isReadOnly, conditions, setStoreValue]
[isReadOnly, rules, setStoreValue]
)
return (
<div className='flex flex-col gap-[8px]'>
{conditions.length === 0 ? (
<EmptyState onAdd={addCondition} disabled={isReadOnly} label='Add sort condition' />
{rules.length === 0 ? (
<EmptyState onAdd={addRule} disabled={isReadOnly} label='Add sort rule' />
) : (
<>
{conditions.map((condition, index) => (
<SortConditionRow
key={condition.id}
condition={condition}
{rules.map((rule, index) => (
<SortRuleRow
key={rule.id}
rule={rule}
index={index}
columns={columns}
directionOptions={directionOptions}
isReadOnly={isReadOnly}
onRemove={removeCondition}
onUpdate={updateCondition}
onRemove={removeRule}
onUpdate={updateRule}
/>
))}
<Button
variant='ghost'
size='sm'
onClick={addCondition}
onClick={addRule}
disabled={isReadOnly}
className='self-start'
>

View File

@@ -3,7 +3,7 @@ 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'
import type { FilterCondition, SortCondition } from '@/lib/table/filters/constants'
import type { FilterRule, SortRule } from '@/lib/table/filters/constants'
import type { FieldDiffStatus } from '@/lib/workflows/diff/types'
import {
CheckboxList,
@@ -820,7 +820,7 @@ function SubBlockComponent({
blockId={blockId}
subBlockId={config.id}
isPreview={isPreview}
previewValue={previewValue as FilterCondition[] | null | undefined}
previewValue={previewValue as FilterRule[] | null | undefined}
disabled={isDisabled}
/>
)
@@ -831,7 +831,7 @@ function SubBlockComponent({
blockId={blockId}
subBlockId={config.id}
isPreview={isPreview}
previewValue={previewValue as SortCondition[] | null | undefined}
previewValue={previewValue as SortRule[] | null | undefined}
disabled={isDisabled}
/>
)

View File

@@ -1,5 +1,5 @@
import { TableIcon } from '@/components/icons'
import { conditionsToFilter, sortConditionsToSort } from '@/lib/table/filters/utils'
import { filterRulesToFilter, sortRulesToSort } from '@/lib/table/filters/utils'
import type { BlockConfig } from '@/blocks/types'
import type { TableQueryResponse } from '@/tools/table/types'
@@ -96,8 +96,9 @@ const paramTransformers: Record<string, (params: TableBlockParams) => ParsedPara
let filter: unknown
if (params.bulkFilterMode === 'builder' && params.bulkFilterBuilder) {
filter =
conditionsToFilter(params.bulkFilterBuilder as Parameters<typeof conditionsToFilter>[0]) ||
undefined
filterRulesToFilter(
params.bulkFilterBuilder as Parameters<typeof filterRulesToFilter>[0]
) || undefined
} else if (params.filter) {
filter = parseJSON(params.filter, 'Filter')
}
@@ -119,8 +120,9 @@ const paramTransformers: Record<string, (params: TableBlockParams) => ParsedPara
let filter: unknown
if (params.bulkFilterMode === 'builder' && params.bulkFilterBuilder) {
filter =
conditionsToFilter(params.bulkFilterBuilder as Parameters<typeof conditionsToFilter>[0]) ||
undefined
filterRulesToFilter(
params.bulkFilterBuilder as Parameters<typeof filterRulesToFilter>[0]
) || undefined
} else if (params.filter) {
filter = parseJSON(params.filter, 'Filter')
}
@@ -145,7 +147,7 @@ const paramTransformers: Record<string, (params: TableBlockParams) => ParsedPara
let filter: unknown
if (params.builderMode === 'builder' && params.filterBuilder) {
filter =
conditionsToFilter(params.filterBuilder as Parameters<typeof conditionsToFilter>[0]) ||
filterRulesToFilter(params.filterBuilder as Parameters<typeof filterRulesToFilter>[0]) ||
undefined
} else if (params.filter) {
filter = parseJSON(params.filter, 'Filter')
@@ -154,8 +156,7 @@ const paramTransformers: Record<string, (params: TableBlockParams) => ParsedPara
let sort: unknown
if (params.builderMode === 'builder' && params.sortBuilder) {
sort =
sortConditionsToSort(params.sortBuilder as Parameters<typeof sortConditionsToSort>[0]) ||
undefined
sortRulesToSort(params.sortBuilder as Parameters<typeof sortRulesToSort>[0]) || undefined
} else if (params.sort) {
sort = parseJSON(params.sort, 'Sort')
}

View File

@@ -1,14 +1,9 @@
/**
* Shared constants for table filtering and sorting UI.
*
* Types (FilterCondition, SortCondition) are defined in ../types.ts
* and re-exported here for convenience.
* Constants for table filtering and sorting UI.
*/
// Re-export UI builder types from central types file
export type { FilterCondition, SortCondition } from '../types'
export type { FilterRule, SortRule } from '../types'
/** Comparison operators for filter conditions (maps to ConditionOperators in types.ts) */
export const COMPARISON_OPERATORS = [
{ value: 'eq', label: 'equals' },
{ value: 'ne', label: 'not equals' },
@@ -20,17 +15,11 @@ export const COMPARISON_OPERATORS = [
{ value: 'in', label: 'in array' },
] as const
/**
* Logical operators for combining filter conditions.
*/
export const LOGICAL_OPERATORS = [
{ value: 'and', label: 'and' },
{ value: 'or', label: 'or' },
] as const
/**
* Sort direction options for UI dropdowns.
*/
export const SORT_DIRECTIONS = [
{ value: 'asc', label: 'ascending' },
{ value: 'desc', label: 'descending' },

View File

@@ -1,7 +1,3 @@
/**
* Filter utilities for table queries.
*/
export * from './constants'
export * from './use-builder'
export * from './utils'

View File

@@ -1,8 +1,5 @@
/**
* Hook for filter builder functionality.
*
* Provides reusable filter condition management logic shared between
* the table data viewer's TableQueryBuilder and workflow block's FilterFormat.
* Hooks for filter and sort builder UI state management.
*/
import { useCallback, useMemo } from 'react'
@@ -10,51 +7,19 @@ import { nanoid } from 'nanoid'
import type { ColumnOption } from '../types'
import {
COMPARISON_OPERATORS,
type FilterCondition,
type FilterRule,
LOGICAL_OPERATORS,
SORT_DIRECTIONS,
type SortCondition,
type SortRule,
} from './constants'
// Re-export ColumnOption for consumers of this module
export type { ColumnOption }
/**
* Hook that provides filter builder logic for managing filter conditions.
*
* @example Basic usage with useState:
* ```tsx
* const [conditions, setConditions] = useState<FilterCondition[]>([])
*
* const {
* comparisonOptions,
* logicalOptions,
* addCondition,
* removeCondition,
* updateCondition,
* } = useFilterBuilder({
* columns: columnOptions,
* conditions,
* setConditions,
* })
* ```
*
* @example With store value:
* ```tsx
* const [conditions, setConditions] = useSubBlockValue<FilterCondition[]>(blockId, subBlockId)
*
* const { addCondition, removeCondition, updateCondition } = useFilterBuilder({
* columns,
* conditions: conditions ?? [],
* setConditions,
* isReadOnly: isPreview || disabled,
* })
* ```
*/
/** Manages filter rule state with add/remove/update operations. */
export function useFilterBuilder({
columns,
conditions,
setConditions,
rules,
setRules,
isReadOnly = false,
}: UseFilterBuilderProps): UseFilterBuilderReturn {
const comparisonOptions = useMemo(
@@ -72,7 +37,7 @@ export function useFilterBuilder({
[]
)
const createDefaultCondition = useCallback((): FilterCondition => {
const createDefaultRule = useCallback((): FilterRule => {
return {
id: nanoid(),
logicalOperator: 'and',
@@ -82,56 +47,43 @@ export function useFilterBuilder({
}
}, [columns])
const addCondition = useCallback(() => {
const addRule = useCallback(() => {
if (isReadOnly) return
setConditions([...conditions, createDefaultCondition()])
}, [isReadOnly, conditions, setConditions, createDefaultCondition])
setRules([...rules, createDefaultRule()])
}, [isReadOnly, rules, setRules, createDefaultRule])
const removeCondition = useCallback(
const removeRule = useCallback(
(id: string) => {
if (isReadOnly) return
setConditions(conditions.filter((c) => c.id !== id))
setRules(rules.filter((r) => r.id !== id))
},
[isReadOnly, conditions, setConditions]
[isReadOnly, rules, setRules]
)
const updateCondition = useCallback(
(id: string, field: keyof FilterCondition, value: string) => {
const updateRule = useCallback(
(id: string, field: keyof FilterRule, value: string) => {
if (isReadOnly) return
setConditions(conditions.map((c) => (c.id === id ? { ...c, [field]: value } : c)))
setRules(rules.map((r) => (r.id === id ? { ...r, [field]: value } : r)))
},
[isReadOnly, conditions, setConditions]
[isReadOnly, rules, setRules]
)
return {
comparisonOptions,
logicalOptions,
sortDirectionOptions,
addCondition,
removeCondition,
updateCondition,
createDefaultCondition,
addRule,
removeRule,
updateRule,
createDefaultRule,
}
}
/**
* Hook that provides sort builder logic.
*
* @example
* ```tsx
* const [sortCondition, setSortCondition] = useState<SortCondition | null>(null)
*
* const { addSort, removeSort, updateSortColumn, updateSortDirection } = useSortBuilder({
* columns: columnOptions,
* sortCondition,
* setSortCondition,
* })
* ```
*/
/** Manages sort rule state with add/remove/update operations. */
export function useSortBuilder({
columns,
sortCondition,
setSortCondition,
sortRule,
setSortRule,
}: UseSortBuilderProps): UseSortBuilderReturn {
const sortDirectionOptions = useMemo(
() => SORT_DIRECTIONS.map((d) => ({ value: d.value, label: d.label })),
@@ -139,33 +91,33 @@ export function useSortBuilder({
)
const addSort = useCallback(() => {
setSortCondition({
setSortRule({
id: nanoid(),
column: columns[0]?.value || '',
direction: 'asc',
})
}, [columns, setSortCondition])
}, [columns, setSortRule])
const removeSort = useCallback(() => {
setSortCondition(null)
}, [setSortCondition])
setSortRule(null)
}, [setSortRule])
const updateSortColumn = useCallback(
(column: string) => {
if (sortCondition) {
setSortCondition({ ...sortCondition, column })
if (sortRule) {
setSortRule({ ...sortRule, column })
}
},
[sortCondition, setSortCondition]
[sortRule, setSortRule]
)
const updateSortDirection = useCallback(
(direction: 'asc' | 'desc') => {
if (sortCondition) {
setSortCondition({ ...sortCondition, direction })
if (sortRule) {
setSortRule({ ...sortRule, direction })
}
},
[sortCondition, setSortCondition]
[sortRule, setSortRule]
)
return {
@@ -179,8 +131,8 @@ export function useSortBuilder({
export interface UseFilterBuilderProps {
columns: ColumnOption[]
conditions: FilterCondition[]
setConditions: (conditions: FilterCondition[]) => void
rules: FilterRule[]
setRules: (rules: FilterRule[]) => void
isReadOnly?: boolean
}
@@ -188,16 +140,16 @@ export interface UseFilterBuilderReturn {
comparisonOptions: ColumnOption[]
logicalOptions: ColumnOption[]
sortDirectionOptions: ColumnOption[]
addCondition: () => void
removeCondition: (id: string) => void
updateCondition: (id: string, field: keyof FilterCondition, value: string) => void
createDefaultCondition: () => FilterCondition
addRule: () => void
removeRule: (id: string) => void
updateRule: (id: string, field: keyof FilterRule, value: string) => void
createDefaultRule: () => FilterRule
}
export interface UseSortBuilderProps {
columns: ColumnOption[]
sortCondition: SortCondition | null
setSortCondition: (sort: SortCondition | null) => void
sortRule: SortRule | null
setSortRule: (sort: SortRule | null) => void
}
export interface UseSortBuilderReturn {

View File

@@ -1,42 +1,27 @@
/**
* Shared utilities for filter builder UI components.
*
* These utilities convert between UI builder types (FilterCondition, SortCondition)
* and API types (Filter, Sort).
* Utilities for converting between UI builder state and API filter/sort objects.
*/
import { nanoid } from 'nanoid'
import type {
Filter,
FilterCondition,
JsonValue,
Sort,
SortCondition,
SortDirection,
} from '../types'
import type { Filter, FilterRule, JsonValue, Sort, SortDirection, SortRule } from '../types'
/**
* Converts builder filter conditions to MongoDB-style filter object.
*
* @param conditions - Array of filter conditions from the builder UI
* @returns Filter object or null if no conditions
*/
export function conditionsToFilter(conditions: FilterCondition[]): Filter | null {
if (conditions.length === 0) return null
/** Converts UI filter rules to a Filter object for API queries. */
export function filterRulesToFilter(rules: FilterRule[]): Filter | null {
if (rules.length === 0) return null
const orGroups: Record<string, JsonValue>[] = []
let currentGroup: Record<string, JsonValue> = {}
const orGroups: Filter[] = []
let currentGroup: Filter = {}
for (const condition of conditions) {
const isOr = condition.logicalOperator === 'or'
const conditionValue = toConditionValue(condition.operator, condition.value)
for (const rule of rules) {
const isOr = rule.logicalOperator === 'or'
const ruleValue = toRuleValue(rule.operator, rule.value)
if (isOr && Object.keys(currentGroup).length > 0) {
orGroups.push({ ...currentGroup })
currentGroup = {}
}
currentGroup[condition.column] = conditionValue
currentGroup[rule.column] = ruleValue as Filter[string]
}
if (Object.keys(currentGroup).length > 0) {
@@ -46,13 +31,8 @@ export function conditionsToFilter(conditions: FilterCondition[]): Filter | null
return orGroups.length > 1 ? { $or: orGroups } : orGroups[0] || null
}
/**
* Converts MongoDB-style filter object to builder conditions.
*
* @param filter - Filter object to convert
* @returns Array of filter conditions for the builder UI
*/
export function filterToConditions(filter: Filter | null): FilterCondition[] {
/** Converts a Filter object back to UI filter rules. */
export function filterToRules(filter: Filter | null): FilterRule[] {
if (!filter) return []
if (filter.$or && Array.isArray(filter.$or)) {
@@ -65,43 +45,28 @@ export function filterToConditions(filter: Filter | null): FilterCondition[] {
return parseFilterGroup(filter)
}
/**
* Converts a single builder sort condition to Sort object.
*
* @param condition - Single sort condition from the builder UI
* @returns Sort object or null if no condition
*/
export function sortConditionToSort(condition: SortCondition | null): Sort | null {
if (!condition || !condition.column) return null
return { [condition.column]: condition.direction }
/** Converts a single UI sort rule to a Sort object for API queries. */
export function sortRuleToSort(rule: SortRule | null): Sort | null {
if (!rule || !rule.column) return null
return { [rule.column]: rule.direction }
}
/**
* Converts builder sort conditions (array) to Sort object.
*
* @param conditions - Array of sort conditions from the builder UI
* @returns Sort object or null if no conditions
*/
export function sortConditionsToSort(conditions: SortCondition[]): Sort | null {
if (conditions.length === 0) return null
/** Converts multiple UI sort rules to a Sort object. */
export function sortRulesToSort(rules: SortRule[]): Sort | null {
if (rules.length === 0) return null
const sort: Sort = {}
for (const condition of conditions) {
if (condition.column) {
sort[condition.column] = condition.direction
for (const rule of rules) {
if (rule.column) {
sort[rule.column] = rule.direction
}
}
return Object.keys(sort).length > 0 ? sort : null
}
/**
* Converts Sort object to builder conditions.
*
* @param sort - Sort object to convert
* @returns Array of sort conditions for the builder UI
*/
export function sortToConditions(sort: Sort | null): SortCondition[] {
/** Converts a Sort object back to UI sort rules. */
export function sortToRules(sort: Sort | null): SortRule[] {
if (!sort) return []
return Object.entries(sort).map(([column, direction]) => ({
@@ -111,29 +76,29 @@ export function sortToConditions(sort: Sort | null): SortCondition[] {
}))
}
function toConditionValue(operator: string, value: string): JsonValue {
function toRuleValue(operator: string, value: string): JsonValue {
const parsedValue = parseValue(value, operator)
return operator === 'eq' ? parsedValue : { [`$${operator}`]: parsedValue }
}
function applyLogicalOperators(groups: FilterCondition[][]): FilterCondition[] {
const conditions: FilterCondition[] = []
function applyLogicalOperators(groups: FilterRule[][]): FilterRule[] {
const rules: FilterRule[] = []
groups.forEach((group, groupIndex) => {
group.forEach((condition, conditionIndex) => {
conditions.push({
...condition,
group.forEach((rule, ruleIndex) => {
rules.push({
...rule,
logicalOperator:
groupIndex === 0 && conditionIndex === 0
groupIndex === 0 && ruleIndex === 0
? 'and'
: groupIndex > 0 && conditionIndex === 0
: groupIndex > 0 && ruleIndex === 0
? 'or'
: 'and',
})
})
})
return conditions
return rules
}
function parseValue(value: string, operator: string): JsonValue {
@@ -155,10 +120,10 @@ function parseScalar(value: string): JsonValue {
return value
}
function parseFilterGroup(group: Filter): FilterCondition[] {
function parseFilterGroup(group: Filter): FilterRule[] {
if (!group || typeof group !== 'object' || Array.isArray(group)) return []
const conditions: FilterCondition[] = []
const rules: FilterRule[] = []
for (const [column, value] of Object.entries(group)) {
if (column === '$or' || column === '$and') continue
@@ -166,7 +131,7 @@ function parseFilterGroup(group: Filter): FilterCondition[] {
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
for (const [op, opValue] of Object.entries(value)) {
if (op.startsWith('$')) {
conditions.push({
rules.push({
id: nanoid(),
logicalOperator: 'and',
column,
@@ -178,7 +143,7 @@ function parseFilterGroup(group: Filter): FilterCondition[] {
continue
}
conditions.push({
rules.push({
id: nanoid(),
logicalOperator: 'and',
column,
@@ -187,7 +152,7 @@ function parseFilterGroup(group: Filter): FilterCondition[] {
})
}
return conditions
return rules
}
function formatValueForBuilder(value: JsonValue): string {

View File

@@ -6,10 +6,7 @@ interface UseTableColumnsOptions {
includeBuiltIn?: boolean
}
/**
* Fetches table schema columns from the API.
* Returns columns as options for use in dropdowns.
*/
/** Fetches table schema columns as dropdown options. */
export function useTableColumns({ tableId, includeBuiltIn = false }: UseTableColumnsOptions) {
const [columns, setColumns] = useState<ColumnOption[]>([])
const fetchedTableIdRef = useRef<string | null>(null)
@@ -45,7 +42,7 @@ export function useTableColumns({ tableId, includeBuiltIn = false }: UseTableCol
fetchedTableIdRef.current = tableId
} catch {
// Silently fail - columns will be empty
// Silently fail
}
}

View File

@@ -1,9 +1,6 @@
/**
* Table utilities module.
*
* Provides validation, query building, service layer, and filter utilities
* for user-defined tables.
*
* Hooks are not re-exported here to avoid pulling React into server code.
* Import hooks directly from '@/lib/table/hooks' in client components.
*/

View File

@@ -1,38 +1,26 @@
/**
* Core type definitions for user-defined tables.
* Type definitions for user-defined tables.
*/
import type { COLUMN_TYPES } from './constants'
/** Primitive values that can be stored in table columns */
export type ColumnValue = string | number | boolean | null | Date
export type JsonValue = ColumnValue | JsonValue[] | { [key: string]: JsonValue }
/** Row data structure for insert/update operations
* key is the column name and value is the value of the column
* value is a JSON-compatible value */
/** Row data mapping column names to values. */
export type RowData = Record<string, JsonValue>
/** Sort direction for query operations */
export type SortDirection = 'asc' | 'desc'
/** Sort specification mapping column names to sort direction
* "asc" for ascending and "desc" for descending
* key is the column name and value is the sort direction */
/** Sort specification mapping column names to direction. */
export type Sort = Record<string, SortDirection>
/**
* Option for column/dropdown selection in UI components.
* Used by filter builders, sort builders, and column selectors.
*/
/** Option for dropdown/select components. */
export interface ColumnOption {
value: string
label: string
}
/**
* Column definition within a table schema.
*/
export interface ColumnDefinition {
name: string
type: (typeof COLUMN_TYPES)[number]
@@ -40,16 +28,10 @@ export interface ColumnDefinition {
unique?: boolean
}
/**
* Table schema definition containing column specifications.
*/
export interface TableSchema {
columns: ColumnDefinition[]
}
/**
* Complete table definition including metadata.
*/
export interface TableDefinition {
id: string
name: string
@@ -63,14 +45,9 @@ export interface TableDefinition {
updatedAt: Date | string
}
/**
* Subset of TableDefinition for UI components that only need basic info.
*/
/** Minimal table info for UI components. */
export type TableInfo = Pick<TableDefinition, 'id' | 'name' | 'schema'>
/**
* Row stored in a user-defined table.
*/
export interface TableRow {
id: string
data: RowData
@@ -79,18 +56,12 @@ export interface TableRow {
}
/**
* Operators that form a condition for a field.
* Supports MongoDB-style query operators.
* MongoDB-style query operators for field comparisons.
*
* @example
* // Single operator
* { $eq: 'John' } // field equals 'John'
* { $gt: 18 } // field greater than 18
* { $in: ['active', 'pending'] } // field in array
* { $contains: 'search' } // field contains 'search' (case-insensitive)
*
* // Multiple operators (all must match)
* { $gte: 18, $lt: 65 } // field >= 18 AND field < 65
* { $eq: 'John' }
* { $gte: 18, $lt: 65 }
* { $in: ['active', 'pending'] }
*/
export interface ConditionOperators {
$eq?: ColumnValue
@@ -105,38 +76,13 @@ export interface ConditionOperators {
}
/**
* Filter for querying table rows.
* Keys are column names, values are either direct values (shorthand for equality)
* or ConditionOperators objects for complex conditions.
* Filter object for querying table rows. Supports direct equality shorthand,
* operator objects, and logical $or/$and combinators.
*
* @example
* // Simple equality (shorthand - equivalent to { name: { $eq: 'John' } })
* { name: 'John' }
*
* // Using ConditionOperators for a single field
* { age: { $gt: 18 } }
* { status: { $in: ['active', 'pending'] } }
*
* // Multiple fields (AND logic)
* { name: 'John', age: { $gte: 18 } } // name = 'John' AND age >= 18
*
* // Logical OR
* { $or: [
* { status: 'active' },
* { status: 'pending' }
* ]}
*
* // Logical AND
* { $and: [
* { age: { $gte: 18 } },
* { verified: true }
* ]}
*
* // Nested logical operators
* { $or: [
* { $and: [{ status: 'active' }, { age: { $gte: 18 } }] },
* { role: 'admin' }
* ]}
* { age: { $gte: 18 } }
* { $or: [{ status: 'active' }, { status: 'pending' }] }
*/
export interface Filter {
$or?: Filter[]
@@ -144,54 +90,33 @@ export interface Filter {
[key: string]: ColumnValue | ConditionOperators | Filter[] | undefined
}
/**
* Result of a validation operation. The list of errors are used to display to the user.
*/
export interface ValidationResult {
valid: boolean
errors: string[]
}
// ============================================================================
// UI Builder Types
// These types represent the state of filter/sort builder UI components.
// They have `id` fields for React keys and string values for form inputs.
// Use the conversion utilities in filters/utils.ts to convert to API types.
// ============================================================================
/**
* Single filter condition in the UI builder.
* This is the UI representation - use `Filter` for API queries.
* UI builder state for a single filter rule.
* Includes an `id` field for React keys and string values for form inputs.
*/
export interface FilterCondition {
/** Unique identifier for the condition (used as React key) */
export interface FilterRule {
id: string
/** How this condition combines with the previous one */
logicalOperator: 'and' | 'or'
/** Column to filter on */
column: string
/** Comparison operator (eq, ne, gt, gte, lt, lte, contains, in) */
operator: string
/** Value to compare against (as string for form input) */
value: string
}
/**
* Single sort condition in the UI builder.
* This is the UI representation - use `Sort` for API queries.
* UI builder state for a single sort rule.
* Includes an `id` field for React keys.
*/
export interface SortCondition {
/** Unique identifier for the condition (used as React key) */
export interface SortRule {
id: string
/** Column to sort by */
column: string
/** Sort direction */
direction: SortDirection
}
/**
* Options for querying table rows.
*/
export interface QueryOptions {
filter?: Filter
sort?: Sort
@@ -199,67 +124,39 @@ export interface QueryOptions {
offset?: number
}
/**
* Result of a row query operation.
*/
export interface QueryResult {
/** Returned rows */
rows: TableRow[]
/** Number of rows returned */
rowCount: number
/** Total rows matching filter (before pagination) */
totalCount: number
/** Limit used in query */
limit: number
/** Offset used in query */
offset: number
}
/**
* Result of a bulk operation (update/delete by filter).
*/
export interface BulkOperationResult {
affectedCount: number
affectedRowIds: string[]
}
/**
* Data required to create a new table.
*/
export interface CreateTableData {
/** Table name */
name: string
/** Optional description */
description?: string
/** Table schema */
schema: TableSchema
/** Workspace ID */
workspaceId: string
/** User ID of creator */
userId: string
}
/**
* Data required to insert a row.
*/
export interface InsertRowData {
tableId: string
data: RowData
workspaceId: string
}
/**
* Data required for batch row insertion.
*/
export interface BatchInsertData {
tableId: string
rows: RowData[]
workspaceId: string
}
/**
* Data required to update a row.
*/
export interface UpdateRowData {
tableId: string
rowId: string
@@ -267,9 +164,6 @@ export interface UpdateRowData {
workspaceId: string
}
/**
* Data required for bulk update by filter.
*/
export interface BulkUpdateData {
tableId: string
filter: Filter
@@ -278,9 +172,6 @@ export interface BulkUpdateData {
workspaceId: string
}
/**
* Data required for bulk delete by filter.
*/
export interface BulkDeleteData {
tableId: string
filter: Filter