33import { useCallback , useMemo , useRef , useState } from 'react'
44import { createLogger } from '@sim/logger'
55import { useParams , useRouter } from 'next/navigation'
6+ import type { ComboboxOption } from '@/components/emcn'
67import {
78 Button ,
9+ Combobox ,
810 Modal ,
911 ModalBody ,
1012 ModalContent ,
@@ -16,7 +18,6 @@ import {
1618import { Columns3 , Rows3 , Table as TableIcon } from '@/components/emcn/icons'
1719import type { TableDefinition } from '@/lib/table'
1820import { generateUniqueTableName } from '@/lib/table/constants'
19- import { cn } from '@/lib/utils'
2021import 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-
6053export 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