From 96a3fe59ff75d45fd441b451859be557a1523f51 Mon Sep 17 00:00:00 2001 From: Lakee Sivaraya Date: Wed, 14 Jan 2026 18:49:31 -0800 Subject: [PATCH] updates --- .../filter-format/components/empty-state.tsx | 19 ++ .../components/filter-condition-row.tsx | 137 ++++++++++ .../filter-format/filter-format.tsx | 237 +++--------------- .../sort-format/components/empty-state.tsx | 19 ++ .../components/sort-condition-row.tsx | 67 +++++ .../components/sort-format/sort-format.tsx | 177 +++---------- apps/sim/lib/table/hooks/index.ts | 2 + .../lib/table/hooks/use-builder-json-sync.ts | 66 +++++ apps/sim/lib/table/hooks/use-table-columns.ts | 60 +++++ apps/sim/lib/table/index.ts | 1 + 10 files changed, 440 insertions(+), 345 deletions(-) create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-format/components/empty-state.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-format/components/filter-condition-row.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-format/components/empty-state.tsx create mode 100644 apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-format/components/sort-condition-row.tsx create mode 100644 apps/sim/lib/table/hooks/index.ts create mode 100644 apps/sim/lib/table/hooks/use-builder-json-sync.ts create mode 100644 apps/sim/lib/table/hooks/use-table-columns.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-format/components/empty-state.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-format/components/empty-state.tsx new file mode 100644 index 000000000..5c229ce5b --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-format/components/empty-state.tsx @@ -0,0 +1,19 @@ +import { Plus } from 'lucide-react' +import { Button } from '@/components/emcn' + +interface EmptyStateProps { + onAdd: () => void + disabled: boolean + label: string +} + +export function EmptyState({ onAdd, disabled, label }: EmptyStateProps) { + return ( +
+ +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-format/components/filter-condition-row.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-format/components/filter-condition-row.tsx new file mode 100644 index 000000000..e25728df2 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-format/components/filter-condition-row.tsx @@ -0,0 +1,137 @@ +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 { 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 { + blockId: string + subBlockId: string + condition: FilterCondition + index: number + columns: ComboboxOption[] + comparisonOptions: ComboboxOption[] + logicalOptions: ComboboxOption[] + isReadOnly: boolean + isPreview: boolean + disabled: boolean + onRemove: (id: string) => void + onUpdate: (id: string, field: keyof FilterCondition, value: string) => void +} + +export function FilterConditionRow({ + blockId, + subBlockId, + condition, + index, + columns, + comparisonOptions, + logicalOptions, + isReadOnly, + isPreview, + disabled, + onRemove, + onUpdate, +}: FilterConditionRowProps) { + const accessiblePrefixes = useAccessibleReferencePrefixes(blockId) + + return ( +
+ + +
+ {index === 0 ? ( + + ) : ( + onUpdate(condition.id, 'logicalOperator', v as 'and' | 'or')} + disabled={isReadOnly} + /> + )} +
+ +
+ onUpdate(condition.id, 'column', v)} + placeholder='Column' + disabled={isReadOnly} + /> +
+ +
+ onUpdate(condition.id, 'operator', v)} + disabled={isReadOnly} + /> +
+ +
+ onUpdate(condition.id, 'value', newValue)} + isPreview={isPreview} + disabled={disabled} + > + {({ ref, value: ctrlValue, onChange, onKeyDown, onDrop, onDragOver }) => { + const formattedText = formatDisplayText(ctrlValue, { + accessiblePrefixes, + highlightAll: !accessiblePrefixes, + }) + + return ( +
+ } + className='h-[28px] w-full overflow-auto text-[12px] text-transparent caret-foreground [-ms-overflow-style:none] [scrollbar-width:none] placeholder:text-muted-foreground/50 [&::-webkit-scrollbar]:hidden' + value={ctrlValue} + onChange={onChange as (e: React.ChangeEvent) => void} + onKeyDown={onKeyDown as (e: React.KeyboardEvent) => void} + onDrop={onDrop as (e: React.DragEvent) => void} + onDragOver={onDragOver as (e: React.DragEvent) => void} + placeholder='Value' + disabled={isReadOnly} + autoComplete='off' + /> +
+
{formattedText}
+
+
+ ) + }} +
+
+
+ ) +} 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 6fff0803e..f540db709 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 @@ -1,16 +1,15 @@ 'use client' -import { useEffect, useMemo, useRef, useState } from 'react' -import { Plus, X } from 'lucide-react' -import { Button, Combobox, type ComboboxOption, Input } from '@/components/emcn' -import { cn } from '@/lib/core/utils/cn' +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 { 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 { useBuilderJsonSync, 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 { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' +import { EmptyState } from './components/empty-state' +import { FilterConditionRow } from './components/filter-condition-row' interface FilterFormatProps { blockId: string @@ -20,19 +19,15 @@ interface FilterFormatProps { disabled?: boolean columns?: Array<{ value: string; label: string }> tableIdSubBlockId?: string - /** SubBlock ID for the mode dropdown (e.g., 'filterMode') - enables builder ↔ JSON sync */ modeSubBlockId?: string - /** SubBlock ID for the JSON filter field (e.g., 'filter') - target for JSON output */ jsonSubBlockId?: string } /** * Visual builder for filter conditions with optional JSON sync. * - * When `modeSubBlockId` and `jsonSubBlockId` are provided, this component handles - * bidirectional conversion between builder conditions and JSON format: - * - Builder → JSON: Conditions sync to JSON when modified in builder mode - * - JSON → Builder: JSON parses to conditions when switching to builder mode + * When `modeSubBlockId` and `jsonSubBlockId` are provided, handles bidirectional + * conversion between builder conditions and JSON format. */ export function FilterFormat({ blockId, @@ -47,86 +42,13 @@ export function FilterFormat({ }: FilterFormatProps) { const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) const [tableIdValue] = useSubBlockValue(blockId, tableIdSubBlockId) - const [dynamicColumns, setDynamicColumns] = useState([]) - const fetchedTableIdRef = useRef(null) - - // Mode sync state - only used when modeSubBlockId and jsonSubBlockId are provided const [modeValue] = useSubBlockValue(blockId, modeSubBlockId || '_unused_mode') const [jsonValue, setJsonValue] = useSubBlockValue( blockId, jsonSubBlockId || '_unused_json' ) - const prevModeRef = useRef(null) - const isSyncingRef = useRef(false) - - const accessiblePrefixes = useAccessibleReferencePrefixes(blockId) - - /** - * Syncs JSON → Builder when mode switches to 'builder'. - * Uses refs to prevent sync loops and only triggers on actual mode transitions. - */ - useEffect(() => { - if (!modeSubBlockId || !jsonSubBlockId || isPreview) return - - const switchingToBuilder = - prevModeRef.current !== null && prevModeRef.current !== 'builder' && modeValue === 'builder' - - if (switchingToBuilder && jsonValue?.trim()) { - isSyncingRef.current = true - const conditions = jsonStringToConditions(jsonValue) - if (conditions.length > 0) { - setStoreValue(conditions) - } - isSyncingRef.current = false - } - - prevModeRef.current = modeValue - }, [modeValue, jsonValue, modeSubBlockId, jsonSubBlockId, setStoreValue, isPreview]) - - /** - * Syncs Builder → JSON when conditions change while in builder mode. - * Skips sync when isSyncingRef is true to prevent loops. - */ - useEffect(() => { - if (!modeSubBlockId || !jsonSubBlockId || isPreview || isSyncingRef.current) return - if (modeValue !== 'builder') return - - const conditions = Array.isArray(storeValue) ? storeValue : [] - if (conditions.length > 0) { - const newJson = conditionsToJsonString(conditions) - if (newJson !== jsonValue) { - setJsonValue(newJson) - } - } - }, [storeValue, modeValue, modeSubBlockId, jsonSubBlockId, jsonValue, setJsonValue, isPreview]) - - /** Fetches table schema columns when tableId changes */ - useEffect(() => { - const fetchColumns = async () => { - if (!tableIdValue || tableIdValue === fetchedTableIdRef.current) return - - try { - const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store') - const workspaceId = useWorkflowRegistry.getState().hydration.workspaceId - if (!workspaceId) return - - const response = await fetch(`/api/table/${tableIdValue}?workspaceId=${workspaceId}`) - if (!response.ok) return - - const result = await response.json() - const cols = result.data?.table?.schema?.columns || result.table?.schema?.columns || [] - setDynamicColumns( - cols.map((col: { name: string }) => ({ value: col.name, label: col.name })) - ) - fetchedTableIdRef.current = tableIdValue - } catch { - // Silently fail - columns will be empty - } - } - - fetchColumns() - }, [tableIdValue]) + const dynamicColumns = useTableColumns({ tableId: tableIdValue }) const columns = useMemo(() => { if (propColumns && propColumns.length > 0) return propColumns return dynamicColumns @@ -136,7 +58,18 @@ export function FilterFormat({ const conditions: FilterCondition[] = Array.isArray(value) && value.length > 0 ? value : [] const isReadOnly = isPreview || disabled - // Use the shared filter builder hook for condition management + useBuilderJsonSync({ + modeValue, + jsonValue, + setJsonValue, + isPreview, + conditions, + setConditions: setStoreValue, + jsonToConditions: jsonStringToConditions, + conditionsToJson: conditionsToJsonString, + enabled: Boolean(modeSubBlockId && jsonSubBlockId), + }) + const { comparisonOptions, logicalOptions, addCondition, removeCondition, updateCondition } = useFilterBuilder({ columns, @@ -148,122 +81,26 @@ export function FilterFormat({ return (
{conditions.length === 0 ? ( -
- -
+ ) : ( <> {conditions.map((condition, index) => ( -
- {/* Remove Button */} - - - {/* Logical Operator */} -
- {index === 0 ? ( - - ) : ( - - updateCondition(condition.id, 'logicalOperator', v as 'and' | 'or') - } - disabled={isReadOnly} - /> - )} -
- - {/* Column Selector */} -
- updateCondition(condition.id, 'column', v)} - placeholder='Column' - disabled={isReadOnly} - /> -
- - {/* Comparison Operator */} -
- updateCondition(condition.id, 'operator', v)} - disabled={isReadOnly} - /> -
- - {/* Value Input with Tag Dropdown */} -
- updateCondition(condition.id, 'value', newValue)} - isPreview={isPreview} - disabled={disabled} - > - {({ ref, value: ctrlValue, onChange, onKeyDown, onDrop, onDragOver }) => { - const formattedText = formatDisplayText(ctrlValue, { - accessiblePrefixes, - highlightAll: !accessiblePrefixes, - }) - - return ( -
- } - className='h-[28px] w-full overflow-auto text-[12px] text-transparent caret-foreground [-ms-overflow-style:none] [scrollbar-width:none] placeholder:text-muted-foreground/50 [&::-webkit-scrollbar]:hidden' - value={ctrlValue} - onChange={onChange as (e: React.ChangeEvent) => void} - onKeyDown={ - onKeyDown as (e: React.KeyboardEvent) => void - } - onDrop={onDrop as (e: React.DragEvent) => void} - onDragOver={onDragOver as (e: React.DragEvent) => void} - placeholder='Value' - disabled={isReadOnly} - autoComplete='off' - /> -
-
{formattedText}
-
-
- ) - }} -
-
-
+ ))} - - {/* Add Button */} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-format/components/sort-condition-row.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-format/components/sort-condition-row.tsx new file mode 100644 index 000000000..1b4447e02 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/sort-format/components/sort-condition-row.tsx @@ -0,0 +1,67 @@ +import { X } from 'lucide-react' +import { Button, Combobox, type ComboboxOption } from '@/components/emcn' +import type { SortCondition } from '@/lib/table/filters/constants' + +interface SortConditionRowProps { + condition: SortCondition + index: number + columns: ComboboxOption[] + directionOptions: ComboboxOption[] + isReadOnly: boolean + onRemove: (id: string) => void + onUpdate: (id: string, field: keyof SortCondition, value: string) => void +} + +export function SortConditionRow({ + condition, + index, + columns, + directionOptions, + isReadOnly, + onRemove, + onUpdate, +}: SortConditionRowProps) { + return ( +
+ + +
+ +
+ +
+ onUpdate(condition.id, 'column', v)} + placeholder='Column' + disabled={isReadOnly} + /> +
+ +
+ onUpdate(condition.id, 'direction', v as 'asc' | 'desc')} + disabled={isReadOnly} + /> +
+
+ ) +} 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 937e20617..a3598568c 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 @@ -1,15 +1,18 @@ 'use client' -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { Plus, X } from 'lucide-react' +import { useCallback, useMemo } from 'react' +import { Plus } from 'lucide-react' import { nanoid } from 'nanoid' -import { Button, Combobox, type ComboboxOption } from '@/components/emcn' +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 { 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' interface SortFormatProps { blockId: string @@ -19,15 +22,10 @@ interface SortFormatProps { disabled?: boolean columns?: Array<{ value: string; label: string }> tableIdSubBlockId?: string - /** SubBlock ID for the mode dropdown (e.g., 'sortMode') - enables builder ↔ JSON sync */ modeSubBlockId?: string - /** SubBlock ID for the JSON sort field (e.g., 'sort') - target for JSON output */ jsonSubBlockId?: string } -/** - * Creates a new sort condition with default values - */ const createDefaultCondition = (columns: ComboboxOption[]): SortCondition => ({ id: nanoid(), column: columns[0]?.value || '', @@ -37,10 +35,8 @@ const createDefaultCondition = (columns: ComboboxOption[]): SortCondition => ({ /** * Visual builder for sort conditions with optional JSON sync. * - * When `modeSubBlockId` and `jsonSubBlockId` are provided, this component handles - * bidirectional conversion between builder conditions and JSON format: - * - Builder → JSON: Conditions sync to JSON when modified in builder mode - * - JSON → Builder: JSON parses to conditions when switching to builder mode + * When `modeSubBlockId` and `jsonSubBlockId` are provided, handles bidirectional + * conversion between builder conditions and JSON format. */ export function SortFormat({ blockId, @@ -55,90 +51,13 @@ export function SortFormat({ }: SortFormatProps) { const [storeValue, setStoreValue] = useSubBlockValue(blockId, subBlockId) const [tableIdValue] = useSubBlockValue(blockId, tableIdSubBlockId) - const [dynamicColumns, setDynamicColumns] = useState([]) - const fetchedTableIdRef = useRef(null) - - // Mode sync state - only used when modeSubBlockId and jsonSubBlockId are provided const [modeValue] = useSubBlockValue(blockId, modeSubBlockId || '_unused_mode') const [jsonValue, setJsonValue] = useSubBlockValue( blockId, jsonSubBlockId || '_unused_json' ) - const prevModeRef = useRef(null) - const isSyncingRef = useRef(false) - - /** - * Syncs JSON → Builder when mode switches to 'builder'. - * Uses refs to prevent sync loops and only triggers on actual mode transitions. - */ - useEffect(() => { - if (!modeSubBlockId || !jsonSubBlockId || isPreview) return - - const switchingToBuilder = - prevModeRef.current !== null && prevModeRef.current !== 'builder' && modeValue === 'builder' - - if (switchingToBuilder && jsonValue?.trim()) { - isSyncingRef.current = true - const conditions = jsonStringToSortConditions(jsonValue) - if (conditions.length > 0) { - setStoreValue(conditions) - } - isSyncingRef.current = false - } - - prevModeRef.current = modeValue - }, [modeValue, jsonValue, modeSubBlockId, jsonSubBlockId, setStoreValue, isPreview]) - - /** - * Syncs Builder → JSON when conditions change while in builder mode. - * Skips sync when isSyncingRef is true to prevent loops. - */ - useEffect(() => { - if (!modeSubBlockId || !jsonSubBlockId || isPreview || isSyncingRef.current) return - if (modeValue !== 'builder') return - - const conditions = Array.isArray(storeValue) ? storeValue : [] - if (conditions.length > 0) { - const newJson = sortConditionsToJsonString(conditions) - if (newJson !== jsonValue) { - setJsonValue(newJson) - } - } - }, [storeValue, modeValue, modeSubBlockId, jsonSubBlockId, jsonValue, setJsonValue, isPreview]) - - /** Fetches table schema columns when tableId changes */ - useEffect(() => { - const fetchColumns = async () => { - if (!tableIdValue || tableIdValue === fetchedTableIdRef.current) return - - try { - const { useWorkflowRegistry } = await import('@/stores/workflows/registry/store') - const workspaceId = useWorkflowRegistry.getState().hydration.workspaceId - if (!workspaceId) return - - const response = await fetch(`/api/table/${tableIdValue}?workspaceId=${workspaceId}`) - if (!response.ok) return - - const result = await response.json() - const cols = result.data?.table?.schema?.columns || result.table?.schema?.columns || [] - const builtInCols = [ - { value: 'createdAt', label: 'createdAt' }, - { value: 'updatedAt', label: 'updatedAt' }, - ] - const schemaCols = cols.map((col: { name: string }) => ({ - value: col.name, - label: col.name, - })) - setDynamicColumns([...schemaCols, ...builtInCols]) - fetchedTableIdRef.current = tableIdValue - } catch { - // Silently fail - columns will be empty - } - } - - fetchColumns() - }, [tableIdValue]) + const dynamicColumns = useTableColumns({ tableId: tableIdValue, includeBuiltIn: true }) const columns = useMemo(() => { if (propColumns && propColumns.length > 0) return propColumns return dynamicColumns @@ -153,6 +72,18 @@ 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)]) @@ -177,65 +108,21 @@ export function SortFormat({ return (
{conditions.length === 0 ? ( -
- -
+ ) : ( <> {conditions.map((condition, index) => ( -
- {/* Remove Button */} - - - {/* Order indicator */} -
- -
- - {/* Column Selector */} -
- updateCondition(condition.id, 'column', v)} - placeholder='Column' - disabled={isReadOnly} - /> -
- - {/* Direction Selector */} -
- updateCondition(condition.id, 'direction', v as 'asc' | 'desc')} - disabled={isReadOnly} - /> -
-
+ ))} - - {/* Add Button */}