Skip to content

Commit e2523e0

Browse files
authored
improvement(tables): migrate inputs to emcn chip components and clean up tables feature (#4995)
* improvement(tables): migrate inputs to emcn chip components and clean up tables feature * fix(tables): address review feedback — stale filter column label, shared FieldError in enrichment config * improvement(tables): scope create-table callback to stable mutateAsync
1 parent 2c75a4a commit e2523e0

15 files changed

Lines changed: 291 additions & 280 deletions

File tree

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

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,24 @@
11
'use client'
22

3-
import type React from 'react'
43
import { useState } from 'react'
54
import { toError } from '@sim/utils/errors'
6-
import { X } from 'lucide-react'
7-
import { Button, ChipCombobox, FieldDivider, Input, Label, Switch, toast } from '@/components/emcn'
5+
import {
6+
Button,
7+
ChipCombobox,
8+
ChipInput,
9+
FieldDivider,
10+
Label,
11+
Switch,
12+
toast,
13+
} from '@/components/emcn'
14+
import { X } from '@/components/emcn/icons'
815
import { findValidationIssue, isValidationError } from '@/lib/api/client/errors'
916
import { cn } from '@/lib/core/utils/cn'
1017
import type { ColumnDefinition } from '@/lib/table'
18+
import {
19+
FieldError,
20+
RequiredLabel,
21+
} from '@/app/workspace/[workspaceId]/tables/[tableId]/components/sidebar-fields'
1122
import { useAddTableColumn, useUpdateColumn } from '@/hooks/queries/tables'
1223
import { PLAIN_COLUMN_TYPE_OPTIONS } from './column-types'
1324

@@ -169,7 +180,7 @@ function ColumnConfigBody({
169180
<div className='flex-1 overflow-y-auto overflow-x-hidden px-2 pt-3 pb-2 [overflow-anchor:none]'>
170181
<div className='flex flex-col gap-[9.5px]'>
171182
<RequiredLabel htmlFor='column-sidebar-name'>Column name</RequiredLabel>
172-
<Input
183+
<ChipInput
173184
id='column-sidebar-name'
174185
value={nameInput}
175186
onChange={(e) => {
@@ -178,6 +189,7 @@ function ColumnConfigBody({
178189
}}
179190
spellCheck={false}
180191
autoComplete='off'
192+
error={Boolean((showValidation && !trimmedName) || nameError)}
181193
aria-invalid={(showValidation && !trimmedName) || nameError ? true : undefined}
182194
/>
183195
{showValidation && !trimmedName && <FieldError message='Column name is required' />}
@@ -228,16 +240,3 @@ function ColumnConfigBody({
228240
</div>
229241
)
230242
}
231-
232-
function RequiredLabel({ htmlFor, children }: { htmlFor?: string; children: React.ReactNode }) {
233-
return (
234-
<Label htmlFor={htmlFor} className='flex items-baseline gap-1.5 whitespace-nowrap pl-0.5'>
235-
{children}
236-
<span className='ml-0.5'>*</span>
237-
</Label>
238-
)
239-
}
240-
241-
function FieldError({ message }: { message: string }) {
242-
return <p className='pl-0.5 text-caption text-destructive'>{message}</p>
243-
}

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

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,18 +7,18 @@ import {
77
Badge,
88
Button,
99
ChipCombobox,
10+
ChipInput,
1011
CollapsibleCard,
1112
FieldDivider,
12-
Input,
1313
Label,
1414
Switch,
1515
toast,
1616
} from '@/components/emcn'
1717
import { ArrowLeft, X } from '@/components/emcn/icons'
1818
import type { AddWorkflowGroupBodyInput } from '@/lib/api/contracts/tables'
19-
import { cn } from '@/lib/core/utils/cn'
2019
import type { ColumnDefinition, WorkflowGroup, WorkflowGroupOutput } from '@/lib/table'
2120
import { deriveOutputColumnName } from '@/lib/table/column-naming'
21+
import { FieldError } from '@/app/workspace/[workspaceId]/tables/[tableId]/components/sidebar-fields'
2222
import type { EnrichmentConfig as EnrichmentDef } from '@/enrichments/types'
2323
import {
2424
useAddWorkflowGroup,
@@ -280,12 +280,10 @@ export function EnrichmentConfig({
280280
onChange={(columnName: string) =>
281281
setInputMappings((prev) => ({ ...prev, [input.id]: columnName }))
282282
}
283-
error={
284-
showValidation && input.required && !inputMappings[input.id]
285-
? 'Required'
286-
: null
287-
}
288283
/>
284+
{showValidation && input.required && !inputMappings[input.id] && (
285+
<FieldError message='Required' />
286+
)}
289287
</CollapsibleCard>
290288
))}
291289
</div>
@@ -317,16 +315,16 @@ export function EnrichmentConfig({
317315
}
318316
>
319317
<Label className='text-small'>Column name</Label>
320-
<Input
318+
<ChipInput
321319
value={outputNames[output.id] ?? ''}
322320
onChange={(e) =>
323321
setOutputNames((prev) => ({ ...prev, [output.id]: e.target.value }))
324322
}
325323
spellCheck={false}
326324
autoComplete='off'
327-
className={cn(outErr && 'border-[var(--text-error)]')}
325+
error={Boolean(outErr)}
328326
/>
329-
{outErr && <p className='text-[var(--text-error)] text-caption'>{outErr}</p>}
327+
{outErr && <FieldError message={outErr} />}
330328
</CollapsibleCard>
331329
)
332330
})}

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

Lines changed: 19 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
'use client'
22

33
import { useState } from 'react'
4-
import { Input } from '@/components/emcn'
4+
import { Button, ChipInput } from '@/components/emcn'
55
import { Search, X } from '@/components/emcn/icons'
66
import { cn } from '@/lib/core/utils/cn'
77
import type { ColumnDefinition, WorkflowGroup } from '@/lib/table'
@@ -74,14 +74,15 @@ function EnrichmentsSidebarBody({
7474
<div className='flex h-full flex-col'>
7575
<div className='flex items-center justify-between border-[var(--border)] border-b px-3 py-[8.5px]'>
7676
<h2 className='font-medium text-[var(--text-primary)] text-small'>Enrichment</h2>
77-
<button
78-
type='button'
77+
<Button
78+
variant='ghost'
79+
size='sm'
7980
onClick={onClose}
80-
className='flex size-7 flex-none items-center justify-center rounded-md text-[var(--text-muted)] transition-colors hover-hover:bg-[var(--surface-hover)] hover-hover:text-[var(--text-primary)]'
81+
className='!p-1 size-7 flex-none'
8182
aria-label='Close'
8283
>
8384
<X className='size-[14px]' />
84-
</button>
85+
</Button>
8586
</div>
8687
<div className='flex flex-1 items-center justify-center px-6 text-center'>
8788
<p className='text-[var(--text-tertiary)] text-small'>
@@ -119,28 +120,26 @@ function EnrichmentsSidebarBody({
119120
<div className='flex h-full flex-col'>
120121
<div className='flex items-center justify-between border-[var(--border)] border-b px-3 py-[8.5px]'>
121122
<h2 className='font-medium text-[var(--text-primary)] text-small'>Enrichments</h2>
122-
<button
123-
type='button'
123+
<Button
124+
variant='ghost'
125+
size='sm'
124126
onClick={onClose}
125-
className='flex size-7 flex-none items-center justify-center rounded-md text-[var(--text-muted)] transition-colors hover-hover:bg-[var(--surface-hover)] hover-hover:text-[var(--text-primary)]'
127+
className='!p-1 size-7 flex-none'
126128
aria-label='Close'
127129
>
128130
<X className='size-[14px]' />
129-
</button>
131+
</Button>
130132
</div>
131133

132134
<div className='px-2 pt-3'>
133-
<div className='relative'>
134-
<Search className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-2 size-[14px] text-[var(--text-muted)]' />
135-
<Input
136-
value={query}
137-
onChange={(e) => setQuery(e.target.value)}
138-
placeholder='Search'
139-
spellCheck={false}
140-
autoComplete='off'
141-
className='pl-7'
142-
/>
143-
</div>
135+
<ChipInput
136+
icon={Search}
137+
value={query}
138+
onChange={(e) => setQuery(e.target.value)}
139+
placeholder='Search'
140+
spellCheck={false}
141+
autoComplete='off'
142+
/>
144143
</div>
145144

146145
<div className='flex-1 overflow-y-auto overflow-x-hidden px-2 py-3 [overflow-anchor:none]'>

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export * from './enrichments-sidebar'
44
export * from './new-column-dropdown'
55
export * from './row-modal'
66
export * from './run-status-control'
7+
export * from './sidebar-fields'
78
export * from './table-action-bar'
89
export * from './table-filter'
910
export * from './table-grid'
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { FieldError, RequiredLabel } from './sidebar-fields'
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
'use client'
2+
3+
import type React from 'react'
4+
import { Label } from '@/components/emcn'
5+
6+
/**
7+
* Field label with a trailing required marker, matching the sidebar field
8+
* rhythm shared by the column-config and workflow sidebars.
9+
*/
10+
export function RequiredLabel({
11+
htmlFor,
12+
children,
13+
}: {
14+
htmlFor?: string
15+
children: React.ReactNode
16+
}) {
17+
return (
18+
<Label htmlFor={htmlFor} className='flex items-baseline gap-1.5 whitespace-nowrap pl-0.5'>
19+
{children}
20+
<span className='ml-0.5'>*</span>
21+
</Label>
22+
)
23+
}
24+
25+
/**
26+
* Inline validation error rendered under a sidebar field.
27+
*/
28+
export function FieldError({ message }: { message: string }) {
29+
return <p className='pl-0.5 text-[var(--text-error)] text-caption'>{message}</p>
30+
}

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

Lines changed: 52 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client'
22

3+
import type React from 'react'
34
import { AnimatePresence, domAnimation, LazyMotion, m } from 'framer-motion'
45
import { Button, Tooltip } from '@/components/emcn'
56
import { Eye, PlayOutline, RefreshCw, Square } from '@/components/emcn/icons'
@@ -98,71 +99,35 @@ export function TableActionBar({
9899

99100
<div className='flex items-center gap-[5px]'>
100101
{showPlay && (
101-
<Tooltip.Root>
102-
<Tooltip.Trigger asChild>
103-
<Button
104-
variant='ghost'
105-
onClick={onPlay}
106-
disabled={isLoading}
107-
className='hover-hover:!text-[var(--text-inverse)] size-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
108-
aria-label={playLabel}
109-
>
110-
<PlayOutline className='size-[12px]' />
111-
</Button>
112-
</Tooltip.Trigger>
113-
<Tooltip.Content side='top'>{playLabel}</Tooltip.Content>
114-
</Tooltip.Root>
102+
<ActionIconButton label={playLabel} onClick={onPlay} disabled={isLoading}>
103+
<PlayOutline className='size-[12px]' />
104+
</ActionIconButton>
115105
)}
116106

117107
{showRefresh && (
118-
<Tooltip.Root>
119-
<Tooltip.Trigger asChild>
120-
<Button
121-
variant='ghost'
122-
onClick={onRefresh}
123-
disabled={isLoading}
124-
className='hover-hover:!text-[var(--text-inverse)] size-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
125-
aria-label={refreshLabel}
126-
>
127-
<RefreshCw className='size-[12px]' />
128-
</Button>
129-
</Tooltip.Trigger>
130-
<Tooltip.Content side='top'>{refreshLabel}</Tooltip.Content>
131-
</Tooltip.Root>
108+
<ActionIconButton label={refreshLabel} onClick={onRefresh} disabled={isLoading}>
109+
<RefreshCw className='size-[12px]' />
110+
</ActionIconButton>
132111
)}
133112

134113
{runningCount > 0 && (
135-
<Tooltip.Root>
136-
<Tooltip.Trigger asChild>
137-
<Button
138-
variant='ghost'
139-
onClick={onStopWorkflows}
140-
disabled={isLoading}
141-
className='hover-hover:!text-[var(--text-inverse)] size-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
142-
aria-label={stopLabel}
143-
>
144-
<Square className='size-[12px]' />
145-
</Button>
146-
</Tooltip.Trigger>
147-
<Tooltip.Content side='top'>{stopLabel}</Tooltip.Content>
148-
</Tooltip.Root>
114+
<ActionIconButton
115+
label={stopLabel}
116+
onClick={onStopWorkflows}
117+
disabled={isLoading}
118+
>
119+
<Square className='size-[12px]' />
120+
</ActionIconButton>
149121
)}
150122

151123
{onViewExecution && (
152-
<Tooltip.Root>
153-
<Tooltip.Trigger asChild>
154-
<Button
155-
variant='ghost'
156-
onClick={onViewExecution}
157-
disabled={isLoading}
158-
className='hover-hover:!text-[var(--text-inverse)] size-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
159-
aria-label='View execution'
160-
>
161-
<Eye className='size-[12px]' />
162-
</Button>
163-
</Tooltip.Trigger>
164-
<Tooltip.Content side='top'>View execution</Tooltip.Content>
165-
</Tooltip.Root>
124+
<ActionIconButton
125+
label='View execution'
126+
onClick={onViewExecution}
127+
disabled={isLoading}
128+
>
129+
<Eye className='size-[12px]' />
130+
</ActionIconButton>
166131
)}
167132
</div>
168133
</div>
@@ -172,3 +137,34 @@ export function TableActionBar({
172137
</LazyMotion>
173138
)
174139
}
140+
141+
interface ActionIconButtonProps {
142+
/** Tooltip text, also used as the button's accessible label. */
143+
label: string
144+
onClick: () => void
145+
disabled: boolean
146+
children: React.ReactNode
147+
}
148+
149+
/**
150+
* Tooltip-wrapped icon button sharing the action bar's brand-hover chrome,
151+
* so the chrome string lives in one place.
152+
*/
153+
function ActionIconButton({ label, onClick, disabled, children }: ActionIconButtonProps) {
154+
return (
155+
<Tooltip.Root>
156+
<Tooltip.Trigger asChild>
157+
<Button
158+
variant='ghost'
159+
onClick={onClick}
160+
disabled={disabled}
161+
className='hover-hover:!text-[var(--text-inverse)] size-[28px] rounded-lg bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover-hover:bg-[var(--brand-secondary)]'
162+
aria-label={label}
163+
>
164+
{children}
165+
</Button>
166+
</Tooltip.Trigger>
167+
<Tooltip.Content side='top'>{label}</Tooltip.Content>
168+
</Tooltip.Root>
169+
)
170+
}

0 commit comments

Comments
 (0)