From b08ce03409ed198071d84f726d5ab959fba60906 Mon Sep 17 00:00:00 2001 From: Lakee Sivaraya Date: Wed, 14 Jan 2026 20:43:56 -0800 Subject: [PATCH] refactoring --- apps/sim/app/api/table/[tableId]/route.ts | 5 +- apps/sim/app/api/table/route.ts | 8 +- apps/sim/app/api/table/utils.ts | 31 +-- .../components/table-query-builder.tsx | 192 ++++-------------- .../[tableId]/components/table-row-modal.tsx | 18 +- .../hooks/use-context-menu.ts | 7 +- .../hooks/use-row-selection.ts | 4 +- .../table-data-viewer/hooks/use-table-data.ts | 16 +- .../table-data-viewer/table-data-viewer.tsx | 5 +- .../[tableId]/table-data-viewer/types.ts | 40 +--- .../tables/components/create-table-modal.tsx | 26 +-- .../tables/components/table-card.tsx | 2 +- .../filter-format/filter-format.tsx | 29 +-- .../components/sort-format/sort-format.tsx | 32 +-- .../sub-block/components/table/table.tsx | 17 +- .../editor/components/sub-block/sub-block.tsx | 27 +-- .../workflow-block/workflow-block.tsx | 6 +- apps/sim/hooks/queries/use-tables.ts | 2 +- apps/sim/lib/table/filters/builder-utils.ts | 61 +----- apps/sim/lib/table/hooks/index.ts | 1 - .../lib/table/hooks/use-builder-json-sync.ts | 66 ------ apps/sim/lib/table/index.ts | 4 +- apps/sim/lib/table/query-builder.ts | 12 +- apps/sim/lib/table/types.ts | 4 - apps/sim/tools/table/types.ts | 18 +- apps/sim/tools/table/upsert-row.ts | 3 +- apps/sim/tools/types.ts | 6 +- 27 files changed, 112 insertions(+), 530 deletions(-) delete mode 100644 apps/sim/lib/table/hooks/use-builder-json-sync.ts diff --git a/apps/sim/app/api/table/[tableId]/route.ts b/apps/sim/app/api/table/[tableId]/route.ts index c54946e4c..c60314bc6 100644 --- a/apps/sim/app/api/table/[tableId]/route.ts +++ b/apps/sim/app/api/table/[tableId]/route.ts @@ -3,8 +3,7 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkHybridAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' -import { deleteTable, getTableById } from '@/lib/table' -import type { TableSchemaData } from '../utils' +import { deleteTable, getTableById, type TableSchema } from '@/lib/table' import { checkTableAccess, checkTableWriteAccess, @@ -106,7 +105,7 @@ export async function GET(request: NextRequest, { params }: TableRouteParams) { logger.info(`[${requestId}] Retrieved table ${tableId} for user ${authResult.userId}`) - const schemaData = table.schema as TableSchemaData + const schemaData = table.schema as TableSchema return NextResponse.json({ success: true, diff --git a/apps/sim/app/api/table/route.ts b/apps/sim/app/api/table/route.ts index 13402072f..952dd0739 100644 --- a/apps/sim/app/api/table/route.ts +++ b/apps/sim/app/api/table/route.ts @@ -6,8 +6,8 @@ import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { checkHybridAuth } from '@/lib/auth/hybrid' import { generateRequestId } from '@/lib/core/utils/request' -import { createTable, listTables, TABLE_LIMITS } from '@/lib/table' -import { normalizeColumn, type TableSchemaData } from './utils' +import { createTable, listTables, TABLE_LIMITS, type TableSchema } from '@/lib/table' +import { normalizeColumn } from './utils' const logger = createLogger('TableAPI') @@ -192,7 +192,7 @@ export async function POST(request: NextRequest) { } // Normalize schema to ensure all fields have explicit defaults - const normalizedSchema: TableSchemaData = { + const normalizedSchema: TableSchema = { columns: params.schema.columns.map(normalizeColumn), } @@ -301,7 +301,7 @@ export async function GET(request: NextRequest) { success: true, data: { tables: tables.map((t) => { - const schemaData = t.schema as TableSchemaData + const schemaData = t.schema as TableSchema return { ...t, schema: { diff --git a/apps/sim/app/api/table/utils.ts b/apps/sim/app/api/table/utils.ts index 4eb245f61..2a6b1c706 100644 --- a/apps/sim/app/api/table/utils.ts +++ b/apps/sim/app/api/table/utils.ts @@ -3,12 +3,15 @@ import { userTableDefinitions } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq, isNull } from 'drizzle-orm' import { NextResponse } from 'next/server' +import type { ColumnDefinition, TableSchema } from '@/lib/table' import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils' const logger = createLogger('TableUtils') /** - * Represents the core data structure for a user-defined table. + * Represents the core data structure for a user-defined table as stored in the database. + * + * This extends the base TableDefinition with DB-specific fields like createdBy and deletedAt. */ export interface TableData { /** Unique identifier for the table */ @@ -22,7 +25,7 @@ export interface TableData { /** Optional description of the table's purpose */ description?: string | null /** JSON schema defining the table's column structure */ - schema: TableSchemaData + schema: TableSchema /** Maximum number of rows allowed in this table */ maxRows: number /** Current number of rows in the table */ @@ -35,28 +38,6 @@ export interface TableData { updatedAt: Date } -/** - * Schema structure for table columns stored in the database. - */ -export interface TableSchemaData { - /** Array of column definitions */ - columns: TableColumnData[] -} - -/** - * Represents a single column definition in the table schema. - */ -export interface TableColumnData { - /** Name of the column */ - name: string - /** Data type of the column */ - type: 'string' | 'number' | 'boolean' | 'date' | 'json' - /** Whether this column is required */ - required?: boolean - /** Whether this column must have unique values */ - unique?: boolean -} - /** * Result returned when a user has access to a table. */ @@ -492,7 +473,7 @@ export function serverErrorResponse( * @param col - The column definition to normalize * @returns A normalized column with explicit required and unique values */ -export function normalizeColumn(col: TableColumnData): TableColumnData { +export function normalizeColumn(col: ColumnDefinition): ColumnDefinition { return { name: col.name, type: col.type, diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-query-builder.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-query-builder.tsx index bbe808926..6858745a7 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-query-builder.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-query-builder.tsx @@ -2,33 +2,21 @@ import { useCallback, useMemo, useState } from 'react' import { ArrowDownAZ, ArrowUpAZ, Plus, X } from 'lucide-react' +import { nanoid } from 'nanoid' import { Button, Combobox, Input } from '@/components/emcn' -import type { FilterCondition } from '@/lib/table/filters/constants' +import { conditionsToFilter } from '@/lib/table/filters/builder-utils' +import type { FilterCondition, SortCondition } from '@/lib/table/filters/constants' import { useFilterBuilder } from '@/lib/table/filters/use-builder' - -/** - * Represents a sort configuration. - */ -export interface SortConfig { - /** Column to sort by */ - column: string - /** Sort direction */ - direction: 'asc' | 'desc' -} - -/** - * Filter value structure for API queries. - */ -type FilterValue = string | number | boolean | null | FilterValue[] | { [key: string]: FilterValue } +import type { JsonValue } from '@/lib/table/types' /** * Query options for the table API. */ export interface QueryOptions { /** Filter criteria or null for no filter, keys are column names, values are filter values */ - filter: Record | null + filter: Record | null /** Sort configuration or null for default sort */ - sort: SortConfig | null + sort: SortCondition | null } /** @@ -53,111 +41,6 @@ interface TableQueryBuilderProps { onAddRow: () => void } -/** - * Parses a string value into its appropriate type. - * - * @param value - String value to parse - * @returns Parsed value (boolean, null, number, or string) - */ -function parseValue(value: string): string | number | boolean | null { - if (value === 'true') return true - if (value === 'false') return false - if (value === 'null') return null - if (!Number.isNaN(Number(value)) && value !== '') return Number(value) - return value -} - -/** - * Parses a comma-separated string into an array of values. - * - * @param value - Comma-separated string - * @returns Array of parsed values - */ -function parseArrayValue(value: string): FilterValue[] { - return value.split(',').map((v) => { - const trimmed = v.trim() - return parseValue(trimmed) - }) -} - -/** - * Converts builder filter conditions to a MongoDB-style filter object. - * - * Iterates through an array of filter conditions, combining them into a filter expression object - * that is compatible with MongoDB's query format. Supports both "AND" and "OR" logical groupings: - * - * - "AND" conditions are grouped together in objects. - * - "OR" conditions start new groups; groups are merged under a single `$or` array. - * - * @param conditions - The list of filter conditions specified by the user. - * @returns A filter object to send to the API, or null if there are no conditions. - * - * @example - * [ - * { logicalOperator: 'and', column: 'age', operator: 'gt', value: '18' }, - * { logicalOperator: 'or', column: 'role', operator: 'eq', value: 'admin' } - * ] - * // => - * { - * $or: [ - * { age: { $gt: 18 } }, - * { role: 'admin' } - * ] - * } - */ -function conditionsToFilter(conditions: FilterCondition[]): Record | null { - // Return null if there are no filter conditions. - if (conditions.length === 0) return null - - // Groups for $or logic; each group is an AND-combined object. - const orGroups: Record[] = [] - // Current group of AND'ed conditions. - let currentAndGroup: Record = {} - - conditions.forEach((condition, index) => { - const { column, operator, value } = condition - const operatorKey = `$${operator}` - - // Parse value as per operator: 'in' receives an array, others get a primitive value. - let parsedValue: FilterValue = value - if (operator === 'in') { - parsedValue = parseArrayValue(value) - } else { - parsedValue = parseValue(value) - } - - // For 'eq', value is direct (shorthand), otherwise use a key for the operator. - const conditionObj: FilterValue = - operator === 'eq' ? parsedValue : { [operatorKey]: parsedValue } - - // Group logic: - // - First condition or 'and': add to the current AND group. - // - 'or': finalize current AND group and start a new one. - if (index === 0 || condition.logicalOperator === 'and') { - currentAndGroup[column] = conditionObj - } else if (condition.logicalOperator === 'or') { - if (Object.keys(currentAndGroup).length > 0) { - // Finalize and push the previous AND group to $or groups. - orGroups.push({ ...currentAndGroup }) - } - // Start a new AND group for subsequent conditions. - currentAndGroup = { [column]: conditionObj } - } - }) - - // Push the last AND group, if any, to the orGroups list. - if (Object.keys(currentAndGroup).length > 0) { - orGroups.push(currentAndGroup) - } - - // If multiple groups exist, return as a $or query; otherwise, return the single group. - if (orGroups.length > 1) { - return { $or: orGroups } - } - - return orGroups[0] || null -} - /** * Component for building filter and sort queries for table data. * @@ -178,7 +61,7 @@ function conditionsToFilter(conditions: FilterCondition[]): Record([]) - const [sortConfig, setSortConfig] = useState(null) + const [sortCondition, setSortCondition] = useState(null) const columnOptions = useMemo( () => columns.map((col) => ({ value: col.name, label: col.name })), @@ -200,46 +83,47 @@ export function TableQueryBuilder({ columns, onApply, onAddRow }: TableQueryBuil }) /** - * Adds a sort configuration. + * Adds a sort condition. */ const handleAddSort = useCallback(() => { - setSortConfig({ + setSortCondition({ + id: nanoid(), column: columns[0]?.name || '', direction: 'asc', }) }, [columns]) /** - * Removes the sort configuration. + * Removes the sort condition. */ const handleRemoveSort = useCallback(() => { - setSortConfig(null) + setSortCondition(null) }, []) /** - * Applies the current filter and sort configuration. + * Applies the current filter and sort conditions. */ const handleApply = useCallback(() => { const filter = conditionsToFilter(conditions) onApply({ filter, - sort: sortConfig, + sort: sortCondition, }) - }, [conditions, sortConfig, onApply]) + }, [conditions, sortCondition, onApply]) /** - * Clears all filters and sort configuration. + * Clears all filters and sort conditions. */ const handleClear = useCallback(() => { setConditions([]) - setSortConfig(null) + setSortCondition(null) onApply({ filter: null, sort: null, }) }, [onApply]) - const hasChanges = conditions.length > 0 || sortConfig !== null + const hasChanges = conditions.length > 0 || sortCondition !== null return (
@@ -259,12 +143,12 @@ export function TableQueryBuilder({ columns, onApply, onAddRow }: TableQueryBuil ))} {/* Sort Row */} - {sortConfig && ( - )} @@ -281,7 +165,7 @@ export function TableQueryBuilder({ columns, onApply, onAddRow }: TableQueryBuil Add filter - {!sortConfig && ( + {!sortCondition && (
@@ -460,13 +344,13 @@ function SortConfigRow({ onChange({ ...sortConfig, direction: value as 'asc' | 'desc' })} + value={sortCondition.direction} + onChange={(value) => onChange({ ...sortCondition, direction: value as 'asc' | 'desc' })} />
- {sortConfig.direction === 'asc' ? ( + {sortCondition.direction === 'asc' ? ( ) : ( diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-row-modal.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-row-modal.tsx index 91b85d16e..7b676774b 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-row-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-row-modal.tsx @@ -16,24 +16,10 @@ import { Textarea, } from '@/components/emcn' import { Input } from '@/components/ui/input' -import type { ColumnDefinition, TableSchema } from '@/lib/table' +import type { ColumnDefinition, TableRow, TableSchema } from '@/lib/table' const logger = createLogger('TableRowModal') -/** - * Represents row data from the table. - */ -export interface TableRowData { - /** Unique identifier for the row */ - id: string - /** Row field values keyed by column name */ - data: Record - /** ISO timestamp when the row was created */ - createdAt: string - /** ISO timestamp when the row was last updated */ - updatedAt: string -} - /** * Table metadata needed for row operations. */ @@ -64,7 +50,7 @@ export interface TableRowModalProps { /** Table to operate on */ table: TableInfo /** Row being edited/deleted (required for edit/delete modes) */ - row?: TableRowData + row?: TableRow /** Row IDs to delete (for delete mode batch operations) */ rowIds?: string[] /** Callback when operation is successful */ diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/hooks/use-context-menu.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/hooks/use-context-menu.ts index 5223ff306..ab06152e4 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/hooks/use-context-menu.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/hooks/use-context-menu.ts @@ -5,11 +5,12 @@ */ import { useCallback, useState } from 'react' -import type { ContextMenuState, TableRowData } from '../types' +import type { TableRow } from '@/lib/table' +import type { ContextMenuState } from '../types' interface UseContextMenuReturn { contextMenu: ContextMenuState - handleRowContextMenu: (e: React.MouseEvent, row: TableRowData) => void + handleRowContextMenu: (e: React.MouseEvent, row: TableRow) => void closeContextMenu: () => void } @@ -28,7 +29,7 @@ export function useContextMenu(): UseContextMenuReturn { /** * Opens the context menu for a row. */ - const handleRowContextMenu = useCallback((e: React.MouseEvent, row: TableRowData) => { + const handleRowContextMenu = useCallback((e: React.MouseEvent, row: TableRow) => { e.preventDefault() e.stopPropagation() setContextMenu({ diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/hooks/use-row-selection.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/hooks/use-row-selection.ts index dc0c07668..cd85687d5 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/hooks/use-row-selection.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/hooks/use-row-selection.ts @@ -5,7 +5,7 @@ */ import { useCallback, useState } from 'react' -import type { TableRowData } from '../types' +import type { TableRow } from '@/lib/table' interface UseRowSelectionReturn { selectedRows: Set @@ -20,7 +20,7 @@ interface UseRowSelectionReturn { * @param rows - The current rows to select from * @returns Selection state and handlers */ -export function useRowSelection(rows: TableRowData[]): UseRowSelectionReturn { +export function useRowSelection(rows: TableRow[]): UseRowSelectionReturn { const [selectedRows, setSelectedRows] = useState>(new Set()) /** diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/hooks/use-table-data.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/hooks/use-table-data.ts index affcc746b..aee49de97 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/hooks/use-table-data.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/hooks/use-table-data.ts @@ -5,9 +5,9 @@ */ import { useQuery } from '@tanstack/react-query' +import type { TableDefinition, TableRow } from '@/lib/table' import type { QueryOptions } from '../../components/table-query-builder' import { ROWS_PER_PAGE } from '../constants' -import type { TableData, TableRowData } from '../types' interface UseTableDataParams { workspaceId: string @@ -17,9 +17,9 @@ interface UseTableDataParams { } interface UseTableDataReturn { - tableData: TableData | undefined + tableData: TableDefinition | undefined isLoadingTable: boolean - rows: TableRowData[] + rows: TableRow[] totalCount: number totalPages: number isLoadingRows: boolean @@ -43,9 +43,9 @@ export function useTableData({ queryFn: async () => { const res = await fetch(`/api/table/${tableId}?workspaceId=${workspaceId}`) if (!res.ok) throw new Error('Failed to fetch table') - const json: { data?: { table: TableData }; table?: TableData } = await res.json() + const json: { data?: { table: TableDefinition }; table?: TableDefinition } = await res.json() const data = json.data || json - return (data as { table: TableData }).table + return (data as { table: TableDefinition }).table }, }) @@ -74,8 +74,8 @@ export function useTableData({ const res = await fetch(`/api/table/${tableId}/rows?${searchParams}`) if (!res.ok) throw new Error('Failed to fetch rows') const json: { - data?: { rows: TableRowData[]; totalCount: number } - rows?: TableRowData[] + data?: { rows: TableRow[]; totalCount: number } + rows?: TableRow[] totalCount?: number } = await res.json() return json.data || json @@ -83,7 +83,7 @@ export function useTableData({ enabled: !!tableData, }) - const rows = (rowsData?.rows || []) as TableRowData[] + const rows = (rowsData?.rows || []) as TableRow[] const totalCount = rowsData?.totalCount || 0 const totalPages = Math.ceil(totalCount / ROWS_PER_PAGE) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/table-data-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/table-data-viewer.tsx index bbf15ec22..e9a40e326 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/table-data-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/table-data-viewer.tsx @@ -19,6 +19,7 @@ import { TableRow, } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' +import type { TableRow as TableRowType } from '@/lib/table' import { type QueryOptions, TableActionBar, TableQueryBuilder, TableRowModal } from '../components' import { CellViewerModal, @@ -31,7 +32,7 @@ import { TablePagination, } from './components' import { useContextMenu, useRowSelection, useTableData } from './hooks' -import type { CellViewerData, TableRowData } from './types' +import type { CellViewerData } from './types' /** * Main component for viewing and managing table data. @@ -62,7 +63,7 @@ export function TableDataViewer() { const [currentPage, setCurrentPage] = useState(0) const [showAddModal, setShowAddModal] = useState(false) - const [editingRow, setEditingRow] = useState(null) + const [editingRow, setEditingRow] = useState(null) const [deletingRows, setDeletingRows] = useState([]) const [showSchemaModal, setShowSchemaModal] = useState(false) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/types.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/types.ts index d0d28274e..4fa151187 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/types.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/types.ts @@ -4,43 +4,7 @@ * @module tables/[tableId]/table-data-viewer/types */ -import type { TableSchema } from '@/lib/table' - -/** - * Represents row data stored in a table. - */ -export interface TableRowData { - /** Unique identifier for the row */ - id: string - /** Row field values keyed by column name */ - data: Record - /** ISO timestamp when the row was created */ - createdAt: string - /** ISO timestamp when the row was last updated */ - updatedAt: string -} - -/** - * Represents table metadata. - */ -export interface TableData { - /** Unique identifier for the table */ - id: string - /** Table name */ - name: string - /** Optional description */ - description?: string - /** Schema defining columns */ - schema: TableSchema - /** Current number of rows */ - rowCount: number - /** Maximum allowed rows */ - maxRows: number - /** ISO timestamp when created */ - createdAt: string - /** ISO timestamp when last updated */ - updatedAt: string -} +import type { TableRow } from '@/lib/table' /** * Data for the cell viewer modal. @@ -63,5 +27,5 @@ export interface ContextMenuState { /** Screen position of the menu */ position: { x: number; y: number } /** Row the menu was opened on */ - row: TableRowData | null + row: TableRow | null } diff --git a/apps/sim/app/workspace/[workspaceId]/tables/components/create-table-modal.tsx b/apps/sim/app/workspace/[workspaceId]/tables/components/create-table-modal.tsx index 831be7b4b..6b22d625a 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/components/create-table-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/components/create-table-modal.tsx @@ -17,29 +17,11 @@ import { ModalHeader, Textarea, } from '@/components/emcn' +import type { ColumnDefinition, ColumnType } from '@/lib/table' import { useCreateTable } from '@/hooks/queries/use-tables' const logger = createLogger('CreateTableModal') -/** - * Supported column data types for table schemas. - */ -type ColumnType = 'string' | 'number' | 'boolean' | 'date' | 'json' - -/** - * Definition for a single table column. - */ -interface ColumnDefinition { - /** Name of the column */ - name: string - /** Data type of the column */ - type: ColumnType - /** Whether this column is required */ - required: boolean - /** Whether this column must have unique values */ - unique: boolean -} - /** * Props for the CreateTableModal component. */ @@ -51,9 +33,9 @@ interface CreateTableModalProps { } /** - * Available column type options for the combobox. + * Available column type options for the combobox UI. */ -const COLUMN_TYPES: Array<{ value: ColumnType; label: string }> = [ +const COLUMN_TYPE_OPTIONS: Array<{ value: ColumnType; label: string }> = [ { value: 'string', label: 'String' }, { value: 'number', label: 'Number' }, { value: 'boolean', label: 'Boolean' }, @@ -356,7 +338,7 @@ function ColumnRow({ column, index, isRemovable, onChange, onRemove }: ColumnRow {/* Column Type */}
onChange(index, 'type', value as ColumnType)} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/components/table-card.tsx b/apps/sim/app/workspace/[workspaceId]/tables/components/table-card.tsx index 3ada80250..cf174af67 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/components/table-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/components/table-card.tsx @@ -24,8 +24,8 @@ import { TableRow, Tooltip, } from '@/components/emcn' +import type { TableDefinition } from '@/lib/table' import { useDeleteTable } from '@/hooks/queries/use-tables' -import type { TableDefinition } from '@/tools/table/types' const logger = createLogger('TableCard') diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-format/filter-format.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-format/filter-format.tsx index f540db709..d5fd02a81 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-format/filter-format.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-format/filter-format.tsx @@ -3,10 +3,9 @@ import { useMemo } from 'react' import { Plus } from 'lucide-react' import { Button } from '@/components/emcn' -import { conditionsToJsonString, jsonStringToConditions } from '@/lib/table/filters/builder-utils' import type { FilterCondition } from '@/lib/table/filters/constants' import { useFilterBuilder } from '@/lib/table/filters/use-builder' -import { useBuilderJsonSync, useTableColumns } from '@/lib/table/hooks' +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' @@ -19,15 +18,10 @@ interface FilterFormatProps { disabled?: boolean columns?: Array<{ value: string; label: string }> tableIdSubBlockId?: string - modeSubBlockId?: string - jsonSubBlockId?: string } /** - * Visual builder for filter conditions with optional JSON sync. - * - * When `modeSubBlockId` and `jsonSubBlockId` are provided, handles bidirectional - * conversion between builder conditions and JSON format. + * Visual builder for filter conditions. */ export function FilterFormat({ blockId, @@ -37,16 +31,9 @@ export function FilterFormat({ disabled = false, columns: propColumns, tableIdSubBlockId = 'tableId', - modeSubBlockId, - jsonSubBlockId, }: FilterFormatProps) { const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) const [tableIdValue] = useSubBlockValue(blockId, tableIdSubBlockId) - const [modeValue] = useSubBlockValue(blockId, modeSubBlockId || '_unused_mode') - const [jsonValue, setJsonValue] = useSubBlockValue( - blockId, - jsonSubBlockId || '_unused_json' - ) const dynamicColumns = useTableColumns({ tableId: tableIdValue }) const columns = useMemo(() => { @@ -58,18 +45,6 @@ export function FilterFormat({ const conditions: FilterCondition[] = Array.isArray(value) && value.length > 0 ? value : [] const isReadOnly = isPreview || disabled - useBuilderJsonSync({ - modeValue, - jsonValue, - setJsonValue, - isPreview, - conditions, - setConditions: setStoreValue, - jsonToConditions: jsonStringToConditions, - conditionsToJson: conditionsToJsonString, - enabled: Boolean(modeSubBlockId && jsonSubBlockId), - }) - const { comparisonOptions, logicalOptions, addCondition, removeCondition, updateCondition } = useFilterBuilder({ columns, diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-format/sort-format.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-format/sort-format.tsx index a3598568c..d980a5002 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-format/sort-format.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-format/sort-format.tsx @@ -4,12 +4,8 @@ import { useCallback, useMemo } from 'react' import { Plus } from 'lucide-react' import { nanoid } from 'nanoid' import { Button, type ComboboxOption } from '@/components/emcn' -import { - jsonStringToSortConditions, - sortConditionsToJsonString, -} from '@/lib/table/filters/builder-utils' import { SORT_DIRECTIONS, type SortCondition } from '@/lib/table/filters/constants' -import { useBuilderJsonSync, useTableColumns } from '@/lib/table/hooks' +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' @@ -22,8 +18,6 @@ interface SortFormatProps { disabled?: boolean columns?: Array<{ value: string; label: string }> tableIdSubBlockId?: string - modeSubBlockId?: string - jsonSubBlockId?: string } const createDefaultCondition = (columns: ComboboxOption[]): SortCondition => ({ @@ -33,10 +27,7 @@ const createDefaultCondition = (columns: ComboboxOption[]): SortCondition => ({ }) /** - * Visual builder for sort conditions with optional JSON sync. - * - * When `modeSubBlockId` and `jsonSubBlockId` are provided, handles bidirectional - * conversion between builder conditions and JSON format. + * Visual builder for sort conditions. */ export function SortFormat({ blockId, @@ -46,16 +37,9 @@ export function SortFormat({ disabled = false, columns: propColumns, tableIdSubBlockId = 'tableId', - modeSubBlockId, - jsonSubBlockId, }: SortFormatProps) { const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) const [tableIdValue] = useSubBlockValue(blockId, tableIdSubBlockId) - const [modeValue] = useSubBlockValue(blockId, modeSubBlockId || '_unused_mode') - const [jsonValue, setJsonValue] = useSubBlockValue( - blockId, - jsonSubBlockId || '_unused_json' - ) const dynamicColumns = useTableColumns({ tableId: tableIdValue, includeBuiltIn: true }) const columns = useMemo(() => { @@ -72,18 +56,6 @@ export function SortFormat({ const conditions: SortCondition[] = Array.isArray(value) && value.length > 0 ? value : [] const isReadOnly = isPreview || disabled - useBuilderJsonSync({ - modeValue, - jsonValue, - setJsonValue, - isPreview, - conditions, - setConditions: setStoreValue, - jsonToConditions: jsonStringToSortConditions, - conditionsToJson: sortConditionsToJsonString, - enabled: Boolean(modeSubBlockId && jsonSubBlockId), - }) - const addCondition = useCallback(() => { if (isReadOnly) return setStoreValue([...conditions, createDefaultCondition(columns)]) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table/table.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table/table.tsx index cb0c51fc4..26761c5cf 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/table/table.tsx @@ -19,11 +19,11 @@ interface TableProps { subBlockId: string columns: string[] isPreview?: boolean - previewValue?: TableRow[] | null + previewValue?: WorkflowTableRow[] | null disabled?: boolean } -interface TableRow { +interface WorkflowTableRow { id: string cells: Record } @@ -38,7 +38,7 @@ export function Table({ }: TableProps) { const params = useParams() const workspaceId = params.workspaceId as string - const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) + const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) const accessiblePrefixes = useAccessibleReferencePrefixes(blockId) // Use the extended hook for field-level management @@ -73,7 +73,7 @@ export function Table({ */ useEffect(() => { if (!isPreview && !disabled && (!Array.isArray(storeValue) || storeValue.length === 0)) { - const initialRow: TableRow = { + const initialRow: WorkflowTableRow = { id: crypto.randomUUID(), cells: { ...emptyCellsTemplate }, } @@ -110,7 +110,7 @@ export function Table({ } }) - return validatedRows as TableRow[] + return validatedRows as WorkflowTableRow[] }, [value, emptyCellsTemplate]) // Helper to update a cell value @@ -164,7 +164,12 @@ export function Table({ ) - const renderCell = (row: TableRow, rowIndex: number, column: string, cellIndex: number) => { + const renderCell = ( + row: WorkflowTableRow, + rowIndex: number, + column: string, + cellIndex: number + ) => { // Defensive programming: ensure row.cells exists and has the expected structure const hasValidCells = row.cells && typeof row.cells === 'object' if (!hasValidCells) logger.warn('Table row has malformed cells data:', row) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx index efda1c05d..ae4903c53 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/sub-block.tsx @@ -814,17 +814,7 @@ function SubBlockComponent({ /> ) - case 'filter-format': { - // Determine sync props based on subBlockId - let modeSubBlockId: string | undefined - let jsonSubBlockId: string | undefined - if (config.id === 'filterBuilder') { - modeSubBlockId = 'builderMode' - jsonSubBlockId = 'filter' - } else if (config.id === 'bulkFilterBuilder') { - modeSubBlockId = 'bulkFilterMode' - jsonSubBlockId = 'filterCriteria' - } + case 'filter-format': return ( ) - } - case 'sort-format': { - // Determine sync props based on subBlockId - let modeSubBlockId: string | undefined - let jsonSubBlockId: string | undefined - if (config.id === 'sortBuilder') { - modeSubBlockId = 'builderMode' - jsonSubBlockId = 'sort' - } + case 'sort-format': return ( ) - } case 'channel-selector': case 'user-selector': diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx index cfaa834b4..964e00e97 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/workflow-block.tsx @@ -46,9 +46,9 @@ import { wouldCreateCycle } from '@/stores/workflows/workflow/utils' const logger = createLogger('WorkflowBlock') /** - * Type guard for table row structure + * Type guard for workflow table row structure (sub-block table inputs) */ -interface TableRow { +interface WorkflowTableRow { id: string cells: Record } @@ -67,7 +67,7 @@ interface FieldFormat { /** * Checks if a value is a table row array */ -const isTableRowArray = (value: unknown): value is TableRow[] => { +const isTableRowArray = (value: unknown): value is WorkflowTableRow[] => { if (!Array.isArray(value) || value.length === 0) return false const firstItem = value[0] return ( diff --git a/apps/sim/hooks/queries/use-tables.ts b/apps/sim/hooks/queries/use-tables.ts index cba8f63c7..4cf4864a9 100644 --- a/apps/sim/hooks/queries/use-tables.ts +++ b/apps/sim/hooks/queries/use-tables.ts @@ -3,7 +3,7 @@ */ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' -import type { TableDefinition } from '@/tools/table/types' +import type { TableDefinition } from '@/lib/table' export const tableKeys = { all: ['tables'] as const, diff --git a/apps/sim/lib/table/filters/builder-utils.ts b/apps/sim/lib/table/filters/builder-utils.ts index 011cddfde..d0cb3a481 100644 --- a/apps/sim/lib/table/filters/builder-utils.ts +++ b/apps/sim/lib/table/filters/builder-utils.ts @@ -5,10 +5,9 @@ */ import { nanoid } from 'nanoid' +import type { JsonValue } from '../types' import type { FilterCondition, SortCondition } from './constants' -type JsonValue = string | number | boolean | null | JsonValue[] | { [key: string]: JsonValue } - /** * Parses a string value into its appropriate type based on the operator. * @@ -163,35 +162,6 @@ function formatValueForBuilder(value: JsonValue): string { return String(value) } -/** - * Converts builder conditions to JSON string. - * - * @param conditions - Array of filter conditions - * @returns JSON string representation - */ -export function conditionsToJsonString(conditions: FilterCondition[]): string { - const filter = conditionsToFilter(conditions) - if (!filter) return '' - return JSON.stringify(filter, null, 2) -} - -/** - * Converts JSON string to builder conditions. - * - * @param jsonString - JSON string to parse - * @returns Array of filter conditions or empty array if invalid - */ -export function jsonStringToConditions(jsonString: string): FilterCondition[] { - if (!jsonString || !jsonString.trim()) return [] - - try { - const filter = JSON.parse(jsonString) - return filterToConditions(filter) - } catch { - return [] - } -} - /** * Converts builder sort conditions to sort object. * @@ -226,32 +196,3 @@ export function sortToConditions(sort: Record | null): SortCondi direction: direction === 'desc' ? 'desc' : 'asc', })) } - -/** - * Converts builder sort conditions to JSON string. - * - * @param conditions - Array of sort conditions - * @returns JSON string representation - */ -export function sortConditionsToJsonString(conditions: SortCondition[]): string { - const sort = sortConditionsToSort(conditions) - if (!sort) return '' - return JSON.stringify(sort, null, 2) -} - -/** - * Converts JSON string to sort builder conditions. - * - * @param jsonString - JSON string to parse - * @returns Array of sort conditions or empty array if invalid - */ -export function jsonStringToSortConditions(jsonString: string): SortCondition[] { - if (!jsonString || !jsonString.trim()) return [] - - try { - const sort = JSON.parse(jsonString) - return sortToConditions(sort) - } catch { - return [] - } -} diff --git a/apps/sim/lib/table/hooks/index.ts b/apps/sim/lib/table/hooks/index.ts index 207dd68c8..ff4efabe1 100644 --- a/apps/sim/lib/table/hooks/index.ts +++ b/apps/sim/lib/table/hooks/index.ts @@ -1,2 +1 @@ -export { useBuilderJsonSync } from './use-builder-json-sync' export { useTableColumns } from './use-table-columns' diff --git a/apps/sim/lib/table/hooks/use-builder-json-sync.ts b/apps/sim/lib/table/hooks/use-builder-json-sync.ts deleted file mode 100644 index 9819d54b8..000000000 --- a/apps/sim/lib/table/hooks/use-builder-json-sync.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { useEffect, useRef } from 'react' - -interface UseBuilderJsonSyncOptions { - modeValue: string | null - jsonValue: string | null - setJsonValue: (value: string) => void - isPreview: boolean - conditions: T[] - setConditions: (conditions: T[]) => void - jsonToConditions: (json: string) => T[] - conditionsToJson: (conditions: T[]) => string - enabled?: boolean -} - -/** - * Handles bidirectional sync between builder conditions and JSON format. - * - * - JSON → Builder: When mode switches to 'builder', parses JSON into conditions - * - Builder → JSON: When conditions change in builder mode, converts to JSON - */ -export function useBuilderJsonSync({ - modeValue, - jsonValue, - setJsonValue, - isPreview, - conditions, - setConditions, - jsonToConditions, - conditionsToJson, - enabled = true, -}: UseBuilderJsonSyncOptions) { - const prevModeRef = useRef(null) - const isSyncingRef = useRef(false) - - // Sync JSON → Builder when switching to builder mode - useEffect(() => { - if (!enabled || isPreview) return - - const switchingToBuilder = - prevModeRef.current !== null && prevModeRef.current !== 'builder' && modeValue === 'builder' - - if (switchingToBuilder && jsonValue?.trim()) { - isSyncingRef.current = true - const parsedConditions = jsonToConditions(jsonValue) - if (parsedConditions.length > 0) { - setConditions(parsedConditions) - } - isSyncingRef.current = false - } - - prevModeRef.current = modeValue - }, [modeValue, jsonValue, setConditions, isPreview, jsonToConditions, enabled]) - - // Sync Builder → JSON when conditions change in builder mode - useEffect(() => { - if (!enabled || isPreview || isSyncingRef.current) return - if (modeValue !== 'builder') return - - if (conditions.length > 0) { - const newJson = conditionsToJson(conditions) - if (newJson !== jsonValue) { - setJsonValue(newJson) - } - } - }, [conditions, modeValue, jsonValue, setJsonValue, isPreview, conditionsToJson, enabled]) -} diff --git a/apps/sim/lib/table/index.ts b/apps/sim/lib/table/index.ts index 66324e4a7..820c60e95 100644 --- a/apps/sim/lib/table/index.ts +++ b/apps/sim/lib/table/index.ts @@ -4,12 +4,14 @@ * 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. + * * @module lib/table */ export * from './constants' export * from './filters' -export * from './hooks' export * from './query-builder' export * from './service' export * from './types' diff --git a/apps/sim/lib/table/query-builder.ts b/apps/sim/lib/table/query-builder.ts index 8cbd89169..11418f39e 100644 --- a/apps/sim/lib/table/query-builder.ts +++ b/apps/sim/lib/table/query-builder.ts @@ -10,12 +10,6 @@ import { sql } from 'drizzle-orm' import { NAME_PATTERN } from './constants' import type { FilterOperators, JsonValue, QueryFilter } from './types' -/** - * Field condition is an alias for FilterOperators. - * @deprecated Use FilterOperators from types.ts instead. - */ -export type FieldCondition = FilterOperators - /** * Whitelist of allowed operators for query filtering. * Only these operators can be used in filter conditions. @@ -107,7 +101,7 @@ function buildContainsClause(tableName: string, field: string, value: string): S * * @param tableName - The name of the table to query (used for SQL table reference) * @param field - The field name to filter on (must match NAME_PATTERN) - * @param condition - Either a simple value (for equality) or a FieldCondition + * @param condition - Either a simple value (for equality) or a FilterOperators * object with operators like $eq, $gt, $in, etc. * @returns Array of SQL condition fragments. Multiple conditions are returned * when the condition object contains multiple operators. @@ -116,7 +110,7 @@ function buildContainsClause(tableName: string, field: string, value: string): S function buildFieldCondition( tableName: string, field: string, - condition: JsonValue | FieldCondition + condition: JsonValue | FilterOperators ): SQL[] { validateFieldName(field) @@ -249,7 +243,7 @@ export function buildFilterClause(filter: QueryFilter, tableName: string): SQL | const fieldConditions = buildFieldCondition( tableName, field, - condition as JsonValue | FieldCondition + condition as JsonValue | FilterOperators ) conditions.push(...fieldConditions) } diff --git a/apps/sim/lib/table/types.ts b/apps/sim/lib/table/types.ts index c649c4eb2..7aad928e0 100644 --- a/apps/sim/lib/table/types.ts +++ b/apps/sim/lib/table/types.ts @@ -1,15 +1,11 @@ /** * Core type definitions for user-defined tables. * - * This module provides the single source of truth for all table-related types. - * Import types from here rather than defining them locally. - * * @module lib/table/types */ import type { ColumnType } from './constants' -// Re-export ColumnType for convenience export type { ColumnType } /** Primitive values that can be stored in table columns */ diff --git a/apps/sim/tools/table/types.ts b/apps/sim/tools/table/types.ts index ffb66e3c3..80a471b95 100644 --- a/apps/sim/tools/table/types.ts +++ b/apps/sim/tools/table/types.ts @@ -1,20 +1,3 @@ -import type { ToolResponse } from '@/tools/types' - -// Re-export shared types from lib/table for convenience -export type { - ColumnDefinition, - ColumnType, - ColumnValue, - FilterOperators, - JsonValue, - QueryFilter, - RowData, - TableDefinition, - TableRow, - TableSchema, -} from '@/lib/table/types' - -// Import types for use in this file import type { ColumnDefinition, QueryFilter, @@ -23,6 +6,7 @@ import type { TableRow, TableSchema, } from '@/lib/table/types' +import type { ToolResponse } from '@/tools/types' /** * Execution context provided by the workflow executor diff --git a/apps/sim/tools/table/upsert-row.ts b/apps/sim/tools/table/upsert-row.ts index a1c577db2..d4755e1d1 100644 --- a/apps/sim/tools/table/upsert-row.ts +++ b/apps/sim/tools/table/upsert-row.ts @@ -1,5 +1,6 @@ +import type { TableRow } from '@/lib/table' import type { ToolConfig, ToolResponse } from '@/tools/types' -import type { TableRow, TableRowInsertParams } from './types' +import type { TableRowInsertParams } from './types' interface TableUpsertResponse extends ToolResponse { output: { diff --git a/apps/sim/tools/types.ts b/apps/sim/tools/types.ts index b307cb9d7..326fd6918 100644 --- a/apps/sim/tools/types.ts +++ b/apps/sim/tools/types.ts @@ -112,7 +112,8 @@ export interface ToolConfig

{ directExecution?: (params: P) => Promise } -export interface TableRow { +/** Key-value pair row for HTTP request tables (headers, params) */ +export interface KeyValueRow { id: string cells: { Key: string @@ -120,6 +121,9 @@ export interface TableRow { } } +/** @deprecated Use KeyValueRow instead */ +export type TableRow = KeyValueRow + export interface OAuthTokenPayload { credentialId?: string credentialAccountUserId?: string