Skip to content

Commit 0142c69

Browse files
committed
feat(tables): add row count, owner, and column type filters
1 parent a3ffc2f commit 0142c69

File tree

1 file changed

+168
-2
lines changed
  • apps/sim/app/workspace/[workspaceId]/tables

1 file changed

+168
-2
lines changed

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

Lines changed: 168 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import {
1616
import { Columns3, Rows3, Table as TableIcon } from '@/components/emcn/icons'
1717
import type { TableDefinition } from '@/lib/table'
1818
import { generateUniqueTableName } from '@/lib/table/constants'
19+
import { cn } from '@/lib/utils'
1920
import type {
21+
FilterTag,
2022
ResourceColumn,
2123
ResourceRow,
2224
SearchConfig,
@@ -47,6 +49,14 @@ const COLUMNS: ResourceColumn[] = [
4749
{ id: 'updated', header: 'Last Updated' },
4850
]
4951

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+
5060
export function Tables() {
5161
const params = useParams()
5262
const router = useRouter()
@@ -71,6 +81,9 @@ export function Tables() {
7181
column: string
7282
direction: 'asc' | 'desc'
7383
} | null>(null)
84+
const [rowCountFilter, setRowCountFilter] = useState<'all' | 'empty' | 'small' | 'large'>('all')
85+
const [ownerFilter, setOwnerFilter] = useState<string[]>([])
86+
const [columnTypeFilter, setColumnTypeFilter] = useState<string[]>([])
7487
const [uploading, setUploading] = useState(false)
7588
const [uploadProgress, setUploadProgress] = useState({ completed: 0, total: 0 })
7689
const csvInputRef = useRef<HTMLInputElement>(null)
@@ -90,10 +103,26 @@ export function Tables() {
90103
} = useContextMenu()
91104

92105
const processedTables = useMemo(() => {
93-
const result = debouncedSearchTerm
106+
let result = debouncedSearchTerm
94107
? tables.filter((t) => t.name.toLowerCase().includes(debouncedSearchTerm.toLowerCase()))
95108
: tables
96109

110+
if (rowCountFilter !== 'all') {
111+
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
115+
})
116+
}
117+
if (ownerFilter.length > 0) {
118+
result = result.filter((t) => ownerFilter.includes(t.createdBy))
119+
}
120+
if (columnTypeFilter.length > 0) {
121+
result = result.filter((t) =>
122+
t.schema.columns.some((col) => columnTypeFilter.includes(col.type))
123+
)
124+
}
125+
97126
const col = activeSort?.column ?? 'created'
98127
const dir = activeSort?.direction ?? 'desc'
99128
return [...result].sort((a, b) => {
@@ -117,7 +146,7 @@ export function Tables() {
117146
}
118147
return dir === 'asc' ? cmp : -cmp
119148
})
120-
}, [tables, debouncedSearchTerm, activeSort])
149+
}, [tables, debouncedSearchTerm, activeSort, rowCountFilter, ownerFilter, columnTypeFilter])
121150

122151
const rows: ResourceRow[] = useMemo(
123152
() =>
@@ -170,6 +199,141 @@ export function Tables() {
170199
[activeSort]
171200
)
172201

202+
const filterContent = (
203+
<div className='w-[200px]'>
204+
<div className='border-[var(--border-1)] border-b px-3 py-2'>
205+
<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' },
211+
{ value: 'empty', label: 'Empty' },
212+
{ value: 'small', label: 'Small (1–100 rows)' },
213+
{ 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+
))}
228+
</div>
229+
<div className='border-[var(--border-1)] border-t border-b px-3 py-2'>
230+
<span className='font-medium text-[var(--text-secondary)] text-caption'>Column Types</span>
231+
</div>
232+
<div className='flex flex-col gap-0.5 px-3 py-2'>
233+
<button
234+
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([])}
240+
>
241+
All
242+
</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+
</>
310+
)}
311+
</div>
312+
)
313+
314+
const filterTags: FilterTag[] = useMemo(() => {
315+
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') })
319+
}
320+
if (columnTypeFilter.length > 0) {
321+
const label =
322+
columnTypeFilter.length === 1
323+
? `Type: ${COLUMN_TYPE_LABELS[columnTypeFilter[0]]}`
324+
: `Types: ${columnTypeFilter.length} selected`
325+
tags.push({ label, onRemove: () => setColumnTypeFilter([]) })
326+
}
327+
if (ownerFilter.length > 0) {
328+
const label =
329+
ownerFilter.length === 1
330+
? `Owner: ${members?.find((m) => m.userId === ownerFilter[0])?.name ?? '1 member'}`
331+
: `Owner: ${ownerFilter.length} members`
332+
tags.push({ label, onRemove: () => setOwnerFilter([]) })
333+
}
334+
return tags
335+
}, [rowCountFilter, columnTypeFilter, ownerFilter, members])
336+
173337
const handleContentContextMenu = useCallback(
174338
(e: React.MouseEvent) => {
175339
const target = e.target as HTMLElement
@@ -317,6 +481,8 @@ export function Tables() {
317481
}}
318482
search={searchConfig}
319483
sort={sortConfig}
484+
filter={filterContent}
485+
filterTags={filterTags}
320486
headerActions={[
321487
{
322488
label: uploadButtonLabel,

0 commit comments

Comments
 (0)