@@ -25,6 +25,7 @@ import {
2525} from '@/components/emcn'
2626import { File as FilesIcon } from '@/components/emcn/icons'
2727import { getDocumentIcon } from '@/components/icons/document-icons'
28+ import { cn } from '@/lib/core/utils/cn'
2829import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
2930import {
3031 downloadWorkspaceFile ,
@@ -38,10 +39,12 @@ import {
3839 SUPPORTED_VIDEO_EXTENSIONS ,
3940} from '@/lib/uploads/utils/validation'
4041import type {
42+ FilterTag ,
4143 HeaderAction ,
4244 ResourceColumn ,
4345 ResourceRow ,
4446 SearchConfig ,
47+ SortConfig ,
4548} from '@/app/workspace/[workspaceId]/components'
4649import {
4750 InlineRenameInput ,
@@ -162,6 +165,11 @@ export function Files() {
162165 const [ uploadProgress , setUploadProgress ] = useState ( { completed : 0 , total : 0 } )
163166 const [ inputValue , setInputValue ] = useState ( '' )
164167 const [ debouncedSearchTerm , setDebouncedSearchTerm ] = useState ( '' )
168+ const [ activeSort , setActiveSort ] = useState < {
169+ column : string
170+ direction : 'asc' | 'desc'
171+ } | null > ( null )
172+ const [ typeFilter , setTypeFilter ] = useState < 'all' | 'document' | 'audio' | 'video' > ( 'all' )
165173 const searchTimerRef = useRef < ReturnType < typeof setTimeout > > ( null )
166174
167175 const handleSearchChange = useCallback ( ( value : string ) => {
@@ -206,10 +214,51 @@ export function Files() {
206214 selectedFileRef . current = selectedFile
207215
208216 const filteredFiles = useMemo ( ( ) => {
209- if ( ! debouncedSearchTerm ) return files
210- const q = debouncedSearchTerm . toLowerCase ( )
211- return files . filter ( ( f ) => f . name . toLowerCase ( ) . includes ( q ) )
212- } , [ files , debouncedSearchTerm ] )
217+ let result = debouncedSearchTerm
218+ ? files . filter ( ( f ) => f . name . toLowerCase ( ) . includes ( debouncedSearchTerm . toLowerCase ( ) ) )
219+ : files
220+
221+ if ( typeFilter !== 'all' ) {
222+ result = result . filter ( ( f ) => {
223+ const ext = getFileExtension ( f . name )
224+ if ( typeFilter === 'document' )
225+ return SUPPORTED_DOCUMENT_EXTENSIONS . includes (
226+ ext as ( typeof SUPPORTED_DOCUMENT_EXTENSIONS ) [ number ]
227+ )
228+ if ( typeFilter === 'audio' )
229+ return SUPPORTED_AUDIO_EXTENSIONS . includes (
230+ ext as ( typeof SUPPORTED_AUDIO_EXTENSIONS ) [ number ]
231+ )
232+ if ( typeFilter === 'video' )
233+ return SUPPORTED_VIDEO_EXTENSIONS . includes (
234+ ext as ( typeof SUPPORTED_VIDEO_EXTENSIONS ) [ number ]
235+ )
236+ return true
237+ } )
238+ }
239+
240+ const col = activeSort ?. column ?? 'created'
241+ const dir = activeSort ?. direction ?? 'desc'
242+ return [ ...result ] . sort ( ( a , b ) => {
243+ let cmp = 0
244+ switch ( col ) {
245+ case 'name' :
246+ cmp = a . name . localeCompare ( b . name )
247+ break
248+ case 'size' :
249+ cmp = a . size - b . size
250+ break
251+ case 'type' :
252+ cmp = formatFileType ( a . type , a . name ) . localeCompare ( formatFileType ( b . type , b . name ) )
253+ break
254+ case 'created' :
255+ case 'updated' :
256+ cmp = new Date ( a . uploadedAt ) . getTime ( ) - new Date ( b . uploadedAt ) . getTime ( )
257+ break
258+ }
259+ return dir === 'asc' ? cmp : - cmp
260+ } )
261+ } , [ files , debouncedSearchTerm , typeFilter , activeSort ] )
213262
214263 const rowCacheRef = useRef (
215264 new Map < string , { row : ResourceRow ; file : WorkspaceFileRecord ; members : typeof members } > ( )
@@ -247,11 +296,6 @@ export function Files() {
247296 owner : ownerCell ( file . uploadedBy , members ) ,
248297 updated : timeCell ( file . uploadedAt ) ,
249298 } ,
250- sortValues : {
251- size : file . size ,
252- created : - new Date ( file . uploadedAt ) . getTime ( ) ,
253- updated : - new Date ( file . uploadedAt ) . getTime ( ) ,
254- } ,
255299 }
256300 nextCache . set ( file . id , { row, file, members } )
257301 return row
@@ -690,7 +734,6 @@ export function Files() {
690734 handleDeleteSelected ,
691735 ] )
692736
693- /** Stable refs for values used in callbacks to avoid dependency churn */
694737 const listRenameRef = useRef ( listRename )
695738 listRenameRef . current = listRename
696739 const headerRenameRef = useRef ( headerRename )
@@ -764,6 +807,69 @@ export function Files() {
764807 [ handleNavigateToFiles ]
765808 )
766809
810+ const sortConfig : SortConfig = useMemo (
811+ ( ) => ( {
812+ options : [
813+ { id : 'name' , label : 'Name' } ,
814+ { id : 'size' , label : 'Size' } ,
815+ { id : 'type' , label : 'Type' } ,
816+ { id : 'created' , label : 'Created' } ,
817+ ] ,
818+ active : activeSort ,
819+ onSort : ( column , direction ) => setActiveSort ( { column, direction } ) ,
820+ onClear : ( ) => setActiveSort ( null ) ,
821+ } ) ,
822+ [ activeSort ]
823+ )
824+
825+ const filterContent = (
826+ < div className = 'w-[200px]' >
827+ < div className = 'border-[var(--border-1)] border-b px-3 py-2' >
828+ < span className = 'font-medium text-[var(--text-secondary)] text-caption' > File Type</ span >
829+ </ div >
830+ < div className = 'flex flex-col gap-0.5 px-3 py-2' >
831+ { (
832+ [
833+ { value : 'all' , label : 'All' } ,
834+ { value : 'document' , label : 'Documents' } ,
835+ { value : 'audio' , label : 'Audio' } ,
836+ { value : 'video' , label : 'Video' } ,
837+ ] as const
838+ ) . map ( ( { value, label } ) => (
839+ < button
840+ key = { value }
841+ type = 'button'
842+ className = { cn (
843+ '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)]' ,
844+ typeFilter === value && 'bg-[var(--surface-active)]'
845+ ) }
846+ onClick = { ( ) => setTypeFilter ( value ) }
847+ >
848+ { label }
849+ </ button >
850+ ) ) }
851+ </ div >
852+ </ div >
853+ )
854+
855+ const filterTags : FilterTag [ ] = useMemo (
856+ ( ) =>
857+ typeFilter === 'all'
858+ ? [ ]
859+ : [
860+ {
861+ label :
862+ typeFilter === 'document'
863+ ? 'Type: Documents'
864+ : typeFilter === 'audio'
865+ ? 'Type: Audio'
866+ : 'Type: Video' ,
867+ onRemove : ( ) => setTypeFilter ( 'all' ) ,
868+ } ,
869+ ] ,
870+ [ typeFilter ]
871+ )
872+
767873 if ( fileIdFromRoute && ! selectedFile ) {
768874 return (
769875 < div className = 'flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]' >
@@ -834,7 +940,9 @@ export function Files() {
834940 title = 'Files'
835941 create = { createConfig }
836942 search = { searchConfig }
837- defaultSort = 'created'
943+ sort = { sortConfig }
944+ filter = { filterContent }
945+ filterTags = { filterTags }
838946 headerActions = { headerActionsConfig }
839947 columns = { COLUMNS }
840948 rows = { rows }
0 commit comments