diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/filter-builder.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/filter-builder.tsx index c2b798ae2..197d2a686 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/filter-builder.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/filter-builder.tsx @@ -69,7 +69,7 @@ type FilterValue = string | number | boolean | null | FilterValue[] | { [key: st * Query options for the table API. */ export interface QueryOptions { - /** Filter criteria or null for no filter */ + /** Filter criteria or null for no filter, keys are column names, values are filter values */ filter: Record | null /** Sort configuration or null for default sort */ sort: SortConfig | null @@ -82,7 +82,7 @@ interface Column { /** Column name */ name: string /** Column data type */ - type: string + type: 'string' | 'number' | 'boolean' | 'json' | 'date' } /** @@ -134,21 +134,44 @@ function parseArrayValue(value: string): FilterValue[] { } /** - * Converts filter conditions to MongoDB-style filter object. + * Converts builder filter conditions to a MongoDB-style filter object. * - * @param conditions - Array of filter conditions - * @returns Filter object for API or null if no conditions + * 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) @@ -156,23 +179,31 @@ function conditionsToFilter(conditions: FilterCondition[]): Record 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 } } diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/cell-renderer.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/cell-renderer.tsx index 8d7ec8f9d..a8f5c7549 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/cell-renderer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/cell-renderer.tsx @@ -89,7 +89,6 @@ export function CellRenderer({ value, column, onCellClick }: CellRendererProps) } } - // Handle long strings const strValue = String(value) if (strValue.length > STRING_TRUNCATE_LENGTH) { return ( diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/empty-rows.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/empty-rows.tsx deleted file mode 100644 index ad53d2a82..000000000 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/empty-rows.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Empty state component for table rows. - * - * @module tables/[tableId]/table-data-viewer/components/empty-rows - */ - -import { Plus } from 'lucide-react' -import { Button, TableCell, TableRow } from '@/components/emcn' - -interface EmptyRowsProps { - columnCount: number - hasFilter: boolean - onAddRow: () => void -} - -/** - * Renders an empty state when no rows are present. - * - * @param props - Component props - * @returns Empty state row - */ -export function EmptyRows({ columnCount, hasFilter, onAddRow }: EmptyRowsProps) { - return ( - - -
- - {hasFilter ? 'No rows match your filter' : 'No data'} - - {!hasFilter && ( - - )} -
-
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/index.ts b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/index.ts index 76e7c8ac6..47b6bd7ed 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/index.ts @@ -6,9 +6,8 @@ export * from './cell-renderer' export * from './cell-viewer-modal' -export * from './empty-rows' -export * from './loading-rows' export * from './pagination' export * from './row-context-menu' export * from './schema-viewer-modal' +export * from './table-body-states' export * from './table-header-bar' diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/row-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/row-context-menu.tsx index e860da611..25be7f539 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/row-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/row-context-menu.tsx @@ -45,13 +45,7 @@ export function RowContextMenu({ contextMenu, onClose, onEdit, onDelete }: RowCo height: '1px', }} /> - e.preventDefault()} - onInteractOutside={(e) => e.preventDefault()} - > + Edit row diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/loading-rows.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/table-body-states.tsx similarity index 55% rename from apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/loading-rows.tsx rename to apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/table-body-states.tsx index a6446fc7c..c05bd7a78 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/loading-rows.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/table-data-viewer/components/table-body-states.tsx @@ -1,10 +1,11 @@ /** - * Loading skeleton for table rows. + * Table body placeholder states (loading and empty). * - * @module tables/[tableId]/table-data-viewer/components/loading-rows + * @module tables/[tableId]/table-data-viewer/components/table-body-states */ -import { TableCell, TableRow } from '@/components/emcn' +import { Plus } from 'lucide-react' +import { Button, TableCell, TableRow } from '@/components/emcn' import { Skeleton } from '@/components/ui/skeleton' import type { ColumnDefinition } from '@/lib/table' @@ -14,9 +15,6 @@ interface LoadingRowsProps { /** * Renders skeleton rows while table data is loading. - * - * @param props - Component props - * @returns Loading skeleton rows */ export function LoadingRows({ columns }: LoadingRowsProps) { return ( @@ -48,14 +46,37 @@ export function LoadingRows({ columns }: LoadingRowsProps) { ) })} - -
- - -
-
))} ) } + +interface EmptyRowsProps { + columnCount: number + hasFilter: boolean + onAddRow: () => void +} + +/** + * Renders an empty state when no rows are present. + */ +export function EmptyRows({ columnCount, hasFilter, onAddRow }: EmptyRowsProps) { + return ( + + +
+ + {hasFilter ? 'No rows match your filter' : 'No data'} + + {!hasFilter && ( + + )} +
+
+
+ ) +} 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 25797cc6a..a34f50377 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 @@ -38,7 +38,6 @@ export function useTableData({ queryOptions, currentPage, }: UseTableDataParams): UseTableDataReturn { - // Fetch table metadata const { data: tableData, isLoading: isLoadingTable } = useQuery({ queryKey: ['table', tableId], queryFn: async () => { @@ -50,7 +49,6 @@ export function useTableData({ }, }) - // Fetch table rows with filter and sort const { data: rowsData, isLoading: isLoadingRows, @@ -69,7 +67,6 @@ export function useTableData({ } if (queryOptions.sort) { - // Convert from {column, direction} to {column: direction} format expected by API const sortParam = { [queryOptions.sort.column]: queryOptions.sort.direction } searchParams.set('sort', JSON.stringify(sortParam)) } 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 8cb00ebe5..9294a5ca3 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 @@ -7,11 +7,9 @@ */ import { useCallback, useState } from 'react' -import { Edit, Trash2 } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' import { Badge, - Button, Checkbox, Table, TableBody, @@ -19,7 +17,6 @@ import { TableHead, TableHeader, TableRow, - Tooltip, } from '@/components/emcn' import { cn } from '@/lib/core/utils/cn' import { @@ -65,24 +62,20 @@ export function TableDataViewer() { const workspaceId = params.workspaceId as string const tableId = params.tableId as string - // Query state const [queryOptions, setQueryOptions] = useState({ filter: null, sort: null, }) const [currentPage, setCurrentPage] = useState(0) - // Modal state const [showAddModal, setShowAddModal] = useState(false) const [editingRow, setEditingRow] = useState(null) const [deletingRows, setDeletingRows] = useState([]) const [showSchemaModal, setShowSchemaModal] = useState(false) - // Cell viewer state const [cellViewer, setCellViewer] = useState(null) const [copied, setCopied] = useState(false) - // Fetch table data const { tableData, isLoadingTable, rows, totalCount, totalPages, isLoadingRows, refetchRows } = useTableData({ workspaceId, @@ -91,13 +84,35 @@ export function TableDataViewer() { currentPage, }) - // Row selection const { selectedRows, handleSelectAll, handleSelectRow, clearSelection } = useRowSelection(rows) - // Context menu const { contextMenu, handleRowContextMenu, closeContextMenu } = useContextMenu() const columns = tableData?.schema?.columns || [] + const selectedCount = selectedRows.size + const hasSelection = selectedCount > 0 + const isAllSelected = rows.length > 0 && selectedCount === rows.length + + /** + * Navigates back to the tables list. + */ + const handleNavigateBack = useCallback(() => { + router.push(`/workspace/${workspaceId}/tables`) + }, [router, workspaceId]) + + /** + * Opens the schema viewer modal. + */ + const handleShowSchema = useCallback(() => { + setShowSchemaModal(true) + }, []) + + /** + * Opens the add row modal. + */ + const handleAddRow = useCallback(() => { + setShowAddModal(true) + }, []) /** * Applies new query options and resets pagination. @@ -181,50 +196,40 @@ export function TableDataViewer() { return (
- {/* Header */} router.push(`/workspace/${workspaceId}/tables`)} - onShowSchema={() => setShowSchemaModal(true)} + onNavigateBack={handleNavigateBack} + onShowSchema={handleShowSchema} onRefresh={refetchRows} /> - {/* Filter Bar */}
setShowAddModal(true)} + onAddRow={handleAddRow} /> - {selectedRows.size > 0 && ( - - {selectedRows.size} selected - + {hasSelection && ( + {selectedCount} selected )}
- {/* Action Bar */} - {selectedRows.size > 0 && ( + {hasSelection && ( )} - {/* Table */}
- 0} - onCheckedChange={handleSelectAll} - /> + {columns.map((column) => ( @@ -239,7 +244,6 @@ export function TableDataViewer() { ))} - Actions @@ -249,7 +253,7 @@ export function TableDataViewer() { setShowAddModal(true)} + onAddRow={handleAddRow} /> ) : ( rows.map((row) => ( @@ -279,31 +283,6 @@ export function TableDataViewer() { ))} - -
- - - - - Edit - - - - - - Delete - -
-
)) )} @@ -311,7 +290,6 @@ export function TableDataViewer() {
- {/* Pagination */} setCurrentPage((p) => Math.min(totalPages - 1, p + 1))} /> - {/* Modals */} setShowAddModal(false)} @@ -358,14 +335,12 @@ export function TableDataViewer() { /> )} - {/* Schema Viewer Modal */} setShowSchemaModal(false)} columns={columns} /> - {/* Cell Viewer Modal */} setCellViewer(null)} @@ -373,7 +348,6 @@ export function TableDataViewer() { copied={copied} /> - {/* Row Context Menu */}