Skip to content

Commit a61dc23

Browse files
committed
improvement: tables, dropdown
1 parent 12c1ede commit a61dc23

File tree

20 files changed

+926
-222
lines changed

20 files changed

+926
-222
lines changed

apps/sim/app/_styles/globals.css

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@
123123
--brand-secondary: #33b4ff;
124124
--brand-tertiary: #22c55e;
125125
--brand-tertiary-2: #32bd7e;
126+
--selection: #1a5cf6;
126127
--warning: #ea580c;
127128

128129
/* Utility */
@@ -245,6 +246,7 @@
245246
--brand-secondary: #33b4ff;
246247
--brand-tertiary: #22c55e;
247248
--brand-tertiary-2: #32bd7e;
249+
--selection: #4b83f7;
248250
--warning: #ff6600;
249251

250252
/* Utility */
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export type { ActiveFilter, ColumnOption, FilterConfig, SortConfig } from './resource-options-bar'
12
export { ResourceOptionsBar } from './resource-options-bar'
Lines changed: 165 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,79 @@
11
import type { ReactNode } from 'react'
2-
import { ArrowUpDown, Button, ListFilter, Search } from '@/components/emcn'
2+
import {
3+
ArrowDown,
4+
ArrowUp,
5+
ArrowUpDown,
6+
Button,
7+
DropdownMenu,
8+
DropdownMenuCheckboxItem,
9+
DropdownMenuContent,
10+
DropdownMenuItem,
11+
DropdownMenuSeparator,
12+
DropdownMenuSub,
13+
DropdownMenuSubContent,
14+
DropdownMenuSubTrigger,
15+
DropdownMenuTrigger,
16+
ListFilter,
17+
Search,
18+
} from '@/components/emcn'
319
import { cn } from '@/lib/core/utils/cn'
420

21+
type SortDirection = 'asc' | 'desc'
22+
23+
export interface ColumnOption {
24+
id: string
25+
label: string
26+
type?: string
27+
icon?: React.ElementType
28+
}
29+
30+
export interface SortConfig {
31+
options: ColumnOption[]
32+
active: { column: string; direction: SortDirection } | null
33+
onSort: (column: string, direction: SortDirection) => void
34+
onClear?: () => void
35+
}
36+
37+
export interface ActiveFilter {
38+
column: string
39+
operator: string
40+
}
41+
42+
export interface FilterConfig {
43+
options: ColumnOption[]
44+
active: ActiveFilter[]
45+
onToggle: (column: string, operator: string) => void
46+
onClear?: () => void
47+
}
48+
49+
const DEFAULT_FILTER_OPERATORS = [
50+
{ id: 'empty', label: 'Is empty' },
51+
{ id: 'not_empty', label: 'Is not empty' },
52+
] as const
53+
54+
const BOOLEAN_FILTER_OPERATORS = [
55+
{ id: 'eq_true', label: 'Is true' },
56+
{ id: 'eq_false', label: 'Is false' },
57+
] as const
58+
559
interface ResourceOptionsBarProps {
660
search?: {
761
value: string
862
onChange: (value: string) => void
963
placeholder?: string
1064
}
11-
onSort?: () => void
12-
onFilter?: () => void
65+
sort?: SortConfig
66+
filter?: FilterConfig
1367
toolbarActions?: ReactNode
1468
}
1569

