Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
e622b6e
improvement(tables): improve table filtering UX
waleedlatif1 Mar 29, 2026
bcc7974
fix(table-filter): use ref to stabilize handleRemove/handleApply call…
waleedlatif1 Mar 29, 2026
5d037ac
improvement(tables,kb): remove hacky patterns, fix KB filter popover …
waleedlatif1 Mar 29, 2026
2e67864
feat(knowledge): add sort and filter to KB list page
waleedlatif1 Mar 29, 2026
866e91d
feat(files): add sort and filter to files list page
waleedlatif1 Mar 29, 2026
6c18471
feat(scheduled-tasks): add sort and filter to scheduled tasks page
waleedlatif1 Mar 29, 2026
f46f83c
fix(table-filter): use explicit close handler instead of toggle
waleedlatif1 Mar 29, 2026
8a1f3dc
improvement(files,knowledge): replace manual debounce with useDebounc…
waleedlatif1 Mar 29, 2026
4899dc3
fix(resource): prevent popover from inheriting anchor min-width
waleedlatif1 Mar 29, 2026
f6edb88
feat(tables): add sort to tables list page
waleedlatif1 Mar 29, 2026
51c9df9
feat(knowledge): add content and owner filters to KB list
waleedlatif1 Mar 29, 2026
12ea734
feat(scheduled-tasks): add status and health filters
waleedlatif1 Mar 29, 2026
a3ffc2f
feat(files): add size and uploaded-by filters to files list
waleedlatif1 Mar 29, 2026
0142c69
feat(tables): add row count, owner, and column type filters
waleedlatif1 Mar 29, 2026
2553cac
improvement(scheduled-tasks): use combobox filter panel matching logs…
waleedlatif1 Mar 29, 2026
9dd3028
improvement(knowledge): use combobox filter panel matching logs UI style
waleedlatif1 Mar 29, 2026
446a665
improvement(files): use combobox filter panel matching logs UI style
waleedlatif1 Mar 29, 2026
c56e3ac
improvement(tables): use combobox filter panel matching logs UI style
waleedlatif1 Mar 29, 2026
f0e988d
feat(settings): add sort to recently deleted page
waleedlatif1 Mar 29, 2026
bf0bcf3
feat(logs): add sort to logs page
waleedlatif1 Mar 29, 2026
ffe6806
improvement(knowledge): upgrade document list filter to combobox style
waleedlatif1 Mar 29, 2026
483c35c
fix(resources): fix missing imports, memoization, and stale refs acro…
waleedlatif1 Mar 29, 2026
c8672f7
improvement(tables): remove column type filter
waleedlatif1 Mar 29, 2026
71794bb
fix(resources): fix filter/sort correctness issues from audit
waleedlatif1 Mar 29, 2026
aeeec6b
fix(chunks): add server-side sort to document chunks API
waleedlatif1 Mar 29, 2026
7b13a1a
perf(resources): memoize filterContent JSX across all resource pages
waleedlatif1 Mar 29, 2026
90bd958
fix(resources): add missing sort options for all visible columns
waleedlatif1 Mar 29, 2026
0318725
whitelabeling updates, sidebar fixes, files bug
waleedlatif1 Mar 29, 2026
432e4d0
increased type safety
waleedlatif1 Mar 29, 2026
f8a26a3
pr fixes
waleedlatif1 Mar 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ import { cn } from '@/lib/core/utils/cn'
const SEARCH_ICON = (
<Search className='pointer-events-none h-[14px] w-[14px] shrink-0 text-[var(--text-icon)]' />
)
const FILTER_ICON = <ListFilter className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
const SORT_ICON = <ArrowUpDown className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />

type SortDirection = 'asc' | 'desc'

