Skip to content

Commit 866e91d

Browse files
committed
feat(files): add sort and filter to files list page
1 parent 2e67864 commit 866e91d

File tree

2 files changed

+124
-11
lines changed

2 files changed

+124
-11
lines changed

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

Lines changed: 119 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
} from '@/components/emcn'
2626
import { File as FilesIcon } from '@/components/emcn/icons'
2727
import { getDocumentIcon } from '@/components/icons/document-icons'
28+
import { cn } from '@/lib/core/utils/cn'
2829
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
2930
import {
3031
downloadWorkspaceFile,
@@ -38,10 +39,12 @@ import {
3839
SUPPORTED_VIDEO_EXTENSIONS,
3940
} from '@/lib/uploads/utils/validation'
4041
import type {
42+
FilterTag,
4143
HeaderAction,
4244
ResourceColumn,
4345
ResourceRow,
4446
SearchConfig,
47+
SortConfig,
4548
} from '@/app/workspace/[workspaceId]/components'
4649
import {
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}

apps/sim/app/workspace/[workspaceId]/scheduled-tasks/scheduled-tasks.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@ export function ScheduledTasks() {
7474
const [activeTask, setActiveTask] = useState<WorkspaceScheduleData | null>(null)
7575
const [searchQuery, setSearchQuery] = useState('')
7676
const debouncedSearchQuery = useDebounce(searchQuery, 300)
77+
const [activeSort, setActiveSort] = useState<{
78+
column: string
79+
direction: 'asc' | 'desc'
80+
} | null>(null)
81+
const [scheduleTypeFilter, setScheduleTypeFilter] = useState<'all' | 'recurring' | 'once'>('all')
7782

7883
const visibleItems = useMemo(
7984
() => allItems.filter((item) => item.sourceType === 'job' && item.status !== 'completed'),

0 commit comments

Comments
 (0)