Skip to content

Commit c56e3ac

Browse files
committed
improvement(tables): use combobox filter panel matching logs UI style
1 parent 446a665 commit c56e3ac

1 file changed

Lines changed: 147 additions & 115 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/tables

apps/sim/app/workspace/[workspaceId]/tables/tables.tsx

Lines changed: 147 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@
33
import { useCallback, useMemo, useRef, useState } from 'react'
44
import { createLogger } from '@sim/logger'
55
import { useParams, useRouter } from 'next/navigation'
6+
import type { ComboboxOption } from '@/components/emcn'
67
import {
78
Button,
9+
Combobox,
810
Modal,
911
ModalBody,
1012
ModalContent,
@@ -16,7 +18,6 @@ import {
1618
import { Columns3, Rows3, Table as TableIcon } from '@/components/emcn/icons'
1719
import type { TableDefinition } from '@/lib/table'
1820
import { generateUniqueTableName } from '@/lib/table/constants'
19-
import { cn } from '@/lib/utils'
2021
import type {
2122
FilterTag,
2223
ResourceColumn,
@@ -49,14 +50,6 @@ const COLUMNS: ResourceColumn[] = [
4950
{ id: 'updated', header: 'Last Updated' },
5051
]
5152

52-
const COLUMN_TYPE_LABELS: Record<string, string> = {
53-
string: 'Text',
54-
number: 'Number',
55-
boolean: 'Boolean',
56-
date: 'Date',
57-
json: 'JSON',
58-
}
59-
6053
export function Tables() {
6154
const params = useParams()
6255
const router = useRouter()
@@ -81,7 +74,7 @@ export function Tables() {
8174
column: string
8275
direction: 'asc' | 'desc'
8376
} | null>(null)
84-
const [rowCountFilter, setRowCountFilter] = useState<'all' | 'empty' | 'small' | 'large'>('all')
77+
const [rowCountFilter, setRowCountFilter] = useState<string[]>([])
8578
const [ownerFilter, setOwnerFilter] = useState<string[]>([])
8679
const [columnTypeFilter, setColumnTypeFilter] = useState<string[]>([])
8780
const [uploading, setUploading] = useState(false)
@@ -107,11 +100,12 @@ export function Tables() {
107100
? tables.filter((t) => t.name.toLowerCase().includes(debouncedSearchTerm.toLowerCase()))
108101
: tables
109102

110-
if (rowCountFilter !== 'all') {
103+
if (rowCountFilter.length > 0) {
111104
result = result.filter((t) => {
112-
if (rowCountFilter === 'empty') return t.rowCount === 0
113-
if (rowCountFilter === 'small') return t.rowCount >= 1 && t.rowCount <= 100
114-
return t.rowCount > 100 // large
105+
if (rowCountFilter.includes('empty') && t.rowCount === 0) return true
106+
if (rowCountFilter.includes('small') && t.rowCount >= 1 && t.rowCount <= 100) return true
107+
if (rowCountFilter.includes('large') && t.rowCount > 100) return true
108+
return false
115109
})
116110
}
117111
if (ownerFilter.length > 0) {
@@ -146,7 +140,7 @@ export function Tables() {
146140
}
147141
return dir === 'asc' ? cmp : -cmp
148142
})
149-
}, [tables, debouncedSearchTerm, activeSort, rowCountFilter, ownerFilter, columnTypeFilter])
143+
}, [tables, debouncedSearchTerm, rowCountFilter, ownerFilter, columnTypeFilter, activeSort])
150144

151145
const rows: ResourceRow[] = useMemo(
152146
() =>
@@ -199,128 +193,166 @@ export function Tables() {
199193
[activeSort]
200194
)
201195

196+
const rowCountDisplayLabel = useMemo(() => {
197+
if (rowCountFilter.length === 0) return 'All'
198+
if (rowCountFilter.length === 1) {
199+
const labels: Record<string, string> = {
200+
empty: 'Empty',
201+
small: 'Small (1–100)',
202+
large: 'Large (100+)',
203+
}
204+
return labels[rowCountFilter[0]] ?? rowCountFilter[0]
205+
}
206+
return `${rowCountFilter.length} selected`
207+
}, [rowCountFilter])
208+
209+
const columnTypeDisplayLabel = useMemo(() => {
210+
if (columnTypeFilter.length === 0) return 'All'
211+
if (columnTypeFilter.length === 1) {
212+
const labels: Record<string, string> = {
213+
string: 'Text',
214+
number: 'Number',
215+
boolean: 'Boolean',
216+
date: 'Date',
217+
json: 'JSON',
218+
}
219+
return labels[columnTypeFilter[0]] ?? columnTypeFilter[0]
220+
}
221+
return `${columnTypeFilter.length} selected`
222+
}, [columnTypeFilter])
223+
224+
const ownerDisplayLabel = useMemo(() => {
225+
if (ownerFilter.length === 0) return 'All'
226+
if (ownerFilter.length === 1)
227+
return members?.find((m) => m.userId === ownerFilter[0])?.name ?? '1 member'
228+
return `${ownerFilter.length} members`
229+
}, [ownerFilter, members])
230+
231+
const memberOptions: ComboboxOption[] = useMemo(
232+
() =>
233+
(members ?? []).map((m) => ({
234+
value: m.userId,
235+
label: m.name,
236+
iconElement: m.image ? (
237+
<img
238+
src={m.image}
239+
alt={m.name}
240+
referrerPolicy='no-referrer'
241+
className='h-[14px] w-[14px] rounded-full border border-[var(--border)] object-cover'
242+
/>
243+
) : (
244+
<span className='flex h-[14px] w-[14px] items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-3)] font-medium text-[8px] text-[var(--text-secondary)]'>
245+
{m.name.charAt(0).toUpperCase()}
246+
</span>
247+
),
248+
})),
249+
[members]
250+
)
251+
252+
const hasActiveFilters =
253+
rowCountFilter.length > 0 || columnTypeFilter.length > 0 || ownerFilter.length > 0
254+
202255
const filterContent = (
203-
<div className='w-[200px]'>
204-
<div className='border-[var(--border-1)] border-b px-3 py-2'>
256+
<div className='flex w-[240px] flex-col gap-3 p-3'>
257+
<div className='flex flex-col gap-1.5'>
205258
<span className='font-medium text-[var(--text-secondary)] text-caption'>Row Count</span>
206-
</div>
207-
<div className='flex flex-col gap-0.5 px-3 py-2'>
208-
{(
209-
[
210-
{ value: 'all', label: 'All' },
259+
<Combobox
260+
options={[
211261
{ value: 'empty', label: 'Empty' },
212262
{ value: 'small', label: 'Small (1–100 rows)' },
213263
{ value: 'large', label: 'Large (100+ rows)' },
214-
] as const
215-
).map(({ value, label }) => (
216-
<button
217-
key={value}
218-
type='button'
219-
className={cn(
220-
'flex w-full cursor-pointer select-none items-center rounded-[5px] px-2 py-[5px] font-medium text-[var(--text-secondary)] text-caption outline-none transition-colors hover-hover:bg-[var(--surface-active)]',
221-
rowCountFilter === value && 'bg-[var(--surface-active)]'
222-
)}
223-
onClick={() => setRowCountFilter(value)}
224-
>
225-
{label}
226-
</button>
227-
))}
264+
]}
265+
multiSelect
266+
multiSelectValues={rowCountFilter}
267+
onMultiSelectChange={setRowCountFilter}
268+
overlayContent={
269+
<span className='truncate text-[var(--text-primary)]'>{rowCountDisplayLabel}</span>
270+
}
271+
showAllOption
272+
allOptionLabel='All'
273+
size='sm'
274+
className='h-[32px] w-full rounded-md'
275+
/>
228276
</div>
229-
<div className='border-[var(--border-1)] border-t border-b px-3 py-2'>
277+
<div className='flex flex-col gap-1.5'>
230278
<span className='font-medium text-[var(--text-secondary)] text-caption'>Column Types</span>
279+
<Combobox
280+
options={[
281+
{ value: 'string', label: 'Text' },
282+
{ value: 'number', label: 'Number' },
283+
{ value: 'boolean', label: 'Boolean' },
284+
{ value: 'date', label: 'Date' },
285+
{ value: 'json', label: 'JSON' },
286+
]}
287+
multiSelect
288+
multiSelectValues={columnTypeFilter}
289+
onMultiSelectChange={setColumnTypeFilter}
290+
overlayContent={
291+
<span className='truncate text-[var(--text-primary)]'>{columnTypeDisplayLabel}</span>
292+
}
293+
showAllOption
294+
allOptionLabel='All'
295+
size='sm'
296+
className='h-[32px] w-full rounded-md'
297+
/>
231298
</div>
232-
<div className='flex flex-col gap-0.5 px-3 py-2'>
299+
{memberOptions.length > 0 && (
300+
<div className='flex flex-col gap-1.5'>
301+
<span className='font-medium text-[var(--text-secondary)] text-caption'>Owner</span>
302+
<Combobox
303+
options={memberOptions}
304+
multiSelect
305+
multiSelectValues={ownerFilter}
306+
onMultiSelectChange={setOwnerFilter}
307+
overlayContent={
308+
<span className='truncate text-[var(--text-primary)]'>{ownerDisplayLabel}</span>
309+
}
310+
searchable
311+
searchPlaceholder='Search members...'
312+
showAllOption
313+
allOptionLabel='All'
314+
size='sm'
315+
className='h-[32px] w-full rounded-md'
316+
/>
317+
</div>
318+
)}
319+
{hasActiveFilters && (
233320
<button
234321
type='button'
235-
className={cn(
236-
'flex w-full cursor-pointer select-none items-center rounded-[5px] px-2 py-[5px] font-medium text-[var(--text-secondary)] text-caption outline-none transition-colors hover-hover:bg-[var(--surface-active)]',
237-
columnTypeFilter.length === 0 && 'bg-[var(--surface-active)]'
238-
)}
239-
onClick={() => setColumnTypeFilter([])}
322+
onClick={() => {
323+
setRowCountFilter([])
324+
setColumnTypeFilter([])
325+
setOwnerFilter([])
326+
}}
327+
className='flex h-[32px] w-full items-center justify-center rounded-md text-[var(--text-secondary)] text-caption transition-colors hover-hover:bg-[var(--surface-active)]'
240328
>
241-
All
329+
Clear all filters
242330
</button>
243-
{(['string', 'number', 'boolean', 'date', 'json'] as const).map((type) => (
244-
<button
245-
key={type}
246-
type='button'
247-
className={cn(
248-
'flex w-full cursor-pointer select-none items-center rounded-[5px] px-2 py-[5px] font-medium text-[var(--text-secondary)] text-caption outline-none transition-colors hover-hover:bg-[var(--surface-active)]',
249-
columnTypeFilter.includes(type) && 'bg-[var(--surface-active)]'
250-
)}
251-
onClick={() =>
252-
setColumnTypeFilter((prev) =>
253-
prev.includes(type) ? prev.filter((t) => t !== type) : [...prev, type]
254-
)
255-
}
256-
>
257-
{COLUMN_TYPE_LABELS[type]}
258-
</button>
259-
))}
260-
</div>
261-
{members && members.length > 0 && (
262-
<>
263-
<div className='border-[var(--border-1)] border-t border-b px-3 py-2'>
264-
<span className='font-medium text-[var(--text-secondary)] text-caption'>Owner</span>
265-
</div>
266-
<div className='flex flex-col gap-0.5 px-3 py-2'>
267-
<button
268-
type='button'
269-
className={cn(
270-
'flex w-full cursor-pointer select-none items-center rounded-[5px] px-2 py-[5px] font-medium text-[var(--text-secondary)] text-caption outline-none transition-colors hover-hover:bg-[var(--surface-active)]',
271-
ownerFilter.length === 0 && 'bg-[var(--surface-active)]'
272-
)}
273-
onClick={() => setOwnerFilter([])}
274-
>
275-
All
276-
</button>
277-
{members.map((member) => (
278-
<button
279-
key={member.userId}
280-
type='button'
281-
className={cn(
282-
'flex w-full cursor-pointer select-none items-center gap-1.5 rounded-[5px] px-2 py-[5px] font-medium text-[var(--text-secondary)] text-caption outline-none transition-colors hover-hover:bg-[var(--surface-active)]',
283-
ownerFilter.includes(member.userId) && 'bg-[var(--surface-active)]'
284-
)}
285-
onClick={() =>
286-
setOwnerFilter((prev) =>
287-
prev.includes(member.userId)
288-
? prev.filter((id) => id !== member.userId)
289-
: [...prev, member.userId]
290-
)
291-
}
292-
>
293-
{member.image ? (
294-
<img
295-
src={member.image}
296-
alt={member.name}
297-
referrerPolicy='no-referrer'
298-
className='h-[14px] w-[14px] shrink-0 rounded-full border border-[var(--border)] object-cover'
299-
/>
300-
) : (
301-
<span className='flex h-[14px] w-[14px] shrink-0 items-center justify-center rounded-full border border-[var(--border)] bg-[var(--surface-3)] font-medium text-[8px] text-[var(--text-secondary)]'>
302-
{member.name.charAt(0).toUpperCase()}
303-
</span>
304-
)}
305-
<span className='truncate'>{member.name}</span>
306-
</button>
307-
))}
308-
</div>
309-
</>
310331
)}
311332
</div>
312333
)
313334

314335
const filterTags: FilterTag[] = useMemo(() => {
315336
const tags: FilterTag[] = []
316-
if (rowCountFilter !== 'all') {
317-
const labels = { empty: 'Rows: Empty', small: 'Rows: Small', large: 'Rows: Large' }
318-
tags.push({ label: labels[rowCountFilter], onRemove: () => setRowCountFilter('all') })
337+
if (rowCountFilter.length > 0) {
338+
const rowLabels: Record<string, string> = { empty: 'Empty', small: 'Small', large: 'Large' }
339+
const label =
340+
rowCountFilter.length === 1
341+
? `Rows: ${rowLabels[rowCountFilter[0]]}`
342+
: `Rows: ${rowCountFilter.length} selected`
343+
tags.push({ label, onRemove: () => setRowCountFilter([]) })
319344
}
320345
if (columnTypeFilter.length > 0) {
346+
const typeLabels: Record<string, string> = {
347+
string: 'Text',
348+
number: 'Number',
349+
boolean: 'Boolean',
350+
date: 'Date',
351+
json: 'JSON',
352+
}
321353
const label =
322354
columnTypeFilter.length === 1
323-
? `Type: ${COLUMN_TYPE_LABELS[columnTypeFilter[0]]}`
355+
? `Type: ${typeLabels[columnTypeFilter[0]]}`
324356
: `Types: ${columnTypeFilter.length} selected`
325357
tags.push({ label, onRemove: () => setColumnTypeFilter([]) })
326358
}

0 commit comments

Comments
 (0)