Skip to content

Commit 57f00e2

Browse files
fix(table): fix table boolean, add dynamic row number col size, search & replace imporvements (#4515)
* fix(table): fix table boolean, add dynamic row number col size, fix style for search and replace * sidebar styling * refactor(search-replace): extract duplicate WORKFLOW_SEARCH_HIGHLIGHT_CLASS to shared constants * fix(tables): add missing onError toast to useAddTableColumn * fix(tables): add consistent onError toast handlers across all table mutations * subblock sweep * more resources supported * code restructuring * better organization * canonical modes search replace counterpart * more cases * tool inp edge case * more subblock cases * trigger canoncial index ops consolidation * minor codec fix * fix(tables): prevent double toast in workflow-sidebar and csv upload mutations * resolver fix for quoted js literals * code scan context * triple quotes case * escaped behaviour --------- Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
1 parent eb871fc commit 57f00e2

53 files changed

Lines changed: 6535 additions & 547 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-config-sidebar/column-config-sidebar.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,6 @@ function ColumnConfigBody({
135135
toast.success(`Saved "${trimmedName}"`)
136136
onClose()
137137
} catch (err) {
138-
// Server validation errors carry a Zod issue array on the body; surface
139-
// them inline next to the offending field instead of as a raw toast.
140138
if (isValidationError(err)) {
141139
const nameIssue =
142140
findValidationIssue(err, ['updates', 'name']) ??
@@ -146,14 +144,14 @@ function ColumnConfigBody({
146144
setNameError(nameIssue.message)
147145
return
148146
}
147+
toast.error(toError(err).message)
149148
}
150-
toast.error(toError(err).message)
151149
}
152150
}
153151

154152
return (
155153
<div className='flex h-full flex-col'>
156-
<div className='flex items-center justify-between border-[var(--border)] border-b px-3 py-2'>
154+
<div className='flex items-center justify-between border-[var(--border)] border-b px-3 py-[8.5px]'>
157155
<h2 className='font-medium text-[var(--text-primary)] text-small'>Configure column</h2>
158156
<Button
159157
variant='ghost'

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,11 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle
208208
case 'boolean':
209209
return (
210210
<div
211-
className={cn('flex min-h-[20px] items-center justify-center', isEditing && 'invisible')}
211+
data-boolean-cell-toggle
212+
className={cn(
213+
'flex min-h-[20px] w-full items-center justify-center',
214+
isEditing && 'invisible'
215+
)}
212216
>
213217
<Checkbox size='sm' checked={kind.checked} className='pointer-events-none' />
214218
</div>

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx

Lines changed: 41 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -92,25 +92,18 @@ function rowSelectionCoversAll(sel: RowSelection, rows: TableRowType[]): boolean
9292

9393
const COL_WIDTH_MIN = 80
9494
const COL_WIDTH_AUTO_FIT_MAX = 1000
95-
// Wide enough to host the row-number + per-row run button side by side.
96-
// Single-digit row numbers (rows 1–9) and multi-digit need to render with
97-
// the play button at the same x-position so the column doesn't reflow
98-
// row-by-row.
99-
//
100-
// Bucketed by the table's plan-derived `maxRows`, not the live count: a small
101-
// table sized for ≤9,999 always renders the narrow gutter; an enterprise
102-
// table sized up to 9,999,999 always renders the wide one. The gutter never
103-
// changes width as rows are added.
104-
//
105-
// Tables without workflow columns drop the per-row run button (~28px), so
106-
// the gutter shrinks accordingly.
107-
const CHECKBOX_COL_WIDTH_SMALL_WITH_RUN = 48
108-
const CHECKBOX_COL_WIDTH_SMALL_NUMBER_ONLY = 32
109-
const CHECKBOX_COL_WIDTH_LARGE_WITH_RUN = 68
110-
const CHECKBOX_COL_WIDTH_LARGE_NUMBER_ONLY = 52
111-
/** Bucket boundary: tables sized for >9,999 rows get the wide gutter. */
112-
const LARGE_ROW_NUMBER_THRESHOLD = 10000
11395
const ADD_COL_WIDTH = 120
96+
97+
/** Returns sticky row-number column dimensions sized to the digit count of `maxRows`. */
98+
function checkboxColLayout(
99+
maxRows: number,
100+
hasWorkflowCols: boolean
101+
): { colWidth: number; numDivWidth: number } {
102+
const digits = maxRows > 0 ? Math.floor(Math.log10(maxRows)) + 1 : 1
103+
const numDivWidth = Math.max(20, digits * 8 + 4)
104+
const colWidth = Math.max(32, numDivWidth + 8) + (hasWorkflowCols ? 16 : 0)
105+
return { colWidth, numDivWidth }
106+
}
114107
const SKELETON_COL_COUNT = 4
115108
const SKELETON_ROW_COUNT = 10
116109
const ROW_HEIGHT_ESTIMATE = 35
@@ -128,7 +121,7 @@ const CELL_HEADER_CHECKBOX =
128121
const CELL_CONTENT =
129122
'relative flex h-[22px] min-w-0 items-center overflow-clip text-ellipsis whitespace-nowrap text-small'
130123
const SELECTION_OVERLAY =
131-
'pointer-events-none absolute -top-px -right-px -bottom-px -left-px z-[5] border-[2px] border-[var(--selection)]'
124+
'pointer-events-none absolute -top-px -right-px -bottom-px z-[5] border-[2px] border-[var(--selection)]'
132125

133126
/**
134127
* Snapshot of grid selection state the wrapper needs to render `<TableActionBar>`.
@@ -452,22 +445,10 @@ export function TableGrid({
452445
}, [columns, columnOrder, tableWorkflowGroups])
453446

454447
const hasWorkflowColumns = columns.some((c) => !!c.workflowGroupId)
455-
/**
456-
* The sticky left column hosts the row number / checkbox always, plus a
457-
* per-row run button only when the table has workflow columns. Width is
458-
* picked from the table's plan-derived `maxRows` so a free-tier table
459-
* (≤9,999) gets the narrow gutter and an enterprise table (up to
460-
* 9,999,999) gets the wide one. Bucketed, not continuous, so the gutter
461-
* never reflows as rows are added.
462-
*/
463-
const isLargeRowCountTable = (tableData?.maxRows ?? 0) >= LARGE_ROW_NUMBER_THRESHOLD
464-
const checkboxColWidth = isLargeRowCountTable
465-
? hasWorkflowColumns
466-
? CHECKBOX_COL_WIDTH_LARGE_WITH_RUN
467-
: CHECKBOX_COL_WIDTH_LARGE_NUMBER_ONLY
468-
: hasWorkflowColumns
469-
? CHECKBOX_COL_WIDTH_SMALL_WITH_RUN
470-
: CHECKBOX_COL_WIDTH_SMALL_NUMBER_ONLY
448+
const { colWidth: checkboxColWidth, numDivWidth } = checkboxColLayout(
449+
tableData?.maxRows ?? 0,
450+
hasWorkflowColumns
451+
)
471452

472453
const headerGroups = useMemo(
473454
() => buildHeaderGroups(displayColumns, tableWorkflowGroups),
@@ -3053,7 +3034,7 @@ export function TableGrid({
30533034
onRowToggle={handleRowToggle}
30543035
runningCount={runningByRowId.get(row.id) ?? 0}
30553036
hasWorkflowColumns={hasWorkflowColumns}
3056-
isLargeRowCountTable={isLargeRowCountTable}
3037+
numDivWidth={numDivWidth}
30573038
onStopRow={onStopRow}
30583039
onRunRow={handleRunRow}
30593040
workflowGroups={tableWorkflowGroups}
@@ -3178,8 +3159,8 @@ interface DataRowProps {
31783159
runningCount: number
31793160
/** Whether the table has at least one workflow column — controls whether a run/stop icon is rendered. */
31803161
hasWorkflowColumns: boolean
3181-
/** True for tables sized for >9,999 rows; widens the row-number slot to fit 5–7 digit numbers. */
3182-
isLargeRowCountTable: boolean
3162+
/** Width of the row-number inner div in px, derived from the table's maxRows digit count. */
3163+
numDivWidth: number
31833164
onStopRow: (rowId: string) => void
31843165
onRunRow: (rowId: string) => void
31853166
/**
@@ -3239,7 +3220,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean {
32393220
prev.onRowToggle !== next.onRowToggle ||
32403221
prev.runningCount !== next.runningCount ||
32413222
prev.hasWorkflowColumns !== next.hasWorkflowColumns ||
3242-
prev.isLargeRowCountTable !== next.isLargeRowCountTable ||
3223+
prev.numDivWidth !== next.numDivWidth ||
32433224
prev.onStopRow !== next.onStopRow ||
32443225
prev.onRunRow !== next.onRunRow ||
32453226
prev.workflowGroups !== next.workflowGroups
@@ -3281,7 +3262,7 @@ const DataRow = React.memo(function DataRow({
32813262
onRowToggle,
32823263
runningCount,
32833264
hasWorkflowColumns,
3284-
isLargeRowCountTable,
3265+
numDivWidth,
32853266
onStopRow,
32863267
onRunRow,
32873268
workflowGroups,
@@ -3313,20 +3294,23 @@ const DataRow = React.memo(function DataRow({
33133294
return (
33143295
<tr onContextMenu={(e) => onContextMenu(e, row)}>
33153296
<td className={cn(CELL_CHECKBOX, 'cursor-pointer')}>
3316-
<div className='flex items-center justify-between gap-1'>
3297+
<div
3298+
className={cn(
3299+
'flex items-center gap-1',
3300+
hasWorkflowColumns ? 'justify-between' : 'justify-center'
3301+
)}
3302+
>
33173303
<div
3318-
className={cn(
3319-
'group/checkbox flex h-[20px] shrink-0 items-center justify-end',
3320-
isLargeRowCountTable ? 'w-[40px]' : 'w-[20px]'
3321-
)}
3304+
className='group/checkbox flex h-[20px] shrink-0 items-center justify-center'
3305+
style={{ width: numDivWidth }}
33223306
onMouseDown={(e) => {
33233307
if (e.button !== 0) return
33243308
onRowToggle(rowIndex, e.shiftKey)
33253309
}}
33263310
>
33273311
<span
33283312
className={cn(
3329-
'text-right text-[var(--text-tertiary)] text-xs tabular-nums',
3313+
'text-center text-[var(--text-tertiary)] text-xs tabular-nums',
33303314
isRowSelected ? 'hidden' : 'block group-hover/checkbox:hidden'
33313315
)}
33323316
>
@@ -3409,7 +3393,8 @@ const DataRow = React.memo(function DataRow({
34093393
{isHighlighted && (isMultiCell || isRowChecked) && (
34103394
<div
34113395
className={cn(
3412-
'-top-px -right-px -bottom-px -left-px pointer-events-none absolute z-[4]',
3396+
'-top-px -right-px -bottom-px pointer-events-none absolute z-[4]',
3397+
colIndex === 0 ? 'left-0' : '-left-px',
34133398
SELECTION_TINT_BG,
34143399
isFirstRow && isTopEdge && 'top-0',
34153400
isTopEdge && 'border-t border-t-[var(--selection)]',
@@ -3419,7 +3404,15 @@ const DataRow = React.memo(function DataRow({
34193404
)}
34203405
/>
34213406
)}
3422-
{isAnchor && <div className={cn(SELECTION_OVERLAY, isFirstRow && 'top-0')} />}
3407+
{isAnchor && (
3408+
<div
3409+
className={cn(
3410+
SELECTION_OVERLAY,
3411+
colIndex === 0 ? 'left-0' : '-left-px',
3412+
isFirstRow && 'top-0'
3413+
)}
3414+
/>
3415+
)}
34233416
<div className={CELL_CONTENT}>
34243417
<CellContent
34253418
value={

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/workflow-sidebar/workflow-sidebar.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -659,8 +659,8 @@ function WorkflowSidebarBody({
659659
setNameError(nameIssue.message)
660660
return
661661
}
662+
toast.error(toError(err).message)
662663
}
663-
toast.error(toError(err).message)
664664
}
665665
}
666666

@@ -678,7 +678,7 @@ function WorkflowSidebarBody({
678678

679679
return (
680680
<div className='flex h-full flex-col'>
681-
<div className='flex items-center justify-between border-[var(--border)] border-b px-3 py-2'>
681+
<div className='flex items-center justify-between border-[var(--border)] border-b px-3 py-[8.5px]'>
682682
<h2 className='font-medium text-[var(--text-primary)] text-small'>
683683
{titleByMode[config.mode]}
684684
</h2>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export const WORKFLOW_SEARCH_HIGHLIGHT_CLASS =
2+
'rounded-sm bg-orange-400 shadow-[3px_0_0_#fb923c,-3px_0_0_#fb923c]'

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/eval-input/eval-input.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ import { Button, Input, Textarea, Tooltip } from '@/components/emcn'
55
import { Trash } from '@/components/emcn/icons/trash'
66
import { Label } from '@/components/ui/label'
77
import { cn } from '@/lib/core/utils/cn'
8+
import { WORKFLOW_SEARCH_HIGHLIGHT_CLASS } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/constants'
89
import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text'
910
import { TagDropdown } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
1011
import { useSubBlockInput } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-input'
1112
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
1213
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
14+
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
1315

1416
interface EvalMetric {
1517
id: string
@@ -27,6 +29,7 @@ interface EvalInputProps {
2729
isPreview?: boolean
2830
previewValue?: EvalMetric[] | null
2931
disabled?: boolean
32+
activeSearchTarget?: ActiveSearchTarget | null
3033
}
3134

3235
// Default values
@@ -43,6 +46,7 @@ export function EvalInput({
4346
isPreview = false,
4447
previewValue,
4548
disabled = false,
49+
activeSearchTarget,
4650
}: EvalInputProps) {
4751
const [storeValue, setStoreValue] = useSubBlockValue<EvalMetric[]>(blockId, subBlockId)
4852
const accessiblePrefixes = useAccessibleReferencePrefixes(blockId)
@@ -67,6 +71,17 @@ export function EvalInput({
6771
const defaultMetric = useMemo(() => createDefaultMetric(), [])
6872
const metrics: EvalMetric[] = value || [defaultMetric]
6973

74+
const isNestedSearchHighlighted = (metricIndex: number, metricPath: Array<string | number>) =>
75+
activeSearchTarget?.subBlockId === subBlockId &&
76+
activeSearchTarget.valuePath[0] === metricIndex &&
77+
metricPath.every((segment, index) => activeSearchTarget.valuePath[index + 1] === segment)
78+
79+
const renderFieldLabel = (label: string, highlighted: boolean) => (
80+
<Label className='text-small'>
81+
{highlighted ? <mark className={WORKFLOW_SEARCH_HIGHLIGHT_CLASS}>{label}</mark> : label}
82+
</Label>
83+
)
84+
7085
const addMetric = () => {
7186
if (isPreview || disabled) return
7287

@@ -176,7 +191,7 @@ export function EvalInput({
176191

177192
<div className='flex flex-col gap-2 border-[var(--border-1)] px-2.5 pt-1.5 pb-2.5'>
178193
<div key={`name-${metric.id}`} className='flex flex-col gap-1.5'>
179-
<Label className='text-small'>Name</Label>
194+
{renderFieldLabel('Name', isNestedSearchHighlighted(index, ['name']))}
180195
<Input
181196
name='name'
182197
value={metric.name}
@@ -187,7 +202,7 @@ export function EvalInput({
187202
</div>
188203

189204
<div key={`description-${metric.id}`} className='flex flex-col gap-1.5'>
190-
<Label className='text-small'>Description</Label>
205+
{renderFieldLabel('Description', isNestedSearchHighlighted(index, ['description']))}
191206
<div className='relative'>
192207
{(() => {
193208
const fieldState = inputController.fieldHelpers.getFieldState(metric.id)
@@ -259,7 +274,7 @@ export function EvalInput({
259274

260275
<div key={`range-${metric.id}`} className='grid grid-cols-2 gap-2'>
261276
<div className='flex flex-col gap-1.5'>
262-
<Label className='text-small'>Min Value</Label>
277+
{renderFieldLabel('Min Value', isNestedSearchHighlighted(index, ['range', 'min']))}
263278
<Input
264279
type='text'
265280
value={metric.range.min}
@@ -272,7 +287,7 @@ export function EvalInput({
272287
/>
273288
</div>
274289
<div className='flex flex-col gap-1.5'>
275-
<Label className='text-small'>Max Value</Label>
290+
{renderFieldLabel('Max Value', isNestedSearchHighlighted(index, ['range', 'max']))}
276291
<Input
277292
type='text'
278293
value={metric.range.max}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/response/response-format.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { ResponseFormat as SharedResponseFormat } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format'
2+
import type { ActiveSearchTarget } from '@/stores/panel/editor/store'
23

34
interface ResponseFormatProps {
45
blockId: string
@@ -7,6 +8,7 @@ interface ResponseFormatProps {
78
previewValue?: any
89
disabled?: boolean
910
config?: any
11+
activeSearchTarget?: ActiveSearchTarget | null
1012
}
1113

1214
export function ResponseFormat({
@@ -16,6 +18,7 @@ export function ResponseFormat({
1618
previewValue,
1719
disabled = false,
1820
config,
21+
activeSearchTarget,
1922
}: ResponseFormatProps) {
2023
return (
2124
<SharedResponseFormat
@@ -25,6 +28,7 @@ export function ResponseFormat({
2528
previewValue={previewValue}
2629
disabled={disabled}
2730
config={config}
31+
activeSearchTarget={activeSearchTarget}
2832
/>
2933
)
3034
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/short-input/short-input.tsx

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ interface ShortInputProps {
4646
wandControlRef?: React.MutableRefObject<WandControlHandlers | null>
4747
/** Whether to hide the internal wand button (controlled by parent) */
4848
hideInternalWand?: boolean
49+
/** Whether workflow search is actively highlighting this input */
50+
isSearchHighlighted?: boolean
4951
}
5052

5153
/**
@@ -74,6 +76,7 @@ export const ShortInput = memo(function ShortInput({
7476
useWebhookUrl = false,
7577
wandControlRef,
7678
hideInternalWand = false,
79+
isSearchHighlighted = false,
7780
}: ShortInputProps) {
7881
const [localContent, setLocalContent] = useState<string>('')
7982
const [isFocused, setIsFocused] = useState(false)
@@ -332,16 +335,15 @@ export const ShortInput = memo(function ShortInput({
332335
? webhookManagement.webhookUrl
333336
: ctrlValue
334337

335-
const displayValue =
336-
password && !isFocused ? '•'.repeat(actualValue?.length ?? 0) : actualValue
338+
const shouldMask = password && !isFocused && !isSearchHighlighted
339+
const displayValue = shouldMask ? '•'.repeat(actualValue?.length ?? 0) : actualValue
337340

338-
const formattedText =
339-
password && !isFocused
340-
? '•'.repeat(actualValue?.length ?? 0)
341-
: formatDisplayText(actualValue, {
342-
accessiblePrefixes,
343-
highlightAll: !accessiblePrefixes,
344-
})
341+
const formattedText = shouldMask
342+
? '•'.repeat(actualValue?.length ?? 0)
343+
: formatDisplayText(actualValue, {
344+
accessiblePrefixes,
345+
highlightAll: !accessiblePrefixes,
346+
})
345347

346348
return (
347349
<>

0 commit comments

Comments
 (0)