mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
comments
This commit is contained in:
@@ -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]' />
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
@@ -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'
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
/**
|
||||
* Filter utilities for table queries.
|
||||
*/
|
||||
|
||||
export * from './constants'
|
||||
export * from './use-builder'
|
||||
export * from './utils'
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user