@@ -16,7 +16,9 @@ import {
1616import { Columns3 , Rows3 , Table as TableIcon } from '@/components/emcn/icons'
1717import type { TableDefinition } from '@/lib/table'
1818import { generateUniqueTableName } from '@/lib/table/constants'
19+ import { cn } from '@/lib/utils'
1920import 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+
5060export 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