Skip to content

Commit 446a665

Browse files
committed
improvement(files): use combobox filter panel matching logs UI style
Replaces button-list filters with Combobox-based multi-select sections for file type, size, and uploaded-by filters, aligning the panel with the logs page filter UI.
1 parent 9dd3028 commit 446a665

File tree

1 file changed

+152
-110
lines changed
  • apps/sim/app/workspace/[workspaceId]/files

1 file changed

+152
-110
lines changed

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

Lines changed: 152 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { useParams, useRouter } from 'next/navigation'
66
import {
77
Button,
88
Columns2,
9+
type ComboboxOption,
910
Download,
1011
DropdownMenu,
1112
DropdownMenuContent,
@@ -25,7 +26,6 @@ import {
2526
} from '@/components/emcn'
2627
import { File as FilesIcon } from '@/components/emcn/icons'
2728
import { getDocumentIcon } from '@/components/icons/document-icons'
28-
import { cn } from '@/lib/core/utils/cn'
2929
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
3030
import {
3131
downloadWorkspaceFile,
@@ -173,8 +173,8 @@ export function Files() {
173173
column: string
174174
direction: 'asc' | 'desc'
175175
} | null>(null)
176-
const [typeFilter, setTypeFilter] = useState<'all' | 'document' | 'audio' | 'video'>('all')
177-
const [sizeFilter, setSizeFilter] = useState<'all' | 'small' | 'medium' | 'large'>('all')
176+
const [typeFilter, setTypeFilter] = useState<string[]>([])
177+
const [sizeFilter, setSizeFilter] = useState<string[]>([])
178178
const [uploadedByFilter, setUploadedByFilter] = useState<string[]>([])
179179

180180
const [creatingFile, setCreatingFile] = useState(false)
@@ -215,21 +215,23 @@ export function Files() {
215215
? files.filter((f) => f.name.toLowerCase().includes(debouncedSearchTerm.toLowerCase()))
216216
: files
217217

218-
if (typeFilter !== 'all') {
218+
if (typeFilter.length > 0) {
219219
result = result.filter((f) => {
220220
const ext = getFileExtension(f.name)
221-
if (typeFilter === 'document') return isSupportedExtension(ext)
222-
if (typeFilter === 'audio') return isSupportedAudioExtension(ext)
223-
if (typeFilter === 'video') return isSupportedVideoExtension(ext)
224-
return true
221+
if (typeFilter.includes('document') && isSupportedExtension(ext)) return true
222+
if (typeFilter.includes('audio') && isSupportedAudioExtension(ext)) return true
223+
if (typeFilter.includes('video') && isSupportedVideoExtension(ext)) return true
224+
return false
225225
})
226226
}
227227

228-
if (sizeFilter !== 'all') {
228+
if (sizeFilter.length > 0) {
229229
result = result.filter((f) => {
230-
if (sizeFilter === 'small') return f.size < 1_048_576
231-
if (sizeFilter === 'medium') return f.size >= 1_048_576 && f.size <= 10_485_760
232-
return f.size > 10_485_760 // large
230+
if (sizeFilter.includes('small') && f.size < 1_048_576) return true
231+
if (sizeFilter.includes('medium') && f.size >= 1_048_576 && f.size <= 10_485_760)
232+
return true
233+
if (sizeFilter.includes('large') && f.size > 10_485_760) return true
234+
return false
233235
})
234236
}
235237

@@ -803,6 +805,56 @@ export function Files() {
803805
[handleNavigateToFiles]
804806
)
805807

808+
const typeDisplayLabel = useMemo(() => {
809+
if (typeFilter.length === 0) return 'All'
810+
if (typeFilter.length === 1) {
811+
const labels: Record<string, string> = {
812+
document: 'Documents',
813+
audio: 'Audio',
814+
video: 'Video',
815+
}
816+
return labels[typeFilter[0]] ?? typeFilter[0]
817+
}
818+
return `${typeFilter.length} selected`
819+
}, [typeFilter])
820+
821+
const sizeDisplayLabel = useMemo(() => {
822+
if (sizeFilter.length === 0) return 'All'
823+
if (sizeFilter.length === 1) {
824+
const labels: Record<string, string> = { small: 'Small', medium: 'Medium', large: 'Large' }
825+
return labels[sizeFilter[0]] ?? sizeFilter[0]
826+
}
827+
return `${sizeFilter.length} selected`
828+
}, [sizeFilter])
829+
830+
const uploadedByDisplayLabel = useMemo(() => {
831+
if (uploadedByFilter.length === 0) return 'All'
832+
if (uploadedByFilter.length === 1)
833+
return members?.find((m) => m.userId === uploadedByFilter[0])?.name ?? '1 member'
834+
return `${uploadedByFilter.length} members`
835+
}, [uploadedByFilter, members])
836+
837+
const memberOptions: ComboboxOption[] = useMemo(
838+
() =>
839+
(members ?? []).map((m) => ({
840+
value: m.userId,
841+
label: m.name,
842+
iconElement: m.image ? (
843+
<img
844+
src={m.image}
845+
alt={m.name}
846+
referrerPolicy='no-referrer'
847+
className='h-[14px] w-[14px] rounded-full border border-[var(--border)] object-cover'
848+
/>
849+
) : (
850+
<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)]'>
851+
{m.name.charAt(0).toUpperCase()}
852+
</span>
853+
),
854+
})),
855+
[members]
856+
)
857+
806858
const sortConfig: SortConfig = useMemo(
807859
() => ({
808860
options: [
@@ -818,122 +870,112 @@ export function Files() {
818870
[activeSort]
819871
)
820872

873+
const hasActiveFilters =
874+
typeFilter.length > 0 || sizeFilter.length > 0 || uploadedByFilter.length > 0
875+
821876
const filterContent = (
822-
<div className='w-[200px]'>
823-
<div className='border-[var(--border-1)] border-b px-3 py-2'>
877+
<div className='flex w-[240px] flex-col gap-3 p-3'>
878+
<div className='flex flex-col gap-1.5'>
824879
<span className='font-medium text-[var(--text-secondary)] text-caption'>File Type</span>
825-
</div>
826-
<div className='flex flex-col gap-0.5 px-3 py-2'>
827-
{(
828-
[
829-
{ value: 'all', label: 'All' },
880+
<Combobox
881+
options={[
830882
{ value: 'document', label: 'Documents' },
831883
{ value: 'audio', label: 'Audio' },
832884
{ value: 'video', label: 'Video' },
833-
] as const
834-
).map(({ value, label }) => (
835-
<button
836-
key={value}
837-
type='button'
838-
className={cn(
839-
'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)]',
840-
typeFilter === value && 'bg-[var(--surface-active)]'
841-
)}
842-
onClick={() => setTypeFilter(value)}
843-
>
844-
{label}
845-
</button>
846-
))}
885+
]}
886+
multiSelect
887+
multiSelectValues={typeFilter}
888+
onMultiSelectChange={setTypeFilter}
889+
overlayContent={
890+
<span className='truncate text-[var(--text-primary)]'>{typeDisplayLabel}</span>
891+
}
892+
showAllOption
893+
allOptionLabel='All'
894+
size='sm'
895+
className='h-[32px] w-full rounded-md'
896+
/>
847897
</div>
848-
<div className='border-[var(--border-1)] border-t border-b px-3 py-2'>
898+
<div className='flex flex-col gap-1.5'>
849899
<span className='font-medium text-[var(--text-secondary)] text-caption'>Size</span>
850-
</div>
851-
<div className='flex flex-col gap-0.5 px-3 py-2'>
852-
{(
853-
[
854-
{ value: 'all', label: 'All' },
900+
<Combobox
901+
options={[
855902
{ value: 'small', label: 'Small (< 1 MB)' },
856903
{ value: 'medium', label: 'Medium (1–10 MB)' },
857904
{ value: 'large', label: 'Large (> 10 MB)' },
858-
] as const
859-
).map(({ value, label }) => (
860-
<button
861-
key={value}
862-
type='button'
863-
className={cn(
864-
'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)]',
865-
sizeFilter === value && 'bg-[var(--surface-active)]'
866-
)}
867-
onClick={() => setSizeFilter(value)}
868-
>
869-
{label}
870-
</button>
871-
))}
905+
]}
906+
multiSelect
907+
multiSelectValues={sizeFilter}
908+
onMultiSelectChange={setSizeFilter}
909+
overlayContent={
910+
<span className='truncate text-[var(--text-primary)]'>{sizeDisplayLabel}</span>
911+
}
912+
showAllOption
913+
allOptionLabel='All'
914+
size='sm'
915+
className='h-[32px] w-full rounded-md'
916+
/>
872917
</div>
873-
{members && members.length > 0 && (
874-
<>
875-
<div className='border-[var(--border-1)] border-t border-b px-3 py-2'>
876-
<span className='font-medium text-[var(--text-secondary)] text-caption'>
877-
Uploaded By
878-
</span>
879-
</div>
880-
<div className='flex flex-col gap-0.5 px-3 py-2'>
881-
<button
882-
type='button'
883-
className={cn(
884-
'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)]',
885-
uploadedByFilter.length === 0 && 'bg-[var(--surface-active)]'
886-
)}
887-
onClick={() => setUploadedByFilter([])}
888-
>
889-
All
890-
</button>
891-
{members.map((member) => (
892-
<button
893-
key={member.userId}
894-
type='button'
895-
className={cn(
896-
'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)]',
897-
uploadedByFilter.includes(member.userId) && 'bg-[var(--surface-active)]'
898-
)}
899-
onClick={() =>
900-
setUploadedByFilter((prev) =>
901-
prev.includes(member.userId)
902-
? prev.filter((id) => id !== member.userId)
903-
: [...prev, member.userId]
904-
)
905-
}
906-
>
907-
{member.image ? (
908-
<img
909-
src={member.image}
910-
alt={member.name}
911-
referrerPolicy='no-referrer'
912-
className='h-[14px] w-[14px] shrink-0 rounded-full border border-[var(--border)] object-cover'
913-
/>
914-
) : (
915-
<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)]'>
916-
{member.name.charAt(0).toUpperCase()}
917-
</span>
918-
)}
919-
<span className='truncate'>{member.name}</span>
920-
</button>
921-
))}
922-
</div>
923-
</>
918+
{memberOptions.length > 0 && (
919+
<div className='flex flex-col gap-1.5'>
920+
<span className='font-medium text-[var(--text-secondary)] text-caption'>Uploaded By</span>
921+
<Combobox
922+
options={memberOptions}
923+
multiSelect
924+
multiSelectValues={uploadedByFilter}
925+
onMultiSelectChange={setUploadedByFilter}
926+
overlayContent={
927+
<span className='truncate text-[var(--text-primary)]'>{uploadedByDisplayLabel}</span>
928+
}
929+
searchable
930+
searchPlaceholder='Search members...'
931+
showAllOption
932+
allOptionLabel='All'
933+
size='sm'
934+
className='h-[32px] w-full rounded-md'
935+
/>
936+
</div>
937+
)}
938+
{hasActiveFilters && (
939+
<button
940+
type='button'
941+
onClick={() => {
942+
setTypeFilter([])
943+
setSizeFilter([])
944+
setUploadedByFilter([])
945+
}}
946+
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)]'
947+
>
948+
Clear all filters
949+
</button>
924950
)}
925951
</div>
926952
)
927953

928954
const filterTags: FilterTag[] = useMemo(() => {
929955
const tags: FilterTag[] = []
930-
if (typeFilter !== 'all') {
931-
const labels = { document: 'Type: Documents', audio: 'Type: Audio', video: 'Type: Video' }
932-
tags.push({ label: labels[typeFilter], onRemove: () => setTypeFilter('all') })
956+
if (typeFilter.length > 0) {
957+
const typeLabels: Record<string, string> = {
958+
document: 'Documents',
959+
audio: 'Audio',
960+
video: 'Video',
961+
}
962+
const label =
963+
typeFilter.length === 1
964+
? `Type: ${typeLabels[typeFilter[0]]}`
965+
: `Type: ${typeFilter.length} selected`
966+
tags.push({ label, onRemove: () => setTypeFilter([]) })
933967
}
934-
if (sizeFilter !== 'all') {
935-
const labels = { small: 'Size: Small', medium: 'Size: Medium', large: 'Size: Large' }
936-
tags.push({ label: labels[sizeFilter], onRemove: () => setSizeFilter('all') })
968+
if (sizeFilter.length > 0) {
969+
const sizeLabels: Record<string, string> = {
970+
small: 'Small',
971+
medium: 'Medium',
972+
large: 'Large',
973+
}
974+
const label =
975+
sizeFilter.length === 1
976+
? `Size: ${sizeLabels[sizeFilter[0]]}`
977+
: `Size: ${sizeFilter.length} selected`
978+
tags.push({ label, onRemove: () => setSizeFilter([]) })
937979
}
938980
if (uploadedByFilter.length > 0) {
939981
const label =

0 commit comments

Comments
 (0)