Expand Down Expand Up @@ -67,7 +65,12 @@ export interface SearchConfig {
interface ResourceOptionsBarProps {
search?: SearchConfig
sort?: SortConfig
/** Popover content — renders inside a Popover (used by logs, etc.) */
filter?: ReactNode
/** When provided, Filter button acts as a toggle instead of opening a Popover */
onFilterToggle?: () => void
/** Whether the filter is currently active (highlights the toggle button) */
filterActive?: boolean
filterTags?: FilterTag[]
extras?: ReactNode
}
Expand All @@ -76,10 +79,13 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({
search,
sort,
filter,
onFilterToggle,
filterActive,
filterTags,
extras,
}: ResourceOptionsBarProps) {
const hasContent = search || sort || filter || extras || (filterTags && filterTags.length > 0)
const hasContent =
search || sort || filter || onFilterToggle || extras || (filterTags && filterTags.length > 0)
if (!hasContent) return null

return (
Expand All @@ -88,38 +94,53 @@ export const ResourceOptionsBar = memo(function ResourceOptionsBar({
{search && <SearchSection search={search} />}
<div className='flex items-center gap-1.5'>
{extras}
{filterTags?.map((tag) => (
{filterTags?.map((tag, i) => (
<Button
key={tag.label}
key={`${tag.label}-${i}`}
variant='subtle'
className='px-2 py-1 text-caption'
className='max-w-[200px] px-2 py-1 text-caption'
onClick={tag.onRemove}
>
{tag.label}
<span className='ml-1 text-[var(--text-icon)] text-micro'>✕</span>
<span className='truncate'>{tag.label}</span>
<span className='ml-1 shrink-0 text-[var(--text-icon)] text-micro'>✕</span>
</Button>
))}
{filter && (
{onFilterToggle ? (
<Button
variant='subtle'
className={cn(
'px-2 py-1 text-caption',
filterActive && 'bg-[var(--surface-3)] text-[var(--text-primary)]'
)}
onClick={onFilterToggle}
>
<ListFilter
className={cn(
'mr-1.5 h-[14px] w-[14px]',
filterActive ? 'text-[var(--text-primary)]' : 'text-[var(--text-icon)]'
)}
/>
Filter
</Button>
) : filter ? (
<PopoverPrimitive.Root>
<PopoverPrimitive.Trigger asChild>
<Button variant='subtle' className='px-2 py-1 text-caption'>
{FILTER_ICON}
<ListFilter className='mr-1.5 h-[14px] w-[14px] text-[var(--text-icon)]' />
Filter
</Button>
</PopoverPrimitive.Trigger>
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
align='start'
sideOffset={6}
className={cn(
'z-50 rounded-lg border border-[var(--border)] bg-[var(--bg)] shadow-sm'
)}
className='z-50 rounded-lg border border-[var(--border)] bg-[var(--bg)] shadow-sm'
>
{filter}
</PopoverPrimitive.Content>
</PopoverPrimitive.Portal>
</PopoverPrimitive.Root>
)}
) : null}
{sort && <SortDropdown config={sort} />}
</div>
</div>
Expand Down Expand Up @@ -213,8 +234,19 @@ const SortDropdown = memo(function SortDropdown({ config }: { config: SortConfig
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='subtle' className='px-2 py-1 text-caption'>
{SORT_ICON}
<Button
variant='subtle'
className={cn(
'px-2 py-1 text-caption',
active && 'bg-[var(--surface-3)] text-[var(--text-primary)]'
)}
>
<ArrowUpDown
className={cn(
'mr-1.5 h-[14px] w-[14px]',
active ? 'text-[var(--text-primary)]' : 'text-[var(--text-icon)]'
)}
/>
Sort
</Button>
</DropdownMenuTrigger>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useCallback, useMemo, useState } from 'react'
import { memo, useCallback, useMemo, useState } from 'react'
import { X } from 'lucide-react'
import { nanoid } from 'nanoid'
import {
Expand All @@ -11,29 +11,26 @@ import {
DropdownMenuTrigger,
} from '@/components/emcn'
import { ChevronDown, Plus } from '@/components/emcn/icons'
import { cn } from '@/lib/core/utils/cn'
import type { Filter, FilterRule } from '@/lib/table'
import { COMPARISON_OPERATORS } from '@/lib/table/query-builder/constants'
import { filterRulesToFilter } from '@/lib/table/query-builder/converters'

const OPERATOR_LABELS: Record<string, string> = {
eq: '=',
ne: '≠',
gt: '>',
gte: '≥',
lt: '<',
lte: '≤',
contains: '∋',
in: '∈',
} as const
import { filterRulesToFilter, filterToRules } from '@/lib/table/query-builder/converters'

const OPERATOR_LABELS = Object.fromEntries(
COMPARISON_OPERATORS.map((op) => [op.value, op.label])
) as Record<string, string>

interface TableFilterProps {
columns: Array<{ name: string; type: string }>
filter: Filter | null
onApply: (filter: Filter | null) => void
onClose: () => void
}

export function TableFilter({ columns, onApply }: TableFilterProps) {
const [rules, setRules] = useState<FilterRule[]>(() => [createRule(columns)])
export function TableFilter({ columns, filter, onApply, onClose }: TableFilterProps) {
const [rules, setRules] = useState<FilterRule[]>(() => {
const fromFilter = filterToRules(filter)
return fromFilter.length > 0 ? fromFilter : [createRule(columns)]
})

const columnOptions = useMemo(
() => columns.map((col) => ({ value: col.name, label: col.name })),
Expand All @@ -46,71 +43,122 @@ export function TableFilter({ columns, onApply }: TableFilterProps) {

const handleRemove = useCallback(
(id: string) => {
setRules((prev) => {
const next = prev.filter((r) => r.id !== id)
return next.length === 0 ? [createRule(columns)] : next
})
const next = rules.filter((r) => r.id !== id)
if (next.length === 0) {
onApply(null)
onClose()
setRules([createRule(columns)])
} else {
setRules(next)
}
Comment thread
waleedlatif1 marked this conversation as resolved.
},
[columns]
[columns, onApply, onClose, rules]
Comment thread
waleedlatif1 marked this conversation as resolved.
Outdated
)

const handleUpdate = useCallback((id: string, field: keyof FilterRule, value: string) => {
setRules((prev) => prev.map((r) => (r.id === id ? { ...r, [field]: value } : r)))
}, [])

const handleToggleLogical = useCallback((id: string) => {
setRules((prev) =>
prev.map((r) =>
r.id === id ? { ...r, logicalOperator: r.logicalOperator === 'and' ? 'or' : 'and' } : r
)
)
}, [])

const handleApply = useCallback(() => {
const validRules = rules.filter((r) => r.column && r.value)
onApply(filterRulesToFilter(validRules))
}, [rules, onApply])

return (
<div className='flex flex-col gap-1.5 p-2'>
{rules.map((rule) => (
<FilterRuleRow
key={rule.id}
rule={rule}
columns={columnOptions}
onUpdate={handleUpdate}
onRemove={handleRemove}
onApply={handleApply}
/>
))}

<div className='flex items-center justify-between gap-3'>
<Button
variant='ghost'
size='sm'
onClick={handleAdd}
className={cn(
'border border-[var(--border)] border-dashed px-2 py-[3px] text-[var(--text-secondary)] text-xs'
)}
>
<Plus className='mr-1 h-[10px] w-[10px]' />
Add filter
</Button>
const handleClear = useCallback(() => {
setRules([createRule(columns)])
onApply(null)
}, [columns, onApply])

<Button variant='default' size='sm' onClick={handleApply} className='text-xs'>
Apply filter
</Button>
return (
<div className='border-[var(--border)] border-b bg-[var(--bg)] px-4 py-2'>
<div className='flex flex-col gap-1'>
{rules.map((rule, index) => (
<FilterRuleRow
key={rule.id}
rule={rule}
isFirst={index === 0}
columns={columnOptions}
onUpdate={handleUpdate}
onRemove={handleRemove}
onApply={handleApply}
onToggleLogical={handleToggleLogical}
/>
))}

<div className='mt-1 flex items-center justify-between'>
<Button
variant='ghost'
size='sm'
onClick={handleAdd}
className='px-2 py-1 text-[var(--text-secondary)] text-xs'
>
<Plus className='mr-1 h-[10px] w-[10px]' />
Add filter
</Button>
<div className='flex items-center gap-1.5'>
{filter !== null && (
<Button
variant='ghost'
size='sm'
onClick={handleClear}
className='px-2 py-1 text-[var(--text-secondary)] text-xs'
>
Clear filters
</Button>
)}
<Button variant='default' size='sm' onClick={handleApply} className='text-xs'>
Apply filter
</Button>
</div>
</div>
</div>
</div>
)
}

interface FilterRuleRowProps {
rule: FilterRule
isFirst: boolean
columns: Array<{ value: string; label: string }>
onUpdate: (id: string, field: keyof FilterRule, value: string) => void
onRemove: (id: string) => void
onApply: () => void
onToggleLogical: (id: string) => void
}

function FilterRuleRow({ rule, columns, onUpdate, onRemove, onApply }: FilterRuleRowProps) {
const FilterRuleRow = memo(function FilterRuleRow({
rule,
isFirst,
columns,
onUpdate,
onRemove,
onApply,
onToggleLogical,
}: FilterRuleRowProps) {
return (
<div className='flex items-center gap-1'>
<div className='flex items-center gap-1.5'>
{isFirst ? (
<span className='w-[42px] shrink-0 text-right text-[var(--text-muted)] text-xs'>Where</span>
) : (
<button
onClick={() => onToggleLogical(rule.id)}
className='w-[42px] shrink-0 rounded-full py-0.5 text-right font-medium text-[10px] text-[var(--text-muted)] uppercase tracking-wide transition-colors hover:text-[var(--text-secondary)]'
>
{rule.logicalOperator}
</button>
)}

<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className='flex h-[30px] min-w-[100px] items-center justify-between rounded-[5px] border border-[var(--border)] bg-transparent px-2 text-[var(--text-secondary)] text-xs outline-none hover-hover:border-[var(--border-1)]'>
<button className='flex h-[28px] min-w-[100px] items-center justify-between rounded-[5px] border border-[var(--border)] bg-transparent px-2 text-[var(--text-secondary)] text-xs outline-none hover-hover:border-[var(--border-1)]'>
<span className='truncate'>{rule.column || 'Column'}</span>
<ChevronDown className='ml-1 h-[10px] w-[10px] shrink-0 text-[var(--text-icon)]' />
</button>
Expand All @@ -129,8 +177,8 @@ function FilterRuleRow({ rule, columns, onUpdate, onRemove, onApply }: FilterRul

<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className='flex h-[30px] min-w-[50px] items-center justify-between rounded-[5px] border border-[var(--border)] bg-transparent px-2 text-[var(--text-secondary)] text-xs outline-none hover-hover:border-[var(--border-1)]'>
<span>{OPERATOR_LABELS[rule.operator] ?? rule.operator}</span>
<button className='flex h-[28px] min-w-[90px] items-center justify-between rounded-[5px] border border-[var(--border)] bg-transparent px-2 text-[var(--text-secondary)] text-xs outline-none hover-hover:border-[var(--border-1)]'>
<span className='truncate'>{OPERATOR_LABELS[rule.operator] ?? rule.operator}</span>
<ChevronDown className='ml-1 h-[10px] w-[10px] shrink-0 text-[var(--text-icon)]' />
</button>
</DropdownMenuTrigger>
Expand All @@ -151,25 +199,21 @@ function FilterRuleRow({ rule, columns, onUpdate, onRemove, onApply }: FilterRul
value={rule.value}
onChange={(e) => onUpdate(rule.id, 'value', e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleApply()
if (e.key === 'Enter') onApply()
}}
placeholder='Enter a value'
className='h-[30px] min-w-[160px] flex-1 rounded-[5px] border border-[var(--border)] bg-transparent px-2 text-[var(--text-secondary)] text-xs outline-none placeholder:text-[var(--text-subtle)] hover-hover:border-[var(--border-1)] focus:border-[var(--border-1)]'
className='h-[28px] flex-1 rounded-[5px] border border-[var(--border)] bg-transparent px-2 text-[var(--text-secondary)] text-xs outline-none placeholder:text-[var(--text-subtle)] hover-hover:border-[var(--border-1)] focus:border-[var(--border-1)]'
/>

<button
onClick={() => onRemove(rule.id)}
className='flex h-[30px] w-[30px] shrink-0 items-center justify-center rounded-[5px] text-[var(--text-tertiary)] transition-colors hover-hover:bg-[var(--surface-4)] hover-hover:text-[var(--text-primary)]'
className='flex h-[28px] w-[28px] shrink-0 items-center justify-center rounded-[5px] text-[var(--text-tertiary)] transition-colors hover-hover:bg-[var(--surface-4)] hover-hover:text-[var(--text-primary)]'
>
<X className='h-[12px] w-[12px]' />
</button>
</div>
)

function handleApply() {
onApply()
}
}
})

function createRule(columns: Array<{ name: string }>): FilterRule {
return {
Expand Down
Loading
Loading