1670
export function ResourceOptionsBar({
1771
search,
18-
onSort,
19-
onFilter,
72+
sort,
73+
filter,
2074
toolbarActions,
2175
}: ResourceOptionsBarProps) {
22-
const hasContent = search || onSort || onFilter || toolbarActions
76+
const hasContent = search || sort || filter || toolbarActions
2377
if (!hasContent) return null
2478

2579
return (
@@ -43,21 +97,114 @@ export function ResourceOptionsBar({
4397
</div>
4498
)}
4599
<div className='flex items-center gap-[6px]'>
46-
{onFilter && (
47-
<Button variant='subtle' className='px-[8px] py-[4px] text-[12px]' onClick={onFilter}>
48-
<ListFilter className='mr-[6px] h-[14px] w-[14px] text-[var(--text-icon)]' />
49-
Filter
50-
</Button>
51-
)}
52-
{onSort && (
53-
<Button variant='subtle' className='px-[8px] py-[4px] text-[12px]' onClick={onSort}>
54-
<ArrowUpDown className='mr-[6px] h-[14px] w-[14px] text-[var(--text-icon)]' />
55-
Sort
56-
</Button>
57-
)}
100+
{filter && <FilterDropdown config={filter} />}
101+
{sort && <SortDropdown config={sort} />}
58102
{toolbarActions}
59103
</div>
60104
</div>
61105
</div>
62106
)
63107
}
108+
109+
function SortDropdown({ config }: { config: SortConfig }) {
110+
const { options, active, onSort, onClear } = config
111+
112+
return (
113+
<DropdownMenu>
114+
<DropdownMenuTrigger asChild>
115+
<Button variant='subtle' className='px-[8px] py-[4px] text-[12px]'>
116+
<ArrowUpDown className='mr-[6px] h-[14px] w-[14px] text-[var(--text-icon)]' />
117+
Sort
118+
</Button>
119+
</DropdownMenuTrigger>
120+
<DropdownMenuContent align='end'>
121+
{options.map((option) => {
122+
const isActive = active?.column === option.id
123+
const Icon = option.icon
124+
const DirectionIcon = isActive ? (active.direction === 'asc' ? ArrowUp : ArrowDown) : null
125+
126+
return (
127+
<DropdownMenuItem
128+
key={option.id}
129+
onSelect={() => {
130+
if (isActive) {
131+
onSort(option.id, active.direction === 'asc' ? 'desc' : 'asc')
132+
} else {
133+
onSort(option.id, 'desc')
134+
}
135+
}}
136+
>
137+
{Icon && <Icon />}
138+
{option.label}
139+
{DirectionIcon && (
140+
<DirectionIcon className='ml-auto h-[12px] w-[12px] text-[var(--text-tertiary)]' />
141+
)}
142+
</DropdownMenuItem>
143+
)
144+
})}
145+
{active && onClear && (
146+
<>
147+
<DropdownMenuSeparator />
148+
<DropdownMenuItem onSelect={onClear} className='text-[var(--text-tertiary)]'>
149+
Clear sort
150+
</DropdownMenuItem>
151+
</>
152+
)}
153+
</DropdownMenuContent>
154+
</DropdownMenu>
155+
)
156+
}
157+
158+
function FilterDropdown({ config }: { config: FilterConfig }) {
159+
const { options, active, onToggle, onClear } = config
160+
161+
return (
162+
<DropdownMenu>
163+
<DropdownMenuTrigger asChild>
164+
<Button variant='subtle' className='px-[8px] py-[4px] text-[12px]'>
165+
<ListFilter className='mr-[6px] h-[14px] w-[14px] text-[var(--text-icon)]' />
166+
Filter
167+
</Button>
168+
</DropdownMenuTrigger>
169+
<DropdownMenuContent align='end'>
170+
{options.map((option) => {
171+
const operators =
172+
option.type === 'boolean' ? BOOLEAN_FILTER_OPERATORS : DEFAULT_FILTER_OPERATORS
173+
const activeFilter = active.find((f) => f.column === option.id)
174+
const Icon = option.icon
175+
176+
return (
177+
<DropdownMenuSub key={option.id}>
178+
<DropdownMenuSubTrigger>
179+
{Icon && <Icon />}
180+
{option.label}
181+
{activeFilter && (
182+
<span className='ml-auto h-[6px] w-[6px] rounded-full bg-[var(--text-tertiary)]' />
183+
)}
184+
</DropdownMenuSubTrigger>
185+
<DropdownMenuSubContent>
186+
{operators.map((op) => (
187+
<DropdownMenuCheckboxItem
188+
key={op.id}
189+
checked={activeFilter?.operator === op.id}
190+
onCheckedChange={() => onToggle(option.id, op.id)}
191+
>
192+
{op.label}
193+
</DropdownMenuCheckboxItem>
194+
))}
195+
</DropdownMenuSubContent>
196+
</DropdownMenuSub>
197+
)
198+
})}
199+
{active.length > 0 && onClear && (
200+
<>
201+
<DropdownMenuSeparator />
202+
<DropdownMenuItem onSelect={onClear} className='text-[var(--text-tertiary)]'>
203+
Clear all filters
204+
</DropdownMenuItem>
205+
</>
206+
)}
207+
</DropdownMenuContent>
208+
</DropdownMenu>
209+
)
210+
}

apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx

Lines changed: 21 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useCallback, useMemo, useRef, useState } from 'react'
55
import { ArrowDown, ArrowUp, Button, Plus, Skeleton } from '@/components/emcn'
66
import { cn } from '@/lib/core/utils/cn'
77
import { ResourceHeader } from './components/resource-header'
8+
import type { SortConfig } from './components/resource-options-bar'
89
import { ResourceOptionsBar } from './components/resource-options-bar'
910

1011
export interface ResourceColumn {
@@ -37,8 +38,6 @@ interface ResourceProps {
3738
placeholder?: string
3839
}
3940
defaultSort: string
40-
onSort?: () => void
41-
onFilter?: () => void
4241
toolbarActions?: ReactNode
4342
columns: ResourceColumn[]
4443
rows: ResourceRow[]
@@ -61,8 +60,6 @@ export function Resource({
6160
create,
6261
search,
6362
defaultSort,
64-
onSort,
65-
onFilter,
6663
toolbarActions,
6764
columns,
6865
rows,
@@ -84,13 +81,19 @@ export function Resource({
8481
}
8582
}, [])
8683

87-
const handleColumnSort = useCallback((columnId: string) => {
88-
setSort((prev) => {
89-
if (prev.column !== columnId) return { column: columnId, direction: 'desc' }
90-
return { column: columnId, direction: prev.direction === 'desc' ? 'asc' : 'desc' }
91-
})
84+
const handleSort = useCallback((column: string, direction: 'asc' | 'desc') => {
85+
setSort({ column, direction })
9286
}, [])
9387

88+
const sortConfig = useMemo<SortConfig>(
89+
() => ({
90+
options: columns.map((col) => ({ id: col.id, label: col.header })),
91+
active: sort,
92+
onSort: handleSort,
93+
}),
94+
[columns, sort, handleSort]
95+
)
96+
9497
const sortedRows = useMemo(() => {
9598
return [...rows].sort((a, b) => {
9699
const col = sort.column
@@ -110,12 +113,7 @@ export function Resource({
110113
onContextMenu={onContextMenu}
111114
>
112115
<ResourceHeader icon={icon} title={title} create={create} />
113-
<ResourceOptionsBar
114-
search={search}
115-
onSort={onSort}
116-
onFilter={onFilter}
117-
toolbarActions={toolbarActions}
118-
/>
116+
<ResourceOptionsBar search={search} sort={sortConfig} toolbarActions={toolbarActions} />
119117

120118
{isLoading ? (
121119
<DataTableSkeleton columns={columns} rowCount={loadingRows} />
@@ -127,16 +125,22 @@ export function Resource({
127125
<thead className='shadow-[inset_0_-1px_0_var(--border)]'>
128126
<tr>
129127
{columns.map((col) => {
128+
const isActive = sort.column === col.id
130129
const SortIcon = sort.direction === 'asc' ? ArrowUp : ArrowDown
131130
return (
132131
<th key={col.id} className='h-10 px-[16px] py-[6px] text-left align-middle'>
133132
<Button
134133
variant='subtle'
135134
className='px-[8px] py-[4px] font-base text-[var(--text-muted)] hover:text-[var(--text-muted)]'
136-
onClick={() => handleColumnSort(col.id)}
135+
onClick={() =>
136+
handleSort(
137+
col.id,
138+
isActive ? (sort.direction === 'desc' ? 'asc' : 'desc') : 'desc'
139+
)
140+
}
137141
>
138142
{col.header}
139-
{sort.column === col.id && (
143+
{isActive && (
140144
<SortIcon className='ml-[4px] h-[12px] w-[12px] text-[var(--text-icon)]' />
141145
)}
142146
</Button>

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -204,8 +204,6 @@ export function Knowledge() {
204204
placeholder: 'Search knowledge bases...',
205205
}}
206206
defaultSort='created'
207-
onSort={() => {}}
208-
onFilter={() => {}}
209207
columns={COLUMNS}
210208
rows={rows}
211209
onRowClick={handleRowClick}

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

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -189,8 +189,6 @@ export function Schedules() {
189189
placeholder: 'Search schedules...',
190190
}}
191191
defaultSort='nextRun'
192-
onSort={() => {}}
193-
onFilter={() => {}}
194192
columns={COLUMNS}
195193
rows={rows}
196194
onRowClick={handleRowClick}

0 commit comments

Comments
 (